Merge branch 'sec-access-panel'
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index b45853d..7c9fcbf 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -10,7 +10,7 @@
 System Groups
 -------------
 
-Gerrit comes with 3 system groups, with special access privileges
+Gerrit comes with 4 system groups, with special access privileges
 and membership management.  The identity of these groups is set
 in the `system_config` table within the database, so the groups
 can be renamed after installation if desired.
@@ -65,6 +65,21 @@
 Registered users are always permitted to make and publish comments
 on any change in any project they have `Read Access` to.
 
+Project Owners
+~~~~~~~~~~~~~~
+
+Access rights assigned to this group are always evaluated within the
+context of a project and are resolved to access rights for all users
+which own the project.
+
+By assigning access rights to this group on a parent project Gerrit
+administrators can define a set of default access rights for project
+owners. Child projects inherit these access rights where they are
+resolved to the users that own the child project.
+Having default access rights for projects owners assigned on a parent
+project may avoid the need to initially configure access rights for
+newly created child projects.
+
 
 Account Groups
 --------------
diff --git a/Documentation/cmd-create-project.txt b/Documentation/cmd-create-project.txt
index 3da01c4..f698d5c 100644
--- a/Documentation/cmd-create-project.txt
+++ b/Documentation/cmd-create-project.txt
@@ -13,10 +13,13 @@
 [--branch <REF>] \
 [\--owner <GROUP> ...] \
 [\--parent <NAME>] \
+[\--permissions-only] \
 [\--description <DESC>] \
 [\--submit-type <TYPE>] \
+[\--use-content-merge] \
 [\--use-contributor-agreements] \
 [\--use-signed-off-by]
+[\--empty-commit]
 
 DESCRIPTION
 -----------
@@ -68,6 +71,11 @@
 	through. If not specified, the parent is set to the default
 	project `\-- All Projects \--`.
 
+\--permissions-only::
+	Create the project only to serve as a parent for other
+	projects.  The new project's Git repository will not be
+	initialized, and cannot be cloned.
+
 \--description::
 	Initial description of the project.  If not specified,
 	no description is stored.
@@ -89,6 +97,13 @@
 Defaults to MERGE_IF_NECESSARY.  For more details see
 link:project-setup.html#submit_type[Change Submit Actions].
 
+\--use-content-merge::
+	If enabled, Gerrit will try to perform a 3-way merge of text
+	file content when a file has been modified by both the
+	destination branch and the change being submitted.  This
+	option only takes effect if submit type is not
+	FAST_FORWARD_ONLY.  Disabled by default.
+
 \--use-contributor-agreements::
 	If enabled, authors must complete a contributor agreement
 	on the site before pushing any commits or changes to this
@@ -99,6 +114,10 @@
 	from either the author or the uploader in the commit message.
 	Disabled by default.
 
+\--empty-commit:
+	Creates an initial empty commit for the Git repository of the
+	project that is newly created.
+
 
 EXAMPLES
 --------
diff --git a/Documentation/cmd-query.txt b/Documentation/cmd-query.txt
index 76a9b7d..9047202 100644
--- a/Documentation/cmd-query.txt
+++ b/Documentation/cmd-query.txt
@@ -11,7 +11,7 @@
 'ssh' -p <port> <host> 'gerrit query' \
 [\--format {TEXT | JSON}] \
 [\--current-patch-set] \
-[\--patch-sets] \
+[\--patch-sets|--all-approvals] \
 [\--] \
 <query> \
 [limit:<n>] \
@@ -47,6 +47,12 @@
 	the \--current-patch-set flag then the current patch set
 	information will be output twice, once in each field.
 
+\--all-approvals::
+	Include information about all patch sets along with the
+	approval information for each patch set.  If combined with
+	the \--current-patch-set flag then the current patch set
+	information will be output twice, once in each field.
+
 limit:<n>::
 	Maximum number of results to return.  This is actually a
 	query operator, and not a command line option.	If more
diff --git a/Documentation/cmd-stream-events.txt b/Documentation/cmd-stream-events.txt
index aae908a..fb54f67 100644
--- a/Documentation/cmd-stream-events.txt
+++ b/Documentation/cmd-stream-events.txt
@@ -42,7 +42,7 @@
 ------
 The JSON messages consist of nested objects referencing the *change*,
 *patchset*, *account* involved, and other attributes as appropriate.
-The currently supported message types are *patchset-added*,
+The currently supported message types are *patchset-created*,
 *comment-added*, *change-merged*, and *change-abandoned*.
 
 Note that any field may be missing in the JSON messages, so consumers of
@@ -50,9 +50,9 @@
 
 Events
 ~~~~~~
-Patchset Added
-^^^^^^^^^^^^^^
-type:: "patchset-added"
+Patchset Created
+^^^^^^^^^^^^^^^^
+type:: "patchset-created"
 
 change:: link:json.html#change[change attribute]
 
@@ -102,6 +102,15 @@
 
 comment:: Comment text author had written
 
+Ref Updated
+^^^^^^^^^^^
+type:: "ref-updated"
+
+submitter:: link:json.html#account[account attribute]
+
+refUpdate:: link:json.html#refupdate[refupdate attribute]
+
+
 SEE ALSO
 --------
 
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index ebd1c7f..864092e 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -56,6 +56,19 @@
 is also pulled from LDAP, making any LDAP groups that a user is a
 member of available as groups in Gerrit.
 +
+* `CLIENT_SSL_CERT_LDAP`
++
+This authentication type is actually kind of SSO. Gerrit will configure
+Jetty's SSL channel to request client's SSL certificate. For this
+authentication to work a Gerrit administrator has to import the root
+certificate of the trust chain used to issue the client's certificate
+into the <review-site>/etc/keystore.
+After the authentication is done Gerrit will obtain basic user
+registration (name and email) from LDAP, and some group memberships.
+Therefore, the "_LDAP" suffix in the name of this authentication type.
+This authentication type can only be used under hosted daemon mode, and
+the httpd.listenUrl must use https:// as the protocol.
++
 * `LDAP`
 +
 Gerrit prompts the user to enter a username and a password, which
@@ -578,6 +591,27 @@
 +
 Default on JGit is 128 file descriptors on all platforms.
 
+[[core.streamFileThreshold]]core.streamFileThreshold::
++
+Largest object size, in bytes, that JGit will allocate as a
+contiguous byte array.  Any file revision larger than this threshold
+will have to be streamed, typically requiring the use of temporary
+files under '$GIT_DIR/objects' to implement psuedo-random access
+during delta decompression.
++
+Servers with very high traffic should set this to be larger than
+the size of their common big files.  For example a server managing
+the Android platform typically has to deal with ~10-12 MiB XML
+files, so `15 m` would be a reasonable setting in that environment.
+Setting this too high may cause the JVM to run out of heap space
+when handling very big binary files, such as device firmware or
+CD-ROM ISO images.
++
+Default is 50 MiB on all platforms.  Prior to Gerrit 2.1.6,
+this value was effectively 2047 MiB.
++
+Common unit suffixes of 'k', 'm', or 'g' are supported.
+
 [[core.packedGitMmap]]core.packedGitMmap::
 +
 When true, JGit will use `mmap()` rather than `malloc()+read()`
@@ -797,6 +831,11 @@
 by the system administrator, and might not even be running on the
 same host as Gerrit.
 
+[[gerrit.replicateOnStartup]]gerrit.replicateOnStartup::
++
+If true, replicates to all remotes on startup to ensure they are
+in-sync with this server.  By default, true.
+
 [[gitweb]]Section gitweb
 ~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -1079,8 +1118,9 @@
 ~~~~~~~~~~~~~~~~~~~~
 
 LDAP integration is only enabled if `auth.type` was set to
-`HTTP_LDAP` or `LDAP`.  See above for a detailed description of
-the auth.type settings and their implications.
+`HTTP_LDAP`, `LDAP` or `CLIENT_SSL_CERT_LDAP`.  See above for a
+detailed description of the auth.type settings and their
+implications.
 
 An example LDAP configuration follows, and then discussion of
 the parameters introduced here.  Suitable defaults for most
@@ -1315,6 +1355,53 @@
   safe = true
 ----
 
+
+[[pack]]Section pack
+~~~~~~~~~~~~~~~~~~~~
+Global settings controlling how Gerrit Code Review creates pack
+streams for Git clients running clone, fetch, or pull.  Most of these
+variables are per-client request, and thus should be carefully set
+given the expected concurrent request load and available CPU and
+memory resources.
+
+[[pack.deltacompression]]pack.deltacompression::
++
+If true, delta compression between objects is enabled.  This may
+result in a smaller overall transfer for the client, but requires
+more server memory and CPU time.
++
+False (off) by default, matching Gerrit Code Review 2.1.4.
+
+[[pack.threads]]pack.threads::
++
+Maximum number of threads to use for delta compression (if enabled).
+This is per-client request.  If set to 0 then the number of CPUs is
+auto-detected and one thread per CPU is used, per client request.
++
+By default, 1.
+
+
+[[receive]]Section receive
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+Sets the group of users allowed to execute 'receive-pack' on the
+server, 'receive-pack' is what runs on the server during a user's
+push or repo upload command.
+
+----
+[receive]
+  allowGroup = GROUP_ALLOWED_TO_EXECUTE
+  allowGroup = YET_ANOTHER_GROUP_ALLOWED_TO_EXECUTE
+----
+
+[[receive.allowGroup]]receive.allowGroup::
++
+Name of the groups of users that are allowed to execute
+'receive-pack' on the server. One or more groups can be set.
++
+If no groups are added, any user will be allowed to execute
+'receive-pack' on the server.
+
+
 [[repository]]Section repository
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 Repositories in this sense are the same as projects.
@@ -1516,6 +1603,41 @@
 +
 By default, 1 plus the number of CPUs available to the JVM.
 
+[[sshd.maxAuthTries]]sshd.maxAuthTries::
++
+Maximum number of authentication attempts before the server
+disconnects the client.  Each public key that a client has loaded
+into its local agent counts as one auth request.  Users can work
+around the server's limit by loading less keys into their agent,
+or selecting a specific key in their `~/.ssh/config` file with
+the `IdentityFile` option.
++
+By default, 6.
+
+[[sshd.loginGraceTime]]sshd.loginGraceTime::
++
+Time in seconds that a client has to authenticate before the server
+automatically terminates their connection.  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, 2 minutes.
+
+[[sshd.maxConnectionsPerUser]]sshd.maxConnectionsPerUser::
++
+Maximum number of concurrent SSH sessions that a user account
+may open at one time.  This is the number of distinct SSH logins
+the each user may have active at one time, and is not related to
+the number of commands a user may issue over a single connection.
+If set to 0, there is no limit.
++
+By default, 64.
+
 [[sshd.cipher]]sshd.cipher::
 +
 Available ciphers.  To permit multiple ciphers, specify multiple
@@ -1650,6 +1772,28 @@
 +
 Defaults to 0 seconds, wait indefinitely.
 
+
+[[upload]]Section upload
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+Sets the group of users allowed to execute 'upload-pack' on the
+server, 'upload-pack' is what runs on the server during a user's
+fetch, clone or repo sync command.
+
+----
+[upload]
+  allowGroup = GROUP_ALLOWED_TO_EXECUTE
+  allowGroup = YET_ANOTHER_GROUP_ALLOWED_TO_EXECUTE
+----
+
+[[upload.allowGroup]]upload.allowGroup::
++
+Name of the groups of users that are allowed to execute 'upload-pack'
+on the server. One or more groups can be set.
++
+If no groups are added, any user will be allowed to execute
+'upload-pack' on the server.
+
+
 [[user]] Section user
 ~~~~~~~~~~~~~~~~~~~~~
 
diff --git a/Documentation/config-hooks.txt b/Documentation/config-hooks.txt
index e271ba8..fd2ae82 100644
--- a/Documentation/config-hooks.txt
+++ b/Documentation/config-hooks.txt
@@ -66,6 +66,15 @@
   change-restored --change <change id> --change-url <change url> --project <project name> --branch <branch> --restorer <restorer> --reason <reason>
 ====
 
+ref-updated
+~~~~~~~~~~~
+
+Called whenever a ref has been updated.
+
+====
+  ref-updated --oldrev <old rev> --newrev <new rev> --refname <ref name> --project <project name> --submitter <submitter>
+====
+
 
 Configuration Settings
 ----------------------
diff --git a/Documentation/config-mail.txt b/Documentation/config-mail.txt
new file mode 100644
index 0000000..168bbfe
--- /dev/null
+++ b/Documentation/config-mail.txt
@@ -0,0 +1,172 @@
+Gerrit Code Review - Mail Templates
+===================================
+
+Gerrit uses velocity templates for the bulk of the standard mails it sends out.
+There are builtin default templates which are used if they are not overridden.
+These defaults are also provided as examples so that administrators may copy
+them and easily modify them to tweak their contents.
+
+
+Template Locations and Extensions:
+----------------------------------
+
+The default example templates reside under:  `'$site_path'/etc/mail` and are
+terminated with the double extension `.vm.example`. Modifying these example
+files will have no effect on the behavior of Gerrit.  However, copying an
+example template to an equivalently named file without the `.example` extension
+and modifying it will allow an administrator to customize the template.
+
+
+Supported Mail Templates:
+-------------------------
+
+Each mail that Gerrit sends out is controlled by at least one template, these
+are listed below.  Change emails are influenced by two additional templates,
+one to set the subject line, and one to set the footer which gets appended to
+all the change emails (see `ChangeSubject.vm` and `ChangeFooter.vm` below.)
+
+Abandoned.vm
+~~~~~~~~~~~~
+
+The `Abandoned.vm` template will determine the contents of the email related
+to a change being abandoned.  It is a `ChangeEmail`: see `ChangeSubject.vm` and
+`ChangeFooter.vm`.
+
+ChangeFooter.vm
+~~~~~~~~~~~~~~~
+
+The `ChangeFooter.vm` template will determine the contents of the footer
+text that will be appended to emails related to changes (all `ChangeEmails)`.
+
+ChangeSubject.vm
+~~~~~~~~~~~~~~~~
+
+The `ChangeSubject.vm` template will determine the contents of the email
+subject line for ALL emails related to changes.
+
+Comment.vm
+~~~~~~~~~~
+
+The `Comment.vm` template will determine the contents of the email related to
+a user submitting comments on changes.  It is a `ChangeEmail`: see
+
+Merged.vm
+~~~~~~~~~
+
+The `Merged.vm` template will determine the contents of the email related to
+a change successfully merged to the head.  It is a `ChangeEmail`: see
+`ChangeSubject.vm` and `ChangeFooter.vm`.
+
+MergeFail.vm
+~~~~~~~~~~~~
+
+The `MergeFail.vm` template will determine the contents of the email related
+to a failure upon attempting to merge a change to the head.  It is a
+
+NewChange.vm
+~~~~~~~~~~~~
+
+The `NewChange.vm` template will determine the contents of the email related
+to a user submitting a new change for review. It is a `ChangeEmail`: see
+`ChangeSubject.vm` and `ChangeFooter.vm`.
+
+RegisterNewEmail.vm
+~~~~~~~~~~~~~~~~~~~
+
+The `RegisterNewEmail.vm` template will determine the contents of the email
+related to registering new email accounts.
+
+ReplacePatchSet.vm
+~~~~~~~~~~~~~~~~~~
+
+The `ReplacePatchSet.vm` template will determine the contents of the email
+related to a user submitting a new patchset for a change.  It is a
+`ChangeEmail`: see `ChangeSubject.vm` and `ChangeFooter.vm`.
+
+
+Mail Variables and Methods
+--------------------------
+
+Mail templates can access and display objects currently made available to them
+via the velocity context.  While the base objects are documented here, it is
+possible to call public methods on these objects from templates.  Those methods
+are not documented here since they could change with every release.  As these
+templates are meant to be modified only by a qualified sysadmin, it is accepted
+that writing templates for Gerrit emails is likely to require some basic
+knowledge of the class structure to be useful.  Browsing the source code might
+be necessary for anything more than a minor formatting change.
+
+Warning
+~~~~~~~
+
+Be aware that modifying templates can cause them to fail to parse and therefor
+not send out the actual email, or worse, calling methods on the available
+objects could have internal side effects which would adversely affect the
+health of your Gerrit server and/or data.
+
+All OutgoingEmails
+~~~~~~~~~~~~~~~~~~
+
+All outgoing emails have the following variables available to them:
+
+$email::
++
+A reference to the class constructing the current `OutgoingEmail`.  With this
+reference it is possible to call any public method on the OutgoingEmail class
+or the current child class inherited from it.
+
+$messageClass::
++
+A String containing the messageClass
+
+$StringUtils::
++
+A reference to the Apache `StringUtils` class.  This can be very useful for
+formatting strings.
+
+Change Emails
+~~~~~~~~~~~~~
+
+All change related emails have the following additional variables available to them:
+
+$change::
++
+A reference to the current `Change` object
+
+$changeId::
++
+Id of the current change (a `Change.Key`)
+
+$coverLetter::
++
+The text of the `ChangeMessage`
+
+$branch::
++
+A reference to the branch of this change (a `Branch.NameKey`)
+
+$fromName::
++
+The name of the from user
+
+$projectName::
++
+The name of this change's project
+
+$patchSet::
++
+A reference to the current `PatchSet`
+
+$patchSetInfo::
++
+A reference to the current `PatchSetInfo`
+
+
+See Also
+--------
+
+* link:http://velocity.apache.org/[velocity]
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/config-replication.txt b/Documentation/config-replication.txt
index abb6a97..1e20d2b 100644
--- a/Documentation/config-replication.txt
+++ b/Documentation/config-replication.txt
@@ -42,8 +42,8 @@
     url = mirror1.us.some.org:/pub/git/${name}.git
     url = mirror2.us.some.org:/pub/git/${name}.git
     url = mirror3.us.some.org:/pub/git/${name}.git
-    push = +refs/heads/*
-    push = +refs/tags/*
+    push = +refs/heads/*:refs/heads/*
+    push = +refs/tags/*:refs/tags/*
     threads = 3
     authGroup = Public Mirror Group
     authGroup = Second Public Mirror Group
@@ -142,6 +142,19 @@
 +
 By default, 15 seconds.
 
+[[remote.name.replicationRetry]]remote.<name>.replicationRetry::
++
+Number of minutes to wait before scheduling a remote push operation
+previously failed due to an offline remote server.
++
+If a remote push operation fails because a remote server was
+offline, all push operations to the same destination URL are
+blocked, and the remote push is continuously retried.
++
+This is a Gerrit specific extension to the Git remote block.
++
+By default, 1 minute.
+
 [[remote.name.threads]]remote.<name>.threads::
 +
 Number of worker threads to dedicate to pushing to the repositories
diff --git a/Documentation/index.txt b/Documentation/index.txt
index 1e13e8d..e419455 100644
--- a/Documentation/index.txt
+++ b/Documentation/index.txt
@@ -31,6 +31,7 @@
 * link:config-sso.html[Single Sign-On Systems]
 * link:config-apache2.html[Apache 2 Reverse Proxy]
 * link:config-hooks.html[Hooks]
+* link:config-mail.html[Mail Templates]
 
 Developer Documentation
 -----------------------
diff --git a/Documentation/json.txt b/Documentation/json.txt
index 1c9a808..99b158da 100644
--- a/Documentation/json.txt
+++ b/Documentation/json.txt
@@ -107,6 +107,19 @@
 
 by:: Reviewer of the patch set in <<account,account attribute>>.
 
+[[refupdate]]
+refupdate
+--------
+Information about a ref that was updated.
+
+oldRev:: The old value of the ref, prior to the update.
+
+newRev:: The new value the ref was updated to.
+
+project:: Project path in Gerrit
+
+refName:: Ref name within project.
+
 SEE ALSO
 --------
 
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
index 2c123f8..25194a9 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -30,6 +30,7 @@
 Apache MINA                 <<apache2,Apache License 2.0>>
 Apache Tomact Servlet API   <<apache2,Apache License 2.0>>
 Apache SSHD                 <<apache2,Apache License 2.0>>, see also <<sshd,NOTICE>>
+Apache Velocity             <<apache2,Apache License 2.0>>
 Apache Xerces               <<apache2,Apache License 2.0>>
 OpenID4Java                 <<apache2,Apache License 2.0>>
 Neko HTML                   <<apache2,Apache License 2.0>>
diff --git a/Documentation/pgm-daemon.txt b/Documentation/pgm-daemon.txt
index 228c60f8..0b2e72f 100644
--- a/Documentation/pgm-daemon.txt
+++ b/Documentation/pgm-daemon.txt
@@ -49,7 +49,7 @@
 	Run in slave mode, permitting only read operations
     by clients.  Commands which modify state such as
     link:cmd-receive-pack.html[recieve-pack] (creates new changes
-    or updates existing ones) or link:cmd-approve.html[approve]
+    or updates existing ones) or link:cmd-review.html[review]
     (sets approve marks) are disabled.
 +
 This option automatically implies '\--disable-httpd \--enable-sshd'.
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index c62e894..5b2f81e 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -87,7 +87,8 @@
 [[project]]
 project:'PROJECT'::
 +
-Changes occuring in 'PROJECT'.
+Changes occuring in 'PROJECT'.  If 'PROJECT' starts with `^` it
+matches project names by regular expression.
 
 [[branch]]
 branch:'BRANCH'::
@@ -98,6 +99,9 @@
 'branch:master' really means 'ref:refs/heads/master', and searching
 for 'branch:refs/heads/master' is the same as searching for
 'ref:refs/heads/refs/heads/master'.
++
+If 'BRANCH' starts with `^` it matches branch names by regular
+expression patterns.
 
 [[topic]]
 topic:'TOPIC'::
@@ -105,6 +109,9 @@
 Changes whose designated topic at upload was 'TOPIC'.  This is
 often combined with 'branch:' and 'project:' operators to select
 all related changes in a series.
++
+If 'TOPIC' starts with `^` it matches topic names by regular
+expression patterns.
 
 [[ref]]
 ref:'REF'::
@@ -112,6 +119,9 @@
 Changes where the destination branch is exactly the given 'REF'
 name.  Since 'REF' is absolute from the top of the repository it
 must start with 'refs/'.
++
+If 'REF' starts with `^` it matches reference names by regular
+expression patterns.
 
 [[tr]][[bug]]
 tr:'ID', bug:'ID'::
@@ -130,12 +140,29 @@
 a review.  See <<labels,labels>> below for more detail on the format
 of the argument.
 
+[[message]]
+message:'MESSAGE'::
++
+Changes that matches 'MESSAGE' arbitrary string in body commit messages.
+
 [[file]]
 file:\^'REGEX'::
 +
 Matches any change where REGEX matches a file that was affected
 by the change.  The regular expression pattern must start with
-'\^'.  For example, to match all XML files use `file:^.*\.xml$`.
+`\^`.  For example, to match all XML files use `file:^.*\.xml$`.
++
+The `\^` required at the beginning of the regular expression not only
+denotes a regular expression, but it also has the usual meaning of
+anchoring the match to the start of the string.  To match all Java
+files, use `file:^.*\.java`.
++
+The entire regular expression pattern, including the `\^` character,
+should be double quoted when using more complex construction (like
+ones using a bracket expression). For example, to match all XML
+files named like 'name1.xml', 'name2.xml', and 'name3.xml' use
+`\file:"\^name[1-3].xml"`.
++
 Currently this operator is only available on a watched project
 and may not be used in the search bar.
 
diff --git a/gerrit-common/pom.xml b/gerrit-common/pom.xml
index adac3a8..af03191 100644
--- a/gerrit-common/pom.xml
+++ b/gerrit-common/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.1.4-SNAPSHOT</version>
+    <version>2.1-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-common</artifactId>
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ListBranchesResult.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ListBranchesResult.java
index 56c31ad..7341c47 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ListBranchesResult.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ListBranchesResult.java
@@ -19,35 +19,33 @@
 import java.util.List;
 
 /**
- * It holds list of branches and boolean to indicate
- * if it is allowed to add new branches.
+ * It holds list of branches and boolean to indicate if it is allowed to add new
+ * branches.
  */
 public final class ListBranchesResult {
+  protected boolean noRepository;
   protected boolean canAdd;
-
   protected List<Branch> branches;
 
   protected ListBranchesResult() {
   }
 
-  public ListBranchesResult(final List<Branch> branches, boolean canAdd) {
+  public ListBranchesResult(List<Branch> branches, boolean canAdd,
+      boolean noRepository) {
     this.branches = branches;
     this.canAdd = canAdd;
+    this.noRepository = noRepository;
+  }
+
+  public boolean getNoRepository() {
+    return noRepository;
   }
 
   public boolean getCanAdd() {
     return canAdd;
   }
 
-  public void setCanAdd(boolean canAdd) {
-    this.canAdd = canAdd;
-  }
-
   public List<Branch> getBranches() {
     return branches;
   }
-
-  public void setBranches(List<Branch> branches) {
-    this.branches = branches;
-  }
 }
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 048d440..e24405f 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
@@ -34,10 +34,16 @@
     NONE, DIFF, IMG
   }
 
+  public static enum FileMode {
+    FILE, SYMLINK, GITLINK
+  }
+
   protected Change.Key changeId;
   protected ChangeType changeType;
   protected String oldName;
   protected String newName;
+  protected FileMode oldMode;
+  protected FileMode newMode;
   protected List<String> header;
   protected AccountDiffPreference diffPrefs;
   protected SparseFileContent a;
@@ -51,7 +57,8 @@
   protected boolean intralineDifference;
 
   public PatchScript(final Change.Key ck, final ChangeType ct, final String on,
-      final String nn, final List<String> h, final AccountDiffPreference dp,
+      final String nn, final FileMode om, final FileMode nm,
+      final List<String> h, final AccountDiffPreference dp,
       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,
@@ -60,6 +67,8 @@
     changeType = ct;
     oldName = on;
     newName = nn;
+    oldMode = om;
+    newMode = nm;
     header = h;
     diffPrefs = dp;
     a = ca;
@@ -88,6 +97,14 @@
     return displayMethodB;
   }
 
+  public FileMode getFileModeA() {
+    return oldMode;
+  }
+
+  public FileMode getFileModeB() {
+    return newMode;
+  }
+
   public List<String> getPatchHeader() {
     return header;
   }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewerResult.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewerResult.java
index 19772fc..678ec79 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewerResult.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewerResult.java
@@ -50,6 +50,9 @@
       /** Name supplied does not match to a registered account. */
       ACCOUNT_NOT_FOUND,
 
+      /** The account is inactive. */
+      ACCOUNT_INACTIVE,
+
       /** The account is not permitted to see the change. */
       CHANGE_NOT_VISIBLE,
 
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java
index 164df43..9dae169 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java
@@ -28,7 +28,7 @@
   void suggestProjectNameKey(String query, int limit,
       AsyncCallback<List<Project.NameKey>> callback);
 
-  void suggestAccount(String query, int limit,
+  void suggestAccount(String query, Boolean enabled, int limit,
       AsyncCallback<List<AccountInfo>> callback);
 
   void suggestAccountGroup(String query, int limit,
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/errors/InactiveAccountException.java b/gerrit-common/src/main/java/com/google/gerrit/common/errors/InactiveAccountException.java
new file mode 100644
index 0000000..6ae5eb6
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/errors/InactiveAccountException.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.common.errors;
+
+/** Error indicating the account is currently inactive. */
+public class InactiveAccountException extends Exception {
+  private static final long serialVersionUID = 1L;
+
+  public static final String MESSAGE = "Account Inactive: ";
+
+  public InactiveAccountException(String who) {
+    super(MESSAGE + who);
+  }
+}
diff --git a/gerrit-gwtdebug/pom.xml b/gerrit-gwtdebug/pom.xml
index c3a07a4..88246ac 100644
--- a/gerrit-gwtdebug/pom.xml
+++ b/gerrit-gwtdebug/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.1.4-SNAPSHOT</version>
+    <version>2.1-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-gwtdebug</artifactId>
diff --git a/gerrit-gwtui/pom.xml b/gerrit-gwtui/pom.xml
index 29c5b01..9a27d12 100644
--- a/gerrit-gwtui/pom.xml
+++ b/gerrit-gwtui/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.1.4-SNAPSHOT</version>
+    <version>2.1-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-gwtui</artifactId>
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 281c72b..81a9dd5 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
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.common.data.AccountInfo;
 import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.AccountGeneralPreferences;
 import com.google.gwt.i18n.client.DateTimeFormat;
 
 import java.util.Date;
@@ -24,13 +25,29 @@
 public class FormatUtil {
   private static final long ONE_YEAR = 182L * 24 * 60 * 60 * 1000;
 
-  private static final DateTimeFormat sTime =
-      DateTimeFormat.getShortTimeFormat();
-  private static final DateTimeFormat sDate = DateTimeFormat.getFormat("MMM d");
-  private static final DateTimeFormat mDate =
-      DateTimeFormat.getMediumDateFormat();
-  private static final DateTimeFormat dtfmt =
-      DateTimeFormat.getFormat(mDate.getPattern() + " " + sTime.getPattern());
+  private static DateTimeFormat sTime = DateTimeFormat.getShortTimeFormat();
+  private static DateTimeFormat sDate = DateTimeFormat.getFormat("MMM d");
+  private static DateTimeFormat mDate = DateTimeFormat.getMediumDateFormat();
+  private static DateTimeFormat dtfmt;
+
+  public static void setPreferences(AccountGeneralPreferences pref) {
+    if (pref == null) {
+      if (Gerrit.isSignedIn()) {
+        pref = Gerrit.getUserAccount().getGeneralPreferences();
+      } else {
+        pref = new AccountGeneralPreferences();
+        pref.resetToDefaults();
+      }
+    }
+
+    String fmt_sTime = pref.getTimeFormat().getFormat();
+    String fmt_mDate = pref.getDateFormat().getLongFormat();
+
+    sTime = DateTimeFormat.getFormat(fmt_sTime);
+    sDate = DateTimeFormat.getFormat(pref.getDateFormat().getShortFormat());
+    mDate = DateTimeFormat.getFormat(fmt_mDate);
+    dtfmt = DateTimeFormat.getFormat(fmt_mDate + " " + fmt_sTime);
+  }
 
   /** Format a date using a really short format. */
   public static String shortFormat(Date dt) {
@@ -38,6 +55,7 @@
       return "";
     }
 
+    ensureInited();
     final Date now = new Date();
     dt = new Date(dt.getTime());
     if (mDate.format(now).equals(mDate.format(dt))) {
@@ -62,9 +80,16 @@
     if (dt == null) {
       return "";
     }
+    ensureInited();
     return dtfmt.format(new Date(dt.getTime()));
   }
 
+  private static void ensureInited() {
+    if (dtfmt == null) {
+      setPreferences(null);
+    }
+  }
+
   /** Format an account as a name and email address. */
   public static String nameEmail(final Account acct) {
     return nameEmail(new AccountInfo(acct));
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 7bee0af..d5ccdf9 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
@@ -481,6 +481,7 @@
       switch (cfg.getAuthType()) {
         case HTTP:
         case HTTP_LDAP:
+        case CLIENT_SSL_CERT_LDAP:
           break;
 
         case OPENID:
@@ -526,6 +527,7 @@
       if (siteFooter != null) {
         siteFooter.setVisible(p.isShowSiteHeader());
       }
+      FormatUtil.setPreferences(myAccount.getGeneralPreferences());
     }
   }
 
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 3f263d2..b32a32d 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
@@ -46,6 +46,8 @@
   String nameAlreadyUsedBody();
   String noSuchAccountTitle();
 
+  String inactiveAccountBody();
+
   String menuAll();
   String menuAllOpen();
   String menuAllMerged();
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 83a06e4..3363015 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
@@ -29,6 +29,8 @@
 nameAlreadyUsedBody = The name is already in use.
 noSuchAccountTitle = Code Review - Unknown User
 
+inactiveAccountBody = This user is currently inactive.
+
 menuAll = All
 menuAllOpen = Open
 menuAllMerged = Merged
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 dd68479..ca9f248 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
@@ -67,6 +67,7 @@
   String commentPanelHeader();
   String commentPanelLast();
   String commentPanelMessage();
+  String commentPanelMenuBar();
   String commentPanelSummary();
   String commentPanelSummaryCell();
   String complexHeader();
@@ -105,6 +106,7 @@
   String fileLineCONTEXT();
   String fileLineDELETE();
   String fileLineINSERT();
+  String fileLineMode();
   String fileLineNone();
   String filePathCell();
   String gerritTopMenu();
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 2d3d2e8..db5094d 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
@@ -15,14 +15,11 @@
 package com.google.gerrit.client;
 
 import com.google.gerrit.client.changes.QueryScreen;
+import com.google.gerrit.client.ui.HintTextBox;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.reviewdb.Change;
-import com.google.gwt.event.dom.client.BlurEvent;
-import com.google.gwt.event.dom.client.BlurHandler;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.FocusEvent;
-import com.google.gwt.event.dom.client.FocusHandler;
 import com.google.gwt.event.dom.client.KeyCodes;
 import com.google.gwt.event.dom.client.KeyPressEvent;
 import com.google.gwt.event.dom.client.KeyPressHandler;
@@ -35,7 +32,7 @@
 import com.google.gwtexpui.globalkey.client.NpTextBox;
 
 class SearchPanel extends Composite {
-  private final NpTextBox searchBox;
+  private final HintTextBox searchBox;
   private HandlerRegistration regFocus;
 
   SearchPanel() {
@@ -43,39 +40,14 @@
     initWidget(body);
     setStyleName(Gerrit.RESOURCES.css().searchPanel());
 
-    searchBox = new NpTextBox();
+    searchBox = new HintTextBox();
     searchBox.setVisibleLength(70);
-    searchBox.setText(Gerrit.C.searchHint());
-    searchBox.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
-    searchBox.addFocusHandler(new FocusHandler() {
-      @Override
-      public void onFocus(FocusEvent event) {
-        if (Gerrit.C.searchHint().equals(searchBox.getText())) {
-          searchBox.setText("");
-          searchBox.removeStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
-        }
-      }
-    });
-    searchBox.addBlurHandler(new BlurHandler() {
-      @Override
-      public void onBlur(BlurEvent event) {
-        if ("".equals(searchBox.getText())) {
-          searchBox.setText(Gerrit.C.searchHint());
-          searchBox.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
-        }
-      }
-    });
+    searchBox.setHintText(Gerrit.C.searchHint());
     searchBox.addKeyPressHandler(new KeyPressHandler() {
       @Override
       public void onKeyPress(final KeyPressEvent event) {
-        switch (event.getCharCode()) {
-          case KeyCodes.KEY_ENTER:
-            doSearch();
-            break;
-          case KeyCodes.KEY_ESCAPE:
-            searchBox.setText("");
-            searchBox.setFocus(false);
-            break;
+        if (event.getCharCode() == KeyCodes.KEY_ENTER) {
+          doSearch();
         }
       }
     });
@@ -93,13 +65,7 @@
   }
 
   void setText(final String query) {
-    if (query == null || query.equals("")) {
-      searchBox.setText(Gerrit.C.searchHint());
-      searchBox.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
-    } else {
-      searchBox.setText(query);
-      searchBox.removeStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
-    }
+    searchBox.setText(query);
   }
 
   @Override
@@ -129,7 +95,7 @@
 
   private void doSearch() {
     final String query = searchBox.getText().trim();
-    if (query.length() == 0 || Gerrit.C.searchHint().equals(query)) {
+    if ("".equals(query)) {
       return;
     }
 
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 3756bed..276fc27 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
@@ -25,10 +25,12 @@
   String accountId();
 
   String maximumPageSizeFieldLabel();
+  String dateFormatLabel();
   String contextWholeFile();
   String showSiteHeader();
   String useFlashClipboard();
   String copySelfOnEmails();
+  String displayPatchSetsInReverseOrder();
   String buttonSaveChanges();
 
   String tabAccountSummary();
@@ -81,6 +83,10 @@
   String buttonWatchProject();
   String defaultProjectName();
   String defaultFilter();
+  String buttonBrowseProjects();
+  String projects();
+  String projectsClose();
+  String projectListOpen();
   String watchedProjectName();
   String watchedProjectFilter();
   String watchedProjectColumnEmailNotifications();
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 29ee14a..a5a652f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
@@ -7,8 +7,10 @@
 showSiteHeader = Show Site Header
 useFlashClipboard = Use Flash Clipboard Widget
 copySelfOnEmails = CC Me On Comments I Write
+displayPatchSetsInReverseOrder = Display Patch Sets In Reverse Order
 defaultContextFieldLabel = Default Context:
 maximumPageSizeFieldLabel = Maximum Page Size:
+dateFormatLabel = Date/Time Format:
 contextWholeFile = Whole File
 buttonSaveChanges = Save Changes
 
@@ -62,6 +64,10 @@
 buttonWatchProject = Watch
 defaultProjectName = Project Name
 defaultFilter = branch:name, or other search expression
+projects = All Watchable Projects
+projectsClose = Close
+buttonBrowseProjects = Browse
+projectListOpen = Watch Selected project
 watchedProjectName = Project Name
 watchedProjectFilter = Only If
 watchedProjectColumnEmailNotifications = Email Notifications
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelFull.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelFull.java
index 3742bed..b7a00ad 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelFull.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelFull.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.client.account;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.ui.TextSaveButtonListener;
+import com.google.gerrit.client.ui.OnEditEnabler;
 import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.reviewdb.ContactInformation;
 import com.google.gwt.user.client.ui.Grid;
@@ -80,11 +80,11 @@
     infoSecure.getCellFormatter().addStyleName(0, 1, Gerrit.RESOURCES.css().topmost());
     infoSecure.getCellFormatter().addStyleName(3, 0, Gerrit.RESOURCES.css().bottomheader());
 
-    final TextSaveButtonListener sbl = new TextSaveButtonListener(save);
-    addressTxt.addKeyPressHandler(sbl);
-    countryTxt.addKeyPressHandler(sbl);
-    phoneTxt.addKeyPressHandler(sbl);
-    faxTxt.addKeyPressHandler(sbl);
+    final OnEditEnabler sbl = new OnEditEnabler(save);
+    sbl.listenTo(addressTxt);
+    sbl.listenTo(countryTxt);
+    sbl.listenTo(phoneTxt);
+    sbl.listenTo(faxTxt);
   }
 
   @Override
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 21c9163..8fe4d5f 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
@@ -16,7 +16,7 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.TextSaveButtonListener;
+import com.google.gerrit.client.ui.OnEditEnabler;
 import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.reviewdb.AccountExternalId;
 import com.google.gerrit.reviewdb.ContactInformation;
@@ -120,9 +120,8 @@
         doSave();
       }
     });
+    new OnEditEnabler(save, nameTxt);
 
-    final TextSaveButtonListener sbl = new TextSaveButtonListener(save);
-    nameTxt.addKeyPressHandler(sbl);
     emailPick.addChangeHandler(new ChangeHandler() {
       @Override
       public void onChange(final ChangeEvent event) {
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 b9fff04..437d438 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
@@ -26,7 +26,7 @@
   @Override
   protected void onInitUI() {
     super.onInitUI();
-    groups = new GroupTable(false /* do not hyperlink to admin */);
+    groups = new GroupTable(true /* hyperlink to admin */);
     add(groups);
   }
 
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 bb81f3f..3f32195 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
@@ -26,18 +26,25 @@
 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;
 import com.google.gwt.i18n.client.LocaleInfo;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.CheckBox;
+import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Grid;
 import com.google.gwt.user.client.ui.ListBox;
 import com.google.gwtjsonrpc.client.VoidResult;
 
+import java.util.Date;
+
 public class MyPreferencesScreen extends SettingsScreen {
   private CheckBox showSiteHeader;
   private CheckBox useFlashClipboard;
   private CheckBox copySelfOnEmails;
+  private CheckBox displayPatchSetsInReverseOrder;
   private ListBox maximumPageSize;
+  private ListBox dateFormat;
+  private ListBox timeFormat;
   private Button save;
 
   @Override
@@ -66,21 +73,51 @@
     copySelfOnEmails = new CheckBox(Util.C.copySelfOnEmails());
     copySelfOnEmails.addClickHandler(onClickSave);
 
+    displayPatchSetsInReverseOrder = new CheckBox(Util.C.displayPatchSetsInReverseOrder());
+    displayPatchSetsInReverseOrder.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();
+    for (AccountGeneralPreferences.DateFormat fmt : AccountGeneralPreferences.DateFormat
+        .values()) {
+      StringBuilder r = new StringBuilder();
+      r.append(DateTimeFormat.getFormat(fmt.getShortFormat()).format(now));
+      r.append(" ; ");
+      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
+        .values()) {
+      StringBuilder r = new StringBuilder();
+      r.append(DateTimeFormat.getFormat(fmt.getFormat()).format(now));
+      timeFormat.addItem(r.toString(), fmt.name());
+    }
+    timeFormat.addChangeHandler(onChangeSave);
+
+    FlowPanel dateTimePanel = new FlowPanel();
+
     final int labelIdx, fieldIdx;
     if (LocaleInfo.getCurrentLocale().isRTL()) {
       labelIdx = 1;
       fieldIdx = 0;
+      dateTimePanel.add(timeFormat);
+      dateTimePanel.add(dateFormat);
     } else {
       labelIdx = 0;
       fieldIdx = 1;
+      dateTimePanel.add(dateFormat);
+      dateTimePanel.add(timeFormat);
     }
-    final Grid formGrid = new Grid(4, 2);
+    final Grid formGrid = new Grid(6, 2);
 
     int row = 0;
     formGrid.setText(row, labelIdx, "");
@@ -95,10 +132,18 @@
     formGrid.setWidget(row, fieldIdx, copySelfOnEmails);
     row++;
 
+    formGrid.setText(row, labelIdx, "");
+    formGrid.setWidget(row, fieldIdx, displayPatchSetsInReverseOrder);
+    row++;
+
     formGrid.setText(row, labelIdx, Util.C.maximumPageSizeFieldLabel());
     formGrid.setWidget(row, fieldIdx, maximumPageSize);
     row++;
 
+    formGrid.setText(row, labelIdx, Util.C.dateFormatLabel());
+    formGrid.setWidget(row, fieldIdx, dateTimePanel);
+    row++;
+
     add(formGrid);
 
     save = new Button(Util.C.buttonSaveChanges());
@@ -126,21 +171,40 @@
     showSiteHeader.setEnabled(on);
     useFlashClipboard.setEnabled(on);
     copySelfOnEmails.setEnabled(on);
+    displayPatchSetsInReverseOrder.setEnabled(on);
     maximumPageSize.setEnabled(on);
+    dateFormat.setEnabled(on);
+    timeFormat.setEnabled(on);
   }
 
   private void display(final AccountGeneralPreferences p) {
     showSiteHeader.setValue(p.isShowSiteHeader());
     useFlashClipboard.setValue(p.isUseFlashClipboard());
     copySelfOnEmails.setValue(p.isCopySelfOnEmails());
+    displayPatchSetsInReverseOrder.setValue(p.isDisplayPatchSetsInReverseOrder());
     setListBox(maximumPageSize, DEFAULT_PAGESIZE, p.getMaximumPageSize());
+    setListBox(dateFormat, AccountGeneralPreferences.DateFormat.STD, //
+        p.getDateFormat());
+    setListBox(timeFormat, AccountGeneralPreferences.TimeFormat.HHMM_12, //
+        p.getTimeFormat());
   }
 
   private void setListBox(final ListBox f, final short defaultValue,
       final short currentValue) {
+    setListBox(f, String.valueOf(defaultValue), String.valueOf(currentValue));
+  }
+
+  private <T extends Enum<?>> void setListBox(final ListBox f,
+      final T defaultValue, final T currentValue) {
+    setListBox(f, defaultValue.name(), //
+        currentValue != null ? currentValue.name() : "");
+  }
+
+  private void setListBox(final ListBox f, final String defaultValue,
+      final String currentValue) {
     final int n = f.getItemCount();
     for (int i = 0; i < n; i++) {
-      if (Short.parseShort(f.getValue(i)) == currentValue) {
+      if (f.getValue(i).equals(currentValue)) {
         f.setSelectedIndex(i);
         return;
       }
@@ -158,12 +222,33 @@
     return defaultValue;
   }
 
+  private <T extends Enum<?>> T getListBox(final ListBox f,
+      final T defaultValue, T[] all) {
+    final int idx = f.getSelectedIndex();
+    if (0 <= idx) {
+      String v = f.getValue(idx);
+      for (T t : all) {
+        if (t.name().equals(v)) {
+          return t;
+        }
+      }
+    }
+    return defaultValue;
+  }
+
   private void doSave() {
     final AccountGeneralPreferences p = new AccountGeneralPreferences();
     p.setShowSiteHeader(showSiteHeader.getValue());
     p.setUseFlashClipboard(useFlashClipboard.getValue());
     p.setCopySelfOnEmails(copySelfOnEmails.getValue());
+    p.setDisplayPatchSetsInReverseOrder(displayPatchSetsInReverseOrder.getValue());
     p.setMaximumPageSize(getListBox(maximumPageSize, DEFAULT_PAGESIZE));
+    p.setDateFormat(getListBox(dateFormat,
+        AccountGeneralPreferences.DateFormat.STD,
+        AccountGeneralPreferences.DateFormat.values()));
+    p.setTimeFormat(getListBox(timeFormat,
+        AccountGeneralPreferences.TimeFormat.HHMM_12,
+        AccountGeneralPreferences.TimeFormat.values()));
 
     enable(false);
     save.setEnabled(false);
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 28099bf..224ff93 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
@@ -17,179 +17,303 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
-import com.google.gerrit.client.ui.FancyFlexTable;
-import com.google.gerrit.client.ui.ProjectLink;
+import com.google.gerrit.client.ui.HintTextBox;
 import com.google.gerrit.client.ui.ProjectNameSuggestOracle;
+import com.google.gerrit.client.ui.RPCSuggestOracle;
+import com.google.gerrit.client.ui.ProjectsTable;
+import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.AccountProjectWatchInfo;
-import com.google.gerrit.reviewdb.AccountProjectWatch;
-import com.google.gerrit.reviewdb.Change.Status;
-import com.google.gwt.event.dom.client.BlurEvent;
-import com.google.gwt.event.dom.client.BlurHandler;
+import com.google.gerrit.reviewdb.Project;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.FocusEvent;
-import com.google.gwt.event.dom.client.FocusHandler;
 import com.google.gwt.event.dom.client.KeyCodes;
 import com.google.gwt.event.dom.client.KeyPressEvent;
 import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.event.logical.shared.ResizeEvent;
+import com.google.gwt.event.logical.shared.ResizeHandler;
 import com.google.gwt.event.logical.shared.SelectionEvent;
 import com.google.gwt.event.logical.shared.SelectionHandler;
-import com.google.gwt.user.client.DOM;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.Window;
 import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.CheckBox;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Grid;
-import com.google.gwt.user.client.ui.Label;
-import com.google.gwt.user.client.ui.SuggestBox;
-import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
 import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.ScrollPanel;
+import com.google.gwt.user.client.ui.SuggestBox;
 import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
-import com.google.gwtexpui.globalkey.client.NpTextBox;
-import com.google.gwtjsonrpc.client.VoidResult;
+import com.google.gwtexpui.globalkey.client.GlobalKey;
+import com.google.gwtexpui.globalkey.client.HidePopupPanelCommand;
+import com.google.gwtexpui.user.client.PluginSafeDialogBox;
 
-import java.util.HashSet;
 import java.util.List;
 
-public class MyWatchedProjectsScreen extends SettingsScreen {
-  private WatchTable watches;
-
+public class MyWatchedProjectsScreen extends SettingsScreen implements
+    ResizeHandler {
   private Button addNew;
-  private NpTextBox nameBox;
+  private HintTextBox nameBox;
   private SuggestBox nameTxt;
-  private NpTextBox filterTxt;
+  private HintTextBox filterTxt;
+  private MyWatchesTable watchesTab;
+  private Button browse;
+  private PluginSafeDialogBox popup;
+  private Button close;
+  private ProjectsTable projectsTab;
   private Button delSel;
+
+  private PopupPanel.PositionCallback popupPosition;
+  private HandlerRegistration regWindowResize;
+
+  private int preferredPopupWidth = -1;
+
   private boolean submitOnSelection;
+  private boolean firstPopupLoad = true;
+  private boolean popingUp;
+
+  private ScrollPanel sp;
 
   @Override
   protected void onInitUI() {
     super.onInitUI();
+    createWidgets();
 
-    {
-      nameBox = new NpTextBox();
-      nameTxt = new SuggestBox(new ProjectNameSuggestOracle(), nameBox);
-      nameBox.setVisibleLength(50);
-      nameBox.setText(Util.C.defaultProjectName());
-      nameBox.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
-      nameBox.addFocusHandler(new FocusHandler() {
-        @Override
-        public void onFocus(FocusEvent event) {
-          if (Util.C.defaultProjectName().equals(nameBox.getText())) {
-            nameBox.setText("");
-            nameBox.removeStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
+
+    /* top table */
+
+    final Grid grid = new Grid(2, 2);
+    grid.setStyleName(Gerrit.RESOURCES.css().infoBlock());
+    grid.setText(0, 0, Util.C.watchedProjectName());
+    grid.setWidget(0, 1, nameTxt);
+
+    grid.setText(1, 0, Util.C.watchedProjectFilter());
+    grid.setWidget(1, 1, filterTxt);
+
+    final CellFormatter fmt = grid.getCellFormatter();
+    fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().topmost());
+    fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().topmost());
+    fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().header());
+    fmt.addStyleName(1, 0, Gerrit.RESOURCES.css().header());
+    fmt.addStyleName(1, 0, Gerrit.RESOURCES.css().bottomheader());
+
+    final FlowPanel fp = new FlowPanel();
+    fp.setStyleName(Gerrit.RESOURCES.css().addWatchPanel());
+    fp.add(grid);
+    fp.add(addNew);
+    fp.add(browse);
+    add(fp);
+
+
+    /* bottom table */
+
+    add(watchesTab);
+    add(delSel);
+
+
+    /* popup */
+
+    final FlowPanel pfp = new FlowPanel();
+    sp = new ScrollPanel(projectsTab);
+    pfp.add(sp);
+    pfp.add(close);
+    popup.setWidget(pfp);
+
+    popupPosition = new PopupPanel.PositionCallback() {
+      public void setPosition(int offsetWidth, int offsetHeight) {
+        if (preferredPopupWidth == -1) {
+          preferredPopupWidth = offsetWidth;
+        }
+
+        int top = grid.getAbsoluteTop() - 50; // under page header
+
+        // Try to place it to the right of everything else, but not
+        // right justified
+        int left = 5 + Math.max(
+                         grid.getAbsoluteLeft() + grid.getOffsetWidth(),
+                   watchesTab.getAbsoluteLeft() + watchesTab.getOffsetWidth() );
+
+        if (top + offsetHeight > Window.getClientHeight()) {
+          top = Window.getClientHeight() - offsetHeight;
+        }
+        if (left + offsetWidth > Window.getClientWidth()) {
+          left = Window.getClientWidth() - offsetWidth;
+        }
+
+        if (top < 0) {
+          sp.setHeight((sp.getOffsetHeight() + top) + "px");
+          top = 0;
+        }
+        if (left < 0) {
+          sp.setWidth((sp.getOffsetWidth() + left) + "px");
+          left = 0;
+        }
+
+        popup.setPopupPosition(left, top);
+      }
+    };
+  }
+
+  @Override
+  public void onResize(final ResizeEvent event) {
+    sp.setSize("100%","100%");
+
+    // For some reason keeping track of preferredWidth keeps the width better,
+    // but using 100% for height works better.
+    popup.setHeight("100%");
+    popupPosition.setPosition(preferredPopupWidth, popup.getOffsetHeight());
+  }
+
+  protected void createWidgets() {
+    nameBox = new HintTextBox();
+    nameTxt = new SuggestBox(new ProjectNameSuggestOracle(), nameBox);
+    nameBox.setVisibleLength(50);
+    nameBox.setHintText(Util.C.defaultProjectName());
+    nameBox.addKeyPressHandler(new KeyPressHandler() {
+      @Override
+      public void onKeyPress(KeyPressEvent event) {
+        submitOnSelection = false;
+
+        if (event.getCharCode() == KeyCodes.KEY_ENTER) {
+          if (nameTxt.isSuggestionListShowing()) {
+            submitOnSelection = true;
+          } else {
+            doAddNew();
           }
         }
-      });
-      nameBox.addBlurHandler(new BlurHandler() {
-        @Override
-        public void onBlur(BlurEvent event) {
-          if ("".equals(nameBox.getText())) {
-            nameBox.setText(Util.C.defaultProjectName());
-            nameBox.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
-          }
-        }
-      });
-      nameBox.addKeyPressHandler(new KeyPressHandler() {
-        @Override
-        public void onKeyPress(KeyPressEvent event) {
+      }
+    });
+    nameTxt.addSelectionHandler(new SelectionHandler<Suggestion>() {
+      @Override
+      public void onSelection(SelectionEvent<Suggestion> event) {
+        if (submitOnSelection) {
           submitOnSelection = false;
-
-          if (event.getCharCode() == KeyCodes.KEY_ENTER) {
-            if (nameTxt.isSuggestionListShowing()) {
-              submitOnSelection = true;
-            } else {
-              doAddNew();
-            }
-          }
-        }
-      });
-      nameTxt.addSelectionHandler(new SelectionHandler<Suggestion>() {
-        @Override
-        public void onSelection(SelectionEvent<Suggestion> event) {
-          if (submitOnSelection) {
-            submitOnSelection = false;
-            doAddNew();
-          }
-        }
-      });
-
-      filterTxt = new NpTextBox();
-      filterTxt.setVisibleLength(50);
-      filterTxt.setText(Util.C.defaultFilter());
-      filterTxt.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
-      filterTxt.addFocusHandler(new FocusHandler() {
-        @Override
-        public void onFocus(FocusEvent event) {
-          if (Util.C.defaultFilter().equals(filterTxt.getText())) {
-            filterTxt.setText("");
-            filterTxt.removeStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
-          }
-        }
-      });
-      filterTxt.addBlurHandler(new BlurHandler() {
-        @Override
-        public void onBlur(BlurEvent event) {
-          if ("".equals(filterTxt.getText())) {
-            filterTxt.setText(Util.C.defaultFilter());
-            filterTxt.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
-          }
-        }
-      });
-      filterTxt.addKeyPressHandler(new KeyPressHandler() {
-        @Override
-        public void onKeyPress(KeyPressEvent event) {
-          if (event.getCharCode() == KeyCodes.KEY_ENTER) {
-            doAddNew();
-          }
-        }
-      });
-
-      addNew = new Button(Util.C.buttonWatchProject());
-      addNew.addClickHandler(new ClickHandler() {
-        @Override
-        public void onClick(final ClickEvent event) {
           doAddNew();
         }
-      });
+      }
+    });
 
-      final Grid grid = new Grid(2, 2);
-      grid.setStyleName(Gerrit.RESOURCES.css().infoBlock());
-      grid.setText(0, 0, Util.C.watchedProjectName());
-      grid.setWidget(0, 1, nameTxt);
+    filterTxt = new HintTextBox();
+    filterTxt.setVisibleLength(50);
+    filterTxt.setHintText(Util.C.defaultFilter());
+    filterTxt.addKeyPressHandler(new KeyPressHandler() {
+      @Override
+      public void onKeyPress(KeyPressEvent event) {
+        if (event.getCharCode() == KeyCodes.KEY_ENTER) {
+          doAddNew();
+        }
+      }
+    });
 
-      grid.setText(1, 0, Util.C.watchedProjectFilter());
-      grid.setWidget(1, 1, filterTxt);
+    addNew = new Button(Util.C.buttonWatchProject());
+    addNew.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(final ClickEvent event) {
+        doAddNew();
+      }
+    });
 
-      final CellFormatter fmt = grid.getCellFormatter();
-      fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().topmost());
-      fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().topmost());
-      fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().header());
-      fmt.addStyleName(1, 0, Gerrit.RESOURCES.css().header());
-      fmt.addStyleName(1, 0, Gerrit.RESOURCES.css().bottomheader());
+    projectsTab = new ProjectsTable() {
+      {
+        keysNavigation.add(new OpenKeyCommand(0, 'o', Util.C.projectListOpen()));
+        keysNavigation.add(new OpenKeyCommand(0, KeyCodes.KEY_ENTER,
+                                                      Util.C.projectListOpen()));
+      }
 
-      final FlowPanel fp = new FlowPanel();
-      fp.setStyleName(Gerrit.RESOURCES.css().addWatchPanel());
-      fp.add(grid);
-      fp.add(addNew);
-      add(fp);
-    }
+      @Override
+      protected void movePointerTo(final int row, final boolean scroll) {
+        super.movePointerTo(row, scroll);
 
-    watches = new WatchTable();
-    add(watches);
+        // prevent user input from being overwritten by simply poping up
+        if (! popingUp || "".equals(nameBox.getText()) ) {
+          nameBox.setText(getRowItem(row).getName());
+        }
+      }
+
+      @Override
+      protected void onOpenRow(final int row) {
+        super.onOpenRow(row);
+        nameBox.setText(getRowItem(row).getName());
+        doAddNew();
+      }
+    };
+    projectsTab.setSavePointerId(PageLinks.SETTINGS_PROJECTS);
+
+    close = new Button(Util.C.projectsClose());
+    close.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(final ClickEvent event) {
+        closePopup();
+      }
+    });
+
+    popup = new PluginSafeDialogBox();
+    popup.setModal(false);
+    popup.setText(Util.C.projects());
+
+    browse = new Button(Util.C.buttonBrowseProjects());
+    browse.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(final ClickEvent event) {
+        displayPopup();
+      }
+    });
+
+    watchesTab = new MyWatchesTable();
 
     delSel = new Button(Util.C.buttonDeleteSshKey());
     delSel.addClickHandler(new ClickHandler() {
       @Override
       public void onClick(final ClickEvent event) {
-        watches.deleteChecked();
+        watchesTab.deleteChecked();
       }
     });
-    add(delSel);
   }
 
-  void doAddNew() {
+  @Override
+  protected void onLoad() {
+    super.onLoad();
+    populateWatches();
+  }
+
+  @Override
+  protected void onUnload() {
+    super.onUnload();
+    closePopup();
+  }
+
+  protected void displayPopup() {
+    popingUp = true;
+    if (firstPopupLoad) { // For sizing/positioning, delay display until loaded
+      populateProjects();
+    } else {
+      popup.setPopupPositionAndShow(popupPosition);
+
+      GlobalKey.dialog(popup);
+      GlobalKey.addApplication(popup, new HidePopupPanelCommand(0,
+          KeyCodes.KEY_ESCAPE, popup));
+      projectsTab.setRegisterKeys(true);
+
+      projectsTab.finishDisplay();
+
+      if (regWindowResize == null) {
+        regWindowResize = Window.addResizeHandler(this);
+      }
+
+      popingUp = false;
+    }
+  }
+
+  protected void closePopup() {
+    popup.hide();
+    if (regWindowResize != null) {
+      regWindowResize.removeHandler();
+      regWindowResize = null;
+    }
+  }
+
+  protected void doAddNew() {
     final String projectName = nameTxt.getText();
-    if (projectName == null || projectName.length() == 0
-        || Util.C.defaultProjectName().equals(projectName)) {
+    if ("".equals(projectName)) {
       return;
     }
 
@@ -211,7 +335,7 @@
             filterTxt.setEnabled(true);
 
             nameTxt.setText("");
-            watches.insertWatch(result);
+            watchesTab.insertWatch(result);
           }
 
           @Override
@@ -225,190 +349,27 @@
         });
   }
 
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    Util.ACCOUNT_SVC
-        .myProjectWatch(new ScreenLoadCallback<List<AccountProjectWatchInfo>>(
-            this) {
-          public void preDisplay(final List<AccountProjectWatchInfo> result) {
-            watches.display(result);
-          }
-        });
+  protected void populateWatches() {
+    Util.ACCOUNT_SVC.myProjectWatch(
+        new ScreenLoadCallback<List<AccountProjectWatchInfo>>(this) {
+      @Override
+      public void preDisplay(final List<AccountProjectWatchInfo> result) {
+        watchesTab.display(result);
+      }
+    });
   }
 
-  private class WatchTable extends FancyFlexTable<AccountProjectWatchInfo> {
-    WatchTable() {
-      table.setWidth("");
-      table.insertRow(1);
-      table.setText(0, 2, Util.C.watchedProjectName());
-      table.setText(0, 3, Util.C.watchedProjectColumnEmailNotifications());
-
-      final FlexCellFormatter fmt = table.getFlexCellFormatter();
-      fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().iconHeader());
-      fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader());
-      fmt.addStyleName(0, 3, Gerrit.RESOURCES.css().dataHeader());
-      fmt.setRowSpan(0, 0, 2);
-      fmt.setRowSpan(0, 1, 2);
-      fmt.setRowSpan(0, 2, 2);
-      DOM.setElementProperty(fmt.getElement(0, 3), "align", "center");
-
-      fmt.setColSpan(0, 3, 3);
-      table.setText(1, 0, Util.C.watchedProjectColumnNewChanges());
-      table.setText(1, 1, Util.C.watchedProjectColumnAllComments());
-      table.setText(1, 2, Util.C.watchedProjectColumnSubmittedChanges());
-      fmt.addStyleName(1, 0, Gerrit.RESOURCES.css().dataHeader());
-      fmt.addStyleName(1, 1, Gerrit.RESOURCES.css().dataHeader());
-      fmt.addStyleName(1, 2, Gerrit.RESOURCES.css().dataHeader());
-    }
-
-    void deleteChecked() {
-      final HashSet<AccountProjectWatch.Key> ids =
-          new HashSet<AccountProjectWatch.Key>();
-      for (int row = 1; row < table.getRowCount(); row++) {
-        final AccountProjectWatchInfo k = getRowItem(row);
-        if (k != null && ((CheckBox) table.getWidget(row, 1)).getValue()) {
-          ids.add(k.getWatch().getKey());
+  protected void populateProjects() {
+    Util.PROJECT_SVC.visibleProjects(
+        new GerritCallback<List<Project>>() {
+      @Override
+      public void onSuccess(final List<Project> result) {
+        projectsTab.display(result);
+        if (firstPopupLoad) { // Display was delayed until table was loaded
+          firstPopupLoad = false;
+          displayPopup();
         }
       }
-      if (!ids.isEmpty()) {
-        Util.ACCOUNT_SVC.deleteProjectWatches(ids,
-            new GerritCallback<VoidResult>() {
-              public void onSuccess(final VoidResult result) {
-                for (int row = 1; row < table.getRowCount();) {
-                  final AccountProjectWatchInfo k = getRowItem(row);
-                  if (k != null && ids.contains(k.getWatch().getKey())) {
-                    table.removeRow(row);
-                  } else {
-                    row++;
-                  }
-                }
-              }
-            });
-      }
-    }
-
-    void insertWatch(final AccountProjectWatchInfo k) {
-      final String newName = k.getProject().getName();
-      int row = 1;
-      for (; row < table.getRowCount(); row++) {
-        final AccountProjectWatchInfo i = getRowItem(row);
-        if (i != null && i.getProject().getName().compareTo(newName) >= 0) {
-          break;
-        }
-      }
-
-      table.insertRow(row);
-      applyDataRowStyle(row);
-      populate(row, k);
-    }
-
-    void display(final List<AccountProjectWatchInfo> result) {
-      while (2 < table.getRowCount())
-        table.removeRow(table.getRowCount() - 1);
-
-      for (final AccountProjectWatchInfo k : result) {
-        final int row = table.getRowCount();
-        table.insertRow(row);
-        applyDataRowStyle(row);
-        populate(row, k);
-      }
-    }
-
-    void populate(final int row, final AccountProjectWatchInfo k) {
-      final FlowPanel fp = new FlowPanel();
-      fp.add(new ProjectLink(k.getProject().getNameKey(), Status.NEW));
-      if (k.getWatch().getFilter() != null) {
-        Label filter = new Label(k.getWatch().getFilter());
-        filter.setStyleName(Gerrit.RESOURCES.css().watchedProjectFilter());
-        fp.add(filter);
-      }
-
-      table.setWidget(row, 1, new CheckBox());
-      table.setWidget(row, 2, fp);
-      {
-        final CheckBox notifyNewChanges = new CheckBox();
-        notifyNewChanges.addClickHandler(new ClickHandler() {
-          @Override
-          public void onClick(final ClickEvent event) {
-            final boolean oldVal = k.getWatch().isNotifyNewChanges();
-            k.getWatch().setNotifyNewChanges(notifyNewChanges.getValue());
-            Util.ACCOUNT_SVC.updateProjectWatch(k.getWatch(),
-                new GerritCallback<VoidResult>() {
-                  public void onSuccess(final VoidResult result) {
-                  }
-
-                  @Override
-                  public void onFailure(final Throwable caught) {
-                    k.getWatch().setNotifyNewChanges(oldVal);
-                    notifyNewChanges.setValue(oldVal);
-                    super.onFailure(caught);
-                  }
-                });
-          }
-        });
-        notifyNewChanges.setValue(k.getWatch().isNotifyNewChanges());
-        table.setWidget(row, 3, notifyNewChanges);
-      }
-      {
-        final CheckBox notifyAllComments = new CheckBox();
-        notifyAllComments.addClickHandler(new ClickHandler() {
-          @Override
-          public void onClick(final ClickEvent event) {
-            final boolean oldVal = k.getWatch().isNotifyAllComments();
-            k.getWatch().setNotifyAllComments(notifyAllComments.getValue());
-            Util.ACCOUNT_SVC.updateProjectWatch(k.getWatch(),
-                new GerritCallback<VoidResult>() {
-                  public void onSuccess(final VoidResult result) {
-                  }
-
-                  @Override
-                  public void onFailure(final Throwable caught) {
-                    k.getWatch().setNotifyAllComments(oldVal);
-                    notifyAllComments.setValue(oldVal);
-                    super.onFailure(caught);
-                  }
-                });
-          }
-        });
-        notifyAllComments.setValue(k.getWatch().isNotifyAllComments());
-        table.setWidget(row, 4, notifyAllComments);
-      }
-      {
-        final CheckBox notifySubmittedChanges = new CheckBox();
-        notifySubmittedChanges.addClickHandler(new ClickHandler() {
-          @Override
-          public void onClick(final ClickEvent event) {
-            final boolean oldVal = k.getWatch().isNotifySubmittedChanges();
-            k.getWatch().setNotifySubmittedChanges(
-                notifySubmittedChanges.getValue());
-            Util.ACCOUNT_SVC.updateProjectWatch(k.getWatch(),
-                new GerritCallback<VoidResult>() {
-                  public void onSuccess(final VoidResult result) {
-                  }
-
-                  @Override
-                  public void onFailure(final Throwable caught) {
-                    k.getWatch().setNotifySubmittedChanges(oldVal);
-                    notifySubmittedChanges.setValue(oldVal);
-                    super.onFailure(caught);
-                  }
-                });
-          }
-        });
-        notifySubmittedChanges
-            .setValue(k.getWatch().isNotifySubmittedChanges());
-        table.setWidget(row, 5, notifySubmittedChanges);
-      }
-
-      final FlexCellFormatter fmt = table.getFlexCellFormatter();
-      fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().iconCell());
-      fmt.addStyleName(row, 2, Gerrit.RESOURCES.css().dataCell());
-      fmt.addStyleName(row, 3, Gerrit.RESOURCES.css().dataCell());
-      fmt.addStyleName(row, 4, Gerrit.RESOURCES.css().dataCell());
-      fmt.addStyleName(row, 5, Gerrit.RESOURCES.css().dataCell());
-
-      setRowItem(row, k);
-    }
+    });
   }
 }
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
new file mode 100644
index 0000000..be808fa
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchesTable.java
@@ -0,0 +1,178 @@
+// 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.account;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.FancyFlexTable;
+import com.google.gerrit.client.ui.ProjectLink;
+import com.google.gerrit.common.data.AccountProjectWatchInfo;
+import com.google.gerrit.reviewdb.AccountProjectWatch;
+import com.google.gerrit.reviewdb.Change.Status;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.ui.CheckBox;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
+import com.google.gwtjsonrpc.client.VoidResult;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class MyWatchesTable extends FancyFlexTable<AccountProjectWatchInfo> {
+
+  public MyWatchesTable() {
+    table.setWidth("");
+    table.insertRow(1);
+    table.setText(0, 2, Util.C.watchedProjectName());
+    table.setText(0, 3, Util.C.watchedProjectColumnEmailNotifications());
+
+    final FlexCellFormatter fmt = table.getFlexCellFormatter();
+    fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().iconHeader());
+    fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader());
+    fmt.addStyleName(0, 3, Gerrit.RESOURCES.css().dataHeader());
+    fmt.setRowSpan(0, 0, 2);
+    fmt.setRowSpan(0, 1, 2);
+    fmt.setRowSpan(0, 2, 2);
+    DOM.setElementProperty(fmt.getElement(0, 3), "align", "center");
+
+    fmt.setColSpan(0, 3, 3);
+    table.setText(1, 0, Util.C.watchedProjectColumnNewChanges());
+    table.setText(1, 1, Util.C.watchedProjectColumnAllComments());
+    table.setText(1, 2, Util.C.watchedProjectColumnSubmittedChanges());
+    fmt.addStyleName(1, 0, Gerrit.RESOURCES.css().dataHeader());
+    fmt.addStyleName(1, 1, Gerrit.RESOURCES.css().dataHeader());
+    fmt.addStyleName(1, 2, Gerrit.RESOURCES.css().dataHeader());
+  }
+
+  public void deleteChecked() {
+    final Set<AccountProjectWatch.Key> ids = getCheckedIds();
+    if (!ids.isEmpty()) {
+      Util.ACCOUNT_SVC.deleteProjectWatches(ids,
+          new GerritCallback<VoidResult>() {
+            public void onSuccess(final VoidResult result) {
+              remove(ids);
+            }
+          });
+    }
+  }
+
+  protected void remove(Set<AccountProjectWatch.Key> ids) {
+    for (int row = 1; row < table.getRowCount();) {
+      final AccountProjectWatchInfo k = getRowItem(row);
+      if (k != null && ids.contains(k.getWatch().getKey())) {
+        table.removeRow(row);
+      } else {
+        row++;
+      }
+    }
+  }
+
+  protected Set<AccountProjectWatch.Key> getCheckedIds() {
+    final Set<AccountProjectWatch.Key> ids =
+        new HashSet<AccountProjectWatch.Key>();
+    for (int row = 1; row < table.getRowCount(); row++) {
+      final AccountProjectWatchInfo k = getRowItem(row);
+      if (k != null && ((CheckBox) table.getWidget(row, 1)).getValue()) {
+        ids.add(k.getWatch().getKey());
+      }
+    }
+    return ids;
+  }
+
+  public void insertWatch(final AccountProjectWatchInfo k) {
+    final String newName = k.getProject().getName();
+    int row = 1;
+    for (; row < table.getRowCount(); row++) {
+      final AccountProjectWatchInfo i = getRowItem(row);
+      if (i != null && i.getProject().getName().compareTo(newName) >= 0) {
+        break;
+      }
+    }
+
+    table.insertRow(row);
+    applyDataRowStyle(row);
+    populate(row, k);
+  }
+
+  public void display(final List<AccountProjectWatchInfo> result) {
+    while (2 < table.getRowCount())
+      table.removeRow(table.getRowCount() - 1);
+
+    for (final AccountProjectWatchInfo k : result) {
+      final int row = table.getRowCount();
+      table.insertRow(row);
+      applyDataRowStyle(row);
+      populate(row, k);
+    }
+  }
+
+  protected void populate(final int row, final AccountProjectWatchInfo info) {
+    final FlowPanel fp = new FlowPanel();
+    fp.add(new ProjectLink(info.getProject().getNameKey(), Status.NEW));
+    if (info.getWatch().getFilter() != null) {
+      Label filter = new Label(info.getWatch().getFilter());
+      filter.setStyleName(Gerrit.RESOURCES.css().watchedProjectFilter());
+      fp.add(filter);
+    }
+
+    table.setWidget(row, 1, new CheckBox());
+    table.setWidget(row, 2, fp);
+
+    addNotifyButton(AccountProjectWatch.Type.NEW_CHANGES, info, row, 3);
+    addNotifyButton(AccountProjectWatch.Type.COMMENTS, info, row, 4);
+    addNotifyButton(AccountProjectWatch.Type.SUBMITS, info, row, 5);
+
+    final FlexCellFormatter fmt = table.getFlexCellFormatter();
+    fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().iconCell());
+    fmt.addStyleName(row, 2, Gerrit.RESOURCES.css().dataCell());
+    fmt.addStyleName(row, 3, Gerrit.RESOURCES.css().dataCell());
+    fmt.addStyleName(row, 4, Gerrit.RESOURCES.css().dataCell());
+    fmt.addStyleName(row, 5, Gerrit.RESOURCES.css().dataCell());
+
+    setRowItem(row, info);
+  }
+
+  protected void addNotifyButton(final AccountProjectWatch.Type type,
+      final AccountProjectWatchInfo info, final int row, final int col) {
+    final CheckBox cbox = new CheckBox();
+
+    cbox.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(final ClickEvent event) {
+        final boolean oldVal = info.getWatch().isNotify(type);
+        info.getWatch().setNotify(type, cbox.getValue());
+        Util.ACCOUNT_SVC.updateProjectWatch(info.getWatch(),
+            new GerritCallback<VoidResult>() {
+              public void onSuccess(final VoidResult result) {
+              }
+
+              @Override
+              public void onFailure(final Throwable caught) {
+                info.getWatch().setNotify(type, oldVal);
+                cbox.setValue(oldVal);
+                super.onFailure(caught);
+              }
+            });
+      }
+    });
+
+    cbox.setValue(info.getWatch().isNotify(type));
+    table.setWidget(row, col, cbox);
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java
index a0aeced..15b0e4e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java
@@ -18,8 +18,8 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.AccountScreen;
+import com.google.gerrit.client.ui.OnEditEnabler;
 import com.google.gerrit.client.ui.SmallHeading;
-import com.google.gerrit.client.ui.TextSaveButtonListener;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.AgreementInfo;
 import com.google.gerrit.reviewdb.AccountAgreement;
@@ -144,7 +144,7 @@
     });
     finalGroup.add(submit);
     formBody.add(finalGroup);
-    new TextSaveButtonListener(yesIAgreeBox, submit);
+    new OnEditEnabler(submit, yesIAgreeBox);
 
     final FormPanel form = new FormPanel();
     form.add(formBody);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java
index 93ccb74..73e784b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java
@@ -17,7 +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.TextSaveButtonListener;
+import com.google.gerrit.client.ui.OnEditEnabler;
 import com.google.gerrit.common.errors.InvalidUserNameException;
 import com.google.gerrit.reviewdb.Account;
 import com.google.gwt.event.dom.client.ClickEvent;
@@ -73,7 +73,7 @@
           doSetUserName();
         }
       });
-      new TextSaveButtonListener(userNameTxt, setUserName);
+      new OnEditEnabler(setUserName, userNameTxt);
 
       userNameLbl.setVisible(false);
       body.add(userNameLbl);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/Util.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/Util.java
index 5ed79e6..59e439d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/Util.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/Util.java
@@ -16,6 +16,8 @@
 
 import com.google.gerrit.common.data.AccountSecurity;
 import com.google.gerrit.common.data.AccountService;
+import com.google.gerrit.common.data.ProjectAdminService;
+import com.google.gerrit.reviewdb.Project;
 import com.google.gwt.core.client.GWT;
 import com.google.gwtjsonrpc.client.JsonUtil;
 
@@ -24,6 +26,7 @@
   public static final AccountMessages M = GWT.create(AccountMessages.class);
   public static final AccountService ACCOUNT_SVC;
   public static final AccountSecurity ACCOUNT_SEC;
+  public static final ProjectAdminService PROJECT_SVC;
 
   static {
     ACCOUNT_SVC = GWT.create(AccountService.class);
@@ -31,5 +34,8 @@
 
     ACCOUNT_SEC = GWT.create(AccountSecurity.class);
     JsonUtil.bind(ACCOUNT_SEC, "rpc/AccountSecurity");
+
+    PROJECT_SVC = GWT.create(ProjectAdminService.class);
+    JsonUtil.bind(PROJECT_SVC, "rpc/ProjectAdminService");
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessRightEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessRightEditor.java
new file mode 100644
index 0000000..762cc47
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessRightEditor.java
@@ -0,0 +1,408 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.admin;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.AccountGroupSuggestOracle;
+import com.google.gerrit.client.ui.HintTextBox;
+import com.google.gerrit.client.ui.RPCSuggestOracle;
+import com.google.gerrit.common.data.ApprovalType;
+import com.google.gerrit.common.data.ProjectDetail;
+import com.google.gerrit.reviewdb.ApprovalCategory;
+import com.google.gerrit.reviewdb.ApprovalCategoryValue;
+import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.RefRight;
+import com.google.gwt.event.dom.client.BlurEvent;
+import com.google.gwt.event.dom.client.BlurHandler;
+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.FocusEvent;
+import com.google.gwt.event.dom.client.FocusHandler;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.event.logical.shared.ValueChangeHandler;
+import com.google.gwt.event.logical.shared.HasValueChangeHandlers;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.Grid;
+import com.google.gwt.user.client.ui.ListBox;
+import com.google.gwt.user.client.ui.SuggestBox;
+
+public class AccessRightEditor extends Composite
+    implements HasValueChangeHandlers<ProjectDetail> {
+  private Project.NameKey projectKey;
+  private ListBox catBox;
+  private HintTextBox nameTxt;
+  private SuggestBox nameSug;
+  private HintTextBox referenceTxt;
+  private ListBox topBox;
+  private ListBox botBox;
+  private Button addBut;
+  private Button clearBut;
+
+  public AccessRightEditor(final Project.NameKey key) {
+    projectKey = key;
+
+    initWidgets();
+    initCategories();
+
+    final Grid grid = new Grid(5, 2);
+    grid.setText(0, 0, Util.C.columnApprovalCategory() + ":");
+    grid.setWidget(0, 1, catBox);
+
+    grid.setText(1, 0, Util.C.columnGroupName() + ":");
+    grid.setWidget(1, 1, nameSug);
+
+    grid.setText(2, 0, Util.C.columnRefName() + ":");
+    grid.setWidget(2, 1, referenceTxt);
+
+    grid.setText(3, 0, Util.C.columnRightRange() + ":");
+    grid.setWidget(3, 1, topBox);
+
+    grid.setText(4, 0, "");
+    grid.setWidget(4, 1, botBox);
+
+    FlowPanel fp = new FlowPanel();
+    fp.setStyleName(Gerrit.RESOURCES.css().addSshKeyPanel());
+
+    fp.add(grid);
+    fp.add(addBut);
+    fp.add(clearBut);
+    initWidget(fp);
+  }
+
+  protected void initWidgets() {
+    catBox = new ListBox();
+    catBox.addChangeHandler(new ChangeHandler() {
+      @Override
+      public void onChange(final ChangeEvent event) {
+        updateCategorySelection();
+      }
+    });
+
+    nameTxt = new HintTextBox();
+    nameSug = new SuggestBox(new RPCSuggestOracle(
+        new AccountGroupSuggestOracle()), nameTxt);
+    nameTxt.setVisibleLength(50);
+    nameTxt.setHintText(Util.C.defaultAccountGroupName());
+
+    referenceTxt = new HintTextBox();
+    referenceTxt.setVisibleLength(50);
+    referenceTxt.setText("");
+    referenceTxt.addKeyPressHandler(new KeyPressHandler() {
+      @Override
+      public void onKeyPress(KeyPressEvent event) {
+        if (event.getCharCode() == KeyCodes.KEY_ENTER) {
+          doAddNewRight();
+        }
+      }
+    });
+
+    topBox = new ListBox();
+    botBox = new ListBox();
+
+    addBut = new Button(Util.C.buttonAddProjectRight());
+    addBut.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(final ClickEvent event) {
+        doAddNewRight();
+      }
+    });
+
+    clearBut = new Button(Util.C.buttonClearProjectRight());
+    clearBut.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(final ClickEvent event) {
+        clear();
+      }
+    });
+  }
+
+  protected void initCategories() {
+    for (final ApprovalType at : Gerrit.getConfig().getApprovalTypes()
+        .getApprovalTypes()) {
+      final ApprovalCategory c = at.getCategory();
+      catBox.addItem(c.getName(), c.getId().get());
+    }
+    for (final ApprovalType at : Gerrit.getConfig().getApprovalTypes()
+        .getActionTypes()) {
+      final ApprovalCategory c = at.getCategory();
+      if (Gerrit.getConfig().getWildProject().equals(projectKey)
+          && ApprovalCategory.OWN.equals(c.getId())) {
+        // Giving out control of the WILD_PROJECT to other groups beyond
+        // Administrators is dangerous. Having control over WILD_PROJECT
+        // is about the same as having Administrator access as users are
+        // able to affect grants in all projects on the system.
+        //
+        continue;
+      }
+      catBox.addItem(c.getName(), c.getId().get());
+    }
+
+    if (catBox.getItemCount() > 0) {
+      catBox.setSelectedIndex(0);
+      updateCategorySelection();
+    }
+  }
+
+  public void enableForm(final boolean on) {
+    final boolean canAdd = on && catBox.getItemCount() > 0;
+    addBut.setEnabled(canAdd);
+    clearBut.setEnabled(canAdd);
+    nameTxt.setEnabled(canAdd);
+    referenceTxt.setEnabled(canAdd);
+    catBox.setEnabled(canAdd);
+    topBox.setEnabled(canAdd);
+    botBox.setEnabled(canAdd);
+  }
+
+  public void clear() {
+    setCat(null);
+    setName("");
+    setReference("");
+  }
+
+  public void load(final RefRight right, final AccountGroup group) {
+    final ApprovalType atype =
+       Gerrit.getConfig().getApprovalTypes().getApprovalType(
+          right.getApprovalCategoryId());
+
+    setCat(atype != null ? atype.getCategory().getName()
+                         : right.getApprovalCategoryId().get() );
+
+    setName(group.getName());
+    setReference(right.getRefPatternForDisplay());
+
+    setRange(atype.getCategory().isRange() ? atype.getValue(right.getMinValue())
+             : null, atype.getValue(right.getMaxValue()) );
+  }
+
+  protected void doAddNewRight() {
+    final ApprovalType at = getApprovalType();
+    ApprovalCategoryValue min = getMin(at);
+    ApprovalCategoryValue max = getMax(at);
+
+    if (at == null || min == null || max == null) {
+      return;
+    }
+
+    final String groupName = nameSug.getText();
+    if ("".equals(groupName)
+        || Util.C.defaultAccountGroupName().equals(groupName)) {
+      return;
+    }
+
+    final String refPattern = referenceTxt.getText();
+
+    addBut.setEnabled(false);
+    Util.PROJECT_SVC.addRight(projectKey, at.getCategory().getId(),
+        groupName, refPattern, min.getValue(), max.getValue(),
+        new GerritCallback<ProjectDetail>() {
+          public void onSuccess(final ProjectDetail result) {
+            addBut.setEnabled(true);
+            nameSug.setText("");
+            referenceTxt.setText("");
+            ValueChangeEvent.fire(AccessRightEditor.this, result);
+          }
+
+          @Override
+          public void onFailure(final Throwable caught) {
+            addBut.setEnabled(true);
+            super.onFailure(caught);
+          }
+        });
+  }
+
+  protected void updateCategorySelection() {
+    final ApprovalType at = getApprovalType();
+
+    if (at == null || at.getValues().isEmpty()) {
+      topBox.setEnabled(false);
+      botBox.setEnabled(false);
+      referenceTxt.setEnabled(false);
+      addBut.setEnabled(false);
+      clearBut.setEnabled(false);
+      return;
+    }
+
+    updateRanges(at);
+  }
+
+  protected void updateRanges(final ApprovalType at) {
+    ApprovalCategoryValue min = null, max = null, last = null;
+
+    topBox.clear();
+    botBox.clear();
+
+    for(final ApprovalCategoryValue v : at.getValues()) {
+      final int nval = v.getValue();
+      final String vStr = String.valueOf(nval);
+
+      String nStr = vStr + ": " + v.getName();
+      if (nval > 0) {
+        nStr = "+" + nStr;
+      }
+
+      topBox.addItem(nStr, vStr);
+      botBox.addItem(nStr, vStr);
+
+      if (min == null || nval < 0) {
+        min = v;
+      } else if (max == null && nval > 0) {
+        max = v;
+      }
+      last = v;
+    }
+
+    if (max == null) {
+      max = last;
+    }
+
+    if (ApprovalCategory.READ.equals(at.getCategory().getId())) {
+      // Special case; for READ the most logical range is just
+      // +1 READ, so assume that as the default for both.
+      min = max;
+    }
+
+    if (! at.getCategory().isRange()) {
+      max = null;
+    }
+
+    setRange(min, max);
+  }
+
+  protected void setCat(final String cat) {
+    if (cat == null) {
+      catBox.setSelectedIndex(0);
+    } else {
+      setSelectedText(catBox, cat);
+    }
+    updateCategorySelection();
+  }
+
+  protected void setName(final String name) {
+    nameTxt.setText(name);
+  }
+
+  protected void setReference(final String ref) {
+    referenceTxt.setText(ref);
+  }
+
+  protected void setRange(final ApprovalCategoryValue min,
+                          final ApprovalCategoryValue max) {
+    if (min == null || max == null) {
+      botBox.setVisible(false);
+      if (max != null) {
+        setSelectedValue(topBox, "" + max.getValue());
+        return;
+      }
+    } else {
+      botBox.setVisible(true);
+      setSelectedValue(botBox, "" + max.getValue());
+    }
+    setSelectedValue(topBox, "" + min.getValue());
+  }
+
+  private ApprovalType getApprovalType() {
+    int idx = catBox.getSelectedIndex();
+    if (idx < 0) {
+      return null;
+    }
+    return Gerrit.getConfig().getApprovalTypes().getApprovalType(
+             new ApprovalCategory.Id(catBox.getValue(idx)));
+  }
+
+  public ApprovalCategoryValue getMin(ApprovalType at) {
+    final ApprovalCategoryValue top = getTop(at);
+    final ApprovalCategoryValue bot = getBot(at);
+    if (bot == null) {
+      for (final ApprovalCategoryValue v : at.getValues()) {
+        if (0 <= v.getValue() && v.getValue() <= top.getValue()) {
+          return v;
+        }
+      }
+      return at.getMin();
+    }
+
+    if (top.getValue() > bot.getValue()) {
+      return bot;
+    }
+    return top;
+  }
+
+  public ApprovalCategoryValue getMax(ApprovalType at) {
+    final ApprovalCategoryValue top = getTop(at);
+    final ApprovalCategoryValue bot = getBot(at);
+    if (bot == null || bot.getValue() < top.getValue()) {
+      return top;
+    }
+    return bot;
+  }
+
+  protected ApprovalCategoryValue getTop(ApprovalType at) {
+    int idx = topBox.getSelectedIndex();
+    if (idx < 0) {
+      return null;
+    }
+    return at.getValue(Short.parseShort(topBox.getValue(idx)));
+  }
+
+  protected ApprovalCategoryValue getBot(ApprovalType at) {
+    int idx = botBox.getSelectedIndex();
+    if (idx < 0 || ! botBox.isVisible()) {
+      return null;
+    }
+    return at.getValue(Short.parseShort(botBox.getValue(idx)));
+  }
+
+  public HandlerRegistration addValueChangeHandler(
+      final ValueChangeHandler<ProjectDetail> handler) {
+    return addHandler(handler, ValueChangeEvent.getType());
+  }
+
+  public static boolean setSelectedText(ListBox box, String text) {
+    if (text == null) {
+      return false;
+    }
+    for (int i =0 ; i < box.getItemCount(); i++) {
+      if (text.equals(box.getItemText(i))) {
+        box.setSelectedIndex(i);
+        return true;
+      }
+    }
+    return false;
+  }
+
+  public static boolean setSelectedValue(ListBox box, String value) {
+    if (value == null) {
+      return false;
+    }
+    for (int i =0 ; i < box.getItemCount(); i++) {
+      if (value.equals(box.getValue(i))) {
+        box.setSelectedIndex(i);
+        return true;
+      }
+    }
+    return false;
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupScreen.java
index dd76875..389f22b 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
@@ -22,8 +22,9 @@
 import com.google.gerrit.client.ui.AccountScreen;
 import com.google.gerrit.client.ui.AddMemberBox;
 import com.google.gerrit.client.ui.FancyFlexTable;
+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.client.ui.TextSaveButtonListener;
 import com.google.gerrit.common.data.AccountInfoCache;
 import com.google.gerrit.common.data.GroupDetail;
 import com.google.gerrit.reviewdb.Account;
@@ -134,7 +135,7 @@
     groupNamePanel.add(saveName);
     add(groupNamePanel);
 
-    new TextSaveButtonListener(groupNameTxt, saveName);
+    new OnEditEnabler(saveName, groupNameTxt);
   }
 
   private void initOwner() {
@@ -143,7 +144,8 @@
 
     ownerTxtBox = new NpTextBox();
     ownerTxtBox.setVisibleLength(60);
-    ownerTxt = new SuggestBox(new AccountGroupSuggestOracle(), ownerTxtBox);
+    ownerTxt = new SuggestBox(new RPCSuggestOracle(
+        new AccountGroupSuggestOracle()), ownerTxtBox);
     ownerPanel.add(ownerTxt);
 
     saveOwner = new Button(Util.C.buttonChangeGroupOwner());
@@ -165,7 +167,7 @@
     ownerPanel.add(saveOwner);
     add(ownerPanel);
 
-    new TextSaveButtonListener(ownerTxtBox, saveOwner);
+    new OnEditEnabler(saveOwner, ownerTxtBox);
   }
 
   private void initDescription() {
@@ -194,7 +196,7 @@
     vp.add(saveDesc);
     add(vp);
 
-    new TextSaveButtonListener(descTxt, saveDesc);
+    new OnEditEnabler(saveDesc, descTxt);
   }
 
   private void initGroupType() {
@@ -223,6 +225,7 @@
       case HTTP_LDAP:
       case LDAP:
       case LDAP_BIND:
+      case CLIENT_SSL_CERT_LDAP:
         break;
       default:
         return;
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 20de3e3..6f27ce5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
@@ -31,14 +31,17 @@
   String buttonChangeGroupType();
   String buttonSelectGroup();
   String buttonAddProjectRight();
+  String buttonClearProjectRight();
   String buttonSaveChanges();
+  String useContentMerge();
   String useContributorAgreements();
   String useSignedOffBy();
+  String requireChangeID();
 
   String headingOwner();
   String headingParentProjectName();
   String headingDescription();
-  String headingSubmitType();
+  String headingProjectOptions();
   String headingGroupType();
   String headingMembers();
   String headingExternalGroup();
@@ -87,4 +90,5 @@
 
   String noGroupSelected();
   String errorNoMatchingGroups();
+  String errorNoGitRepository();
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
index 28917f1..0874c37 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
@@ -12,14 +12,17 @@
 buttonChangeGroupType = Change Type
 buttonSelectGroup = Select
 buttonAddProjectRight = Add Access Right
+buttonClearProjectRight = Clear Form
 buttonSaveChanges = Save Changes
+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
 
 headingOwner = Owners
 headingParentProjectName = Rights Inherit From
 headingDescription = Description
-headingSubmitType = Change Submit Action
+headingProjectOptions = Project Options
 headingGroupType = Group Type
 headingMembers = Members
 headingExternalGroup = Selected External Group
@@ -68,3 +71,4 @@
 
 noGroupSelected = (No group selected)
 errorNoMatchingGroups = No Matching Groups
+errorNoGitRepository = No Git Repository
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
index 0586c84..9c51b77 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
@@ -19,11 +19,16 @@
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.client.ui.AccountScreen;
+import com.google.gerrit.client.ui.OnEditEnabler;
 import com.google.gerrit.client.ui.SmallHeading;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gwt.event.dom.client.BlurEvent;
+import com.google.gwt.event.dom.client.BlurHandler;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.FocusEvent;
+import com.google.gwt.event.dom.client.FocusHandler;
 import com.google.gwt.event.dom.client.KeyCodes;
 import com.google.gwt.event.dom.client.KeyPressEvent;
 import com.google.gwt.event.dom.client.KeyPressHandler;
@@ -78,14 +83,36 @@
     fp.add(addTxt);
 
     addNew = new Button(Util.C.buttonCreateGroup());
+    addNew.setEnabled(false);
     addNew.addClickHandler(new ClickHandler() {
       @Override
       public void onClick(final ClickEvent event) {
         doCreateGroup();
       }
     });
+    addNew.addFocusHandler(new FocusHandler() {
+      @Override
+      public void onFocus(FocusEvent event) {
+        // unregister the keys for the 'groups' table so that pressing ENTER
+        // when the 'addNew' button has the focus triggers the button (if the
+        // keys for the 'groups' table would not be unregistered the 'addNew'
+        // button would not be triggered on ENTER but the group which is
+        // selected in the 'groups' table would be opened)
+        groups.setRegisterKeys(false);
+      }
+    });
+    addNew.addBlurHandler(new BlurHandler() {
+      @Override
+      public void onBlur(BlurEvent event) {
+        // re-register the keys for the 'groups' table when the 'addNew' button
+        // gets blurred
+        groups.setRegisterKeys(true);
+      }
+    });
     fp.add(addNew);
     add(fp);
+
+    new OnEditEnabler(addNew, addTxt);
   }
 
   @Override
@@ -100,10 +127,17 @@
       return;
     }
 
+    addNew.setEnabled(false);
     Util.GROUP_SVC.createGroup(newName, new GerritCallback<AccountGroup.Id>() {
       public void onSuccess(final AccountGroup.Id result) {
         History.newItem(Dispatcher.toAccountGroup(result));
       }
+
+      @Override
+      public void onFailure(Throwable caught) {
+        super.onFailure(caught);
+        addNew.setEnabled(true);
+      }
     });
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java
index 14449f9..5b863c7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java
@@ -18,7 +18,6 @@
 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.AccountGroupSuggestOracle;
 import com.google.gerrit.client.ui.FancyFlexTable;
 import com.google.gerrit.client.ui.Hyperlink;
 import com.google.gerrit.client.ui.SmallHeading;
@@ -27,31 +26,18 @@
 import com.google.gerrit.common.data.InheritedRefRight;
 import com.google.gerrit.common.data.ProjectDetail;
 import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.ApprovalCategory;
 import com.google.gerrit.reviewdb.ApprovalCategoryValue;
 import com.google.gerrit.reviewdb.Project;
 import com.google.gerrit.reviewdb.RefRight;
-import com.google.gwt.event.dom.client.BlurEvent;
-import com.google.gwt.event.dom.client.BlurHandler;
-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.FocusEvent;
-import com.google.gwt.event.dom.client.FocusHandler;
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.event.logical.shared.ValueChangeHandler;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.CheckBox;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.Grid;
-import com.google.gwt.user.client.ui.ListBox;
 import com.google.gwt.user.client.ui.Panel;
-import com.google.gwt.user.client.ui.SuggestBox;
 import com.google.gwt.user.client.ui.VerticalPanel;
 import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
-import com.google.gwtexpui.globalkey.client.NpTextBox;
 import com.google.gwtexpui.safehtml.client.SafeHtml;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 
@@ -65,14 +51,7 @@
 
   private RightsTable rights;
   private Button delRight;
-  private Button addRight;
-  private ListBox catBox;
-  private ListBox rangeMinBox;
-  private ListBox rangeMaxBox;
-  private NpTextBox nameTxtBox;
-  private SuggestBox nameTxt;
-  private NpTextBox referenceTxt;
-  private FlowPanel addPanel;
+  private AccessRightEditor rightEditor;
 
   public ProjectAccessScreen(final Project.NameKey toShow) {
     super(toShow);
@@ -99,14 +78,7 @@
 
   private void enableForm(final boolean on) {
     delRight.setEnabled(on);
-
-    final boolean canAdd = on && catBox.getItemCount() > 0;
-    addRight.setEnabled(canAdd);
-    nameTxtBox.setEnabled(canAdd);
-    referenceTxt.setEnabled(canAdd);
-    catBox.setEnabled(canAdd);
-    rangeMinBox.setEnabled(canAdd);
-    rangeMaxBox.setEnabled(canAdd);
+    rightEditor.enableForm(on);
   }
 
   private void initParent() {
@@ -119,102 +91,6 @@
   }
 
   private void initRights() {
-    addPanel = new FlowPanel();
-    addPanel.setStyleName(Gerrit.RESOURCES.css().addSshKeyPanel());
-
-    final Grid addGrid = new Grid(5, 2);
-
-    catBox = new ListBox();
-    rangeMinBox = new ListBox();
-    rangeMaxBox = new ListBox();
-
-    catBox.addChangeHandler(new ChangeHandler() {
-      @Override
-      public void onChange(final ChangeEvent event) {
-        updateCategorySelection();
-      }
-    });
-    for (final ApprovalType at : Gerrit.getConfig().getApprovalTypes()
-        .getApprovalTypes()) {
-      final ApprovalCategory c = at.getCategory();
-      catBox.addItem(c.getName(), c.getId().get());
-    }
-    for (final ApprovalType at : Gerrit.getConfig().getApprovalTypes()
-        .getActionTypes()) {
-      final ApprovalCategory c = at.getCategory();
-      if (Gerrit.getConfig().getWildProject().equals(getProjectKey())
-          && ApprovalCategory.OWN.equals(c.getId())) {
-        // Giving out control of the WILD_PROJECT to other groups beyond
-        // Administrators is dangerous. Having control over WILD_PROJECT
-        // is about the same as having Administrator access as users are
-        // able to affect grants in all projects on the system.
-        //
-        continue;
-      }
-      catBox.addItem(c.getName(), c.getId().get());
-    }
-
-    addGrid.setText(0, 0, Util.C.columnApprovalCategory() + ":");
-    addGrid.setWidget(0, 1, catBox);
-
-    nameTxtBox = new NpTextBox();
-    nameTxt = new SuggestBox(new AccountGroupSuggestOracle(), nameTxtBox);
-    nameTxtBox.setVisibleLength(50);
-    nameTxtBox.setText(Util.C.defaultAccountGroupName());
-    nameTxtBox.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
-    nameTxtBox.addFocusHandler(new FocusHandler() {
-      @Override
-      public void onFocus(FocusEvent event) {
-        if (Util.C.defaultAccountGroupName().equals(nameTxtBox.getText())) {
-          nameTxtBox.setText("");
-          nameTxtBox.removeStyleName(Gerrit.RESOURCES.css()
-              .inputFieldTypeHint());
-        }
-      }
-    });
-    nameTxtBox.addBlurHandler(new BlurHandler() {
-      @Override
-      public void onBlur(BlurEvent event) {
-        if ("".equals(nameTxtBox.getText())) {
-          nameTxtBox.setText(Util.C.defaultAccountGroupName());
-          nameTxtBox.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
-        }
-      }
-    });
-    addGrid.setText(1, 0, Util.C.columnGroupName() + ":");
-    addGrid.setWidget(1, 1, nameTxt);
-
-    referenceTxt = new NpTextBox();
-    referenceTxt.setVisibleLength(50);
-    referenceTxt.setText("");
-    referenceTxt.addKeyPressHandler(new KeyPressHandler() {
-      @Override
-      public void onKeyPress(KeyPressEvent event) {
-        if (event.getCharCode() == KeyCodes.KEY_ENTER) {
-          doAddNewRight();
-        }
-      }
-    });
-
-    addGrid.setText(2, 0, Util.C.columnRefName() + ":");
-    addGrid.setWidget(2, 1, referenceTxt);
-
-    addGrid.setText(3, 0, Util.C.columnRightRange() + ":");
-    addGrid.setWidget(3, 1, rangeMinBox);
-
-    addGrid.setText(4, 0, "");
-    addGrid.setWidget(4, 1, rangeMaxBox);
-
-    addRight = new Button(Util.C.buttonAddProjectRight());
-    addRight.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(final ClickEvent event) {
-        doAddNewRight();
-      }
-    });
-    addPanel.add(addGrid);
-    addPanel.add(addRight);
-
     rights = new RightsTable();
 
     delRight = new Button(Util.C.buttonDeleteGroupMembers());
@@ -226,15 +102,18 @@
       }
     });
 
+    rightEditor = new AccessRightEditor(getProjectKey());
+    rightEditor.addValueChangeHandler(new ValueChangeHandler<ProjectDetail>() {
+        @Override
+        public void onValueChange(ValueChangeEvent<ProjectDetail> event) {
+          display(event.getValue());
+        }
+      });
+
     add(new SmallHeading(Util.C.headingAccessRights()));
     add(rights);
     add(delRight);
-    add(addPanel);
-
-    if (catBox.getItemCount() > 0) {
-      catBox.setSelectedIndex(0);
-      updateCategorySelection();
-    }
+    add(rightEditor);
   }
 
   void display(final ProjectDetail result) {
@@ -253,7 +132,7 @@
 
     rights.display(result.groups, result.rights);
 
-    addPanel.setVisible(result.canModifyAccess);
+    rightEditor.setVisible(result.canModifyAccess);
     delRight.setVisible(rights.getCanDelete());
   }
 
@@ -270,144 +149,9 @@
     }
   }
 
-  private void doAddNewRight() {
-    int idx = catBox.getSelectedIndex();
-    final ApprovalType at;
-    ApprovalCategoryValue min, max;
-    if (idx < 0) {
-      return;
-    }
-    at =
-        Gerrit.getConfig().getApprovalTypes().getApprovalType(
-            new ApprovalCategory.Id(catBox.getValue(idx)));
-    if (at == null) {
-      return;
-    }
-
-    idx = rangeMinBox.getSelectedIndex();
-    if (idx < 0) {
-      return;
-    }
-    min = at.getValue(Short.parseShort(rangeMinBox.getValue(idx)));
-    if (min == null) {
-      return;
-    }
-
-    if (at.getCategory().isRange()) {
-      idx = rangeMaxBox.getSelectedIndex();
-      if (idx < 0) {
-        return;
-      }
-      max = at.getValue(Short.parseShort(rangeMaxBox.getValue(idx)));
-      if (max == null) {
-        return;
-      }
-    } else {
-      // If its not a range, the maximum box was disabled.  Use the min
-      // value as the max, and select the min from the category values.
-      //
-      max = min;
-      min = at.getMin();
-      for (ApprovalCategoryValue v : at.getValues()) {
-        if (0 <= v.getValue() && v.getValue() <= max.getValue()) {
-          min = v;
-          break;
-        }
-      }
-    }
-
-    final String groupName = nameTxt.getText();
-    if ("".equals(groupName)
-        || Util.C.defaultAccountGroupName().equals(groupName)) {
-      return;
-    }
-
-    final String refPattern = referenceTxt.getText();
-
-    if (min.getValue() > max.getValue()) {
-      // If the user selects it backwards in the web UI, help them out
-      // by reversing the order to what we would expect.
-      //
-      final ApprovalCategoryValue newMin = max;
-      final ApprovalCategoryValue newMax = min;
-      min = newMin;
-      max = newMax;
-    }
-
-    addRight.setEnabled(false);
-    Util.PROJECT_SVC.addRight(getProjectKey(), at.getCategory().getId(),
-        groupName, refPattern, min.getValue(), max.getValue(),
-        new GerritCallback<ProjectDetail>() {
-          public void onSuccess(final ProjectDetail result) {
-            addRight.setEnabled(true);
-            nameTxt.setText("");
-            referenceTxt.setText("");
-            display(result);
-          }
-
-          @Override
-          public void onFailure(final Throwable caught) {
-            addRight.setEnabled(true);
-            super.onFailure(caught);
-          }
-        });
-  }
-
-  private void updateCategorySelection() {
-    final int idx = catBox.getSelectedIndex();
-    final ApprovalType at;
-    if (idx >= 0) {
-      at =
-          Gerrit.getConfig().getApprovalTypes().getApprovalType(
-              new ApprovalCategory.Id(catBox.getValue(idx)));
-    } else {
-      at = null;
-    }
-
-    if (at == null || at.getValues().isEmpty()) {
-      rangeMinBox.setEnabled(false);
-      rangeMaxBox.setEnabled(false);
-      referenceTxt.setEnabled(false);
-      addRight.setEnabled(false);
-      return;
-    }
-
-    int curIndex = 0, minIndex = -1, maxIndex = -1;
-    rangeMinBox.clear();
-    rangeMaxBox.clear();
-    for (final ApprovalCategoryValue v : at.getValues()) {
-      final String vStr = String.valueOf(v.getValue());
-      String nStr = vStr + ": " + v.getName();
-      if (v.getValue() > 0) {
-        nStr = "+" + nStr;
-      }
-
-      rangeMinBox.addItem(nStr, vStr);
-      rangeMaxBox.addItem(nStr, vStr);
-
-      if (v.getValue() < 0) {
-        minIndex = curIndex;
-      }
-      if (maxIndex < 0 && v.getValue() > 0) {
-        maxIndex = curIndex;
-      }
-
-      curIndex++;
-    }
-    if (ApprovalCategory.READ.equals(at.getCategory().getId())) {
-      // Special case; for READ the most logical range is just
-      // +1 READ, so assume that as the default for both.
-      minIndex = maxIndex;
-    }
-    rangeMinBox.setSelectedIndex(minIndex >= 0 ? minIndex : 0);
-    rangeMaxBox.setSelectedIndex(maxIndex >= 0 ? maxIndex : curIndex - 1);
-    rangeMaxBox.setVisible(at.getCategory().isRange());
-
-    addRight.setEnabled(true);
-  }
-
   private class RightsTable extends FancyFlexTable<RefRight> {
     boolean canDelete;
+    Map<AccountGroup.Id, AccountGroup> groups;
 
     RightsTable() {
       table.setWidth("");
@@ -422,6 +166,13 @@
       fmt.addStyleName(0, 3, Gerrit.RESOURCES.css().dataHeader());
       fmt.addStyleName(0, 4, Gerrit.RESOURCES.css().dataHeader());
       fmt.addStyleName(0, 5, Gerrit.RESOURCES.css().dataHeader());
+
+      table.addClickHandler(new ClickHandler() {
+        @Override
+        public void onClick(final ClickEvent event) {
+          onOpenRow(table.getCellForEvent(event).getRowIndex());
+        }
+      });
     }
 
     HashSet<RefRight.Key> getRefRightIdsChecked() {
@@ -436,8 +187,9 @@
       return refRightIds;
     }
 
-    void display(final Map<AccountGroup.Id, AccountGroup> groups,
+    void display(final Map<AccountGroup.Id, AccountGroup> grps,
         final List<InheritedRefRight> refRights) {
+      groups = grps;
       canDelete = false;
 
       while (1 < table.getRowCount())
@@ -447,12 +199,17 @@
         final int row = table.getRowCount();
         table.insertRow(row);
         applyDataRowStyle(row);
-        populate(row, groups, r);
+        populate(row, r);
+      }
+    }
+    protected void onOpenRow(final int row) {
+      if (row > 0) {
+        RefRight right = getRowItem(row);
+        rightEditor.load(right, groups.get(right.getAccountGroupId()));
       }
     }
 
-    void populate(final int row, final Map<AccountGroup.Id, AccountGroup> groups,
-        final InheritedRefRight r) {
+    void populate(final int row, final InheritedRefRight r) {
       final GerritConfig config = Gerrit.getConfig();
       final RefRight right = r.getRight();
       final ApprovalType ar =
@@ -467,14 +224,12 @@
         canDelete = true;
       }
 
-      if (ar != null) {
-        table.setText(row, 2, ar.getCategory().getName());
-      } else {
-        table.setText(row, 2, right.getApprovalCategoryId().get());
-      }
+      table.setText(row, 2, ar != null ? ar.getCategory().getName()
+                                       : right.getApprovalCategoryId().get() );
 
       if (group != null) {
-        table.setText(row, 3, group.getName());
+        table.setWidget(row, 3, new Hyperlink(group.getName(), Dispatcher
+            .toAccountGroup(group.getId())));
       } else {
         table.setText(row, 3, Util.M.deletedGroup(right.getAccountGroupId()
             .get()));
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 8bff3e5..77c431a 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
@@ -20,18 +20,15 @@
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.client.ui.FancyFlexTable;
+import com.google.gerrit.client.ui.HintTextBox;
 import com.google.gerrit.common.data.GitwebLink;
 import com.google.gerrit.common.data.ListBranchesResult;
 import com.google.gerrit.common.errors.InvalidNameException;
 import com.google.gerrit.common.errors.InvalidRevisionException;
 import com.google.gerrit.reviewdb.Branch;
 import com.google.gerrit.reviewdb.Project;
-import com.google.gwt.event.dom.client.BlurEvent;
-import com.google.gwt.event.dom.client.BlurHandler;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.FocusEvent;
-import com.google.gwt.event.dom.client.FocusHandler;
 import com.google.gwt.event.dom.client.KeyCodes;
 import com.google.gwt.event.dom.client.KeyPressEvent;
 import com.google.gwt.event.dom.client.KeyPressHandler;
@@ -43,8 +40,8 @@
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Grid;
 import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.Label;
 import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
-import com.google.gwtexpui.globalkey.client.NpTextBox;
 import com.google.gwtjsonrpc.client.RemoteJsonException;
 
 import java.util.HashSet;
@@ -55,8 +52,8 @@
   private BranchesTable branches;
   private Button delBranch;
   private Button addBranch;
-  private NpTextBox nameTxtBox;
-  private NpTextBox irevTxtBox;
+  private HintTextBox nameTxtBox;
+  private HintTextBox irevTxtBox;
   private FlowPanel addPanel;
 
   public ProjectBranchesScreen(final Project.NameKey toShow) {
@@ -68,10 +65,22 @@
     super.onLoad();
     Util.PROJECT_SVC.listBranches(getProjectKey(),
         new ScreenLoadCallback<ListBranchesResult>(this) {
+          @Override
           public void preDisplay(final ListBranchesResult result) {
-            enableForm(true);
-            display(result.getBranches());
-            addPanel.setVisible(result.getCanAdd());
+            if (result.getNoRepository()) {
+              branches.setVisible(false);
+              addPanel.setVisible(false);
+              delBranch.setVisible(false);
+
+              Label no = new Label(Util.C.errorNoGitRepository());
+              no.setStyleName(Gerrit.RESOURCES.css().smallHeading());
+              add(no);
+
+            } else {
+              enableForm(true);
+              display(result.getBranches());
+              addPanel.setVisible(result.getCanAdd());
+            }
           }
         });
   }
@@ -97,28 +106,9 @@
 
     final Grid addGrid = new Grid(2, 2);
 
-    nameTxtBox = new NpTextBox();
+    nameTxtBox = new HintTextBox();
     nameTxtBox.setVisibleLength(50);
-    nameTxtBox.setText(Util.C.defaultBranchName());
-    nameTxtBox.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
-    nameTxtBox.addFocusHandler(new FocusHandler() {
-      @Override
-      public void onFocus(FocusEvent event) {
-        if (Util.C.defaultBranchName().equals(nameTxtBox.getText())) {
-          nameTxtBox.setText("");
-          nameTxtBox.removeStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
-        }
-      }
-    });
-    nameTxtBox.addBlurHandler(new BlurHandler() {
-      @Override
-      public void onBlur(BlurEvent event) {
-        if ("".equals(nameTxtBox.getText())) {
-          nameTxtBox.setText(Util.C.defaultBranchName());
-          nameTxtBox.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
-        }
-      }
-    });
+    nameTxtBox.setHintText(Util.C.defaultBranchName());
     nameTxtBox.addKeyPressHandler(new KeyPressHandler() {
       @Override
       public void onKeyPress(KeyPressEvent event) {
@@ -130,28 +120,9 @@
     addGrid.setText(0, 0, Util.C.columnBranchName() + ":");
     addGrid.setWidget(0, 1, nameTxtBox);
 
-    irevTxtBox = new NpTextBox();
+    irevTxtBox = new HintTextBox();
     irevTxtBox.setVisibleLength(50);
-    irevTxtBox.setText(Util.C.defaultRevisionSpec());
-    irevTxtBox.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
-    irevTxtBox.addFocusHandler(new FocusHandler() {
-      @Override
-      public void onFocus(FocusEvent event) {
-        if (Util.C.defaultRevisionSpec().equals(irevTxtBox.getText())) {
-          irevTxtBox.setText("");
-          irevTxtBox.removeStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
-        }
-      }
-    });
-    irevTxtBox.addBlurHandler(new BlurHandler() {
-      @Override
-      public void onBlur(BlurEvent event) {
-        if ("".equals(irevTxtBox.getText())) {
-          irevTxtBox.setText(Util.C.defaultRevisionSpec());
-          irevTxtBox.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
-        }
-      }
-    });
+    irevTxtBox.setHintText(Util.C.defaultRevisionSpec());
     irevTxtBox.addKeyPressHandler(new KeyPressHandler() {
       @Override
       public void onKeyPress(KeyPressEvent event) {
@@ -190,13 +161,13 @@
 
   private void doAddNewBranch() {
     String branchName = nameTxtBox.getText();
-    if ("".equals(branchName) || Util.C.defaultBranchName().equals(branchName)) {
+    if ("".equals(branchName)) {
       nameTxtBox.setFocus(true);
       return;
     }
 
     String rev = irevTxtBox.getText();
-    if ("".equals(rev) || Util.C.defaultRevisionSpec().equals(rev)) {
+    if ("".equals(rev)) {
       irevTxtBox.setText("HEAD");
       DeferredCommand.addCommand(new Command() {
         @Override
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 d308fa6..22fd8d9 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
@@ -17,16 +17,15 @@
 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.client.ui.SmallHeading;
-import com.google.gerrit.client.ui.TextSaveButtonListener;
 import com.google.gerrit.common.data.ProjectDetail;
 import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.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.event.logical.shared.ValueChangeEvent;
-import com.google.gwt.event.logical.shared.ValueChangeHandler;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.CheckBox;
 import com.google.gwt.user.client.ui.ListBox;
@@ -37,8 +36,10 @@
 public class ProjectInfoScreen extends ProjectScreen {
   private Project project;
 
-  private Panel submitTypePanel;
+  private Panel projectOptionsPanel;
+  private CheckBox requireChangeID;
   private ListBox submitType;
+  private CheckBox useContentMerge;
 
   private Panel agreementsPanel;
   private CheckBox useContributorAgreements;
@@ -47,6 +48,8 @@
   private NpTextArea descTxt;
   private Button saveProject;
 
+  private OnEditEnabler saveEnabler;
+
   public ProjectInfoScreen(final Project.NameKey toShow) {
     super(toShow);
   }
@@ -64,7 +67,7 @@
     });
 
     initDescription();
-    initSubmitType();
+    initProjectOptions();
     initAgreements();
     add(saveProject);
   }
@@ -81,7 +84,6 @@
                 result.canModifyAgreements ||
                 result.canModifyDescription ||
                 result.canModifyMergeType);
-            saveProject.setEnabled(false);
             display(result);
           }
         });
@@ -90,11 +92,11 @@
   private void enableForm(final boolean canModifyAgreements,
       final boolean canModifyDescription, final boolean canModifyMergeType) {
     submitType.setEnabled(canModifyMergeType);
+    useContentMerge.setEnabled(canModifyMergeType);
     descTxt.setEnabled(canModifyDescription);
     useContributorAgreements.setEnabled(canModifyAgreements);
     useSignedOffBy.setEnabled(canModifyAgreements);
-    saveProject.setEnabled(
-        canModifyAgreements || canModifyDescription || canModifyMergeType);
+    requireChangeID.setEnabled(canModifyMergeType);
   }
 
   private void initDescription() {
@@ -107,12 +109,13 @@
     vp.add(descTxt);
 
     add(vp);
-    new TextSaveButtonListener(descTxt, saveProject);
+    saveEnabler = new OnEditEnabler(saveProject);
+    saveEnabler.listenTo(descTxt);
   }
 
-  private void initSubmitType() {
-    submitTypePanel = new VerticalPanel();
-    submitTypePanel.add(new SmallHeading(Util.C.headingSubmitType()));
+  private void initProjectOptions() {
+    projectOptionsPanel = new VerticalPanel();
+    projectOptionsPanel.add(new SmallHeading(Util.C.headingProjectOptions()));
 
     submitType = new ListBox();
     for (final Project.SubmitType type : Project.SubmitType.values()) {
@@ -120,46 +123,66 @@
     }
     submitType.addChangeHandler(new ChangeHandler() {
       @Override
-      public void onChange(final ChangeEvent event) {
-        saveProject.setEnabled(true);
+      public void onChange(ChangeEvent event) {
+        setEnabledForUseContentMerge();
       }
     });
-    submitTypePanel.add(submitType);
-    add(submitTypePanel);
+    saveEnabler.listenTo(submitType);
+    projectOptionsPanel.add(submitType);
+
+    useContentMerge = new CheckBox(Util.C.useContentMerge(), true);
+    saveEnabler.listenTo(useContentMerge);
+    projectOptionsPanel.add(useContentMerge);
+
+    requireChangeID = new CheckBox(Util.C.requireChangeID(), true);
+    saveEnabler.listenTo(requireChangeID);
+    projectOptionsPanel.add(requireChangeID);
+
+    add(projectOptionsPanel);
+  }
+
+  /**
+   * Enables the {@link #useContentMerge} 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.
+   */
+  private void setEnabledForUseContentMerge() {
+    if (SubmitType.FAST_FORWARD_ONLY.equals(Project.SubmitType
+        .valueOf(submitType.getValue(submitType.getSelectedIndex())))) {
+      useContentMerge.setEnabled(false);
+      useContentMerge.setValue(false);
+    } else {
+      useContentMerge.setEnabled(true);
+    }
   }
 
   private void initAgreements() {
-    final ValueChangeHandler<Boolean> onChangeSave =
-        new ValueChangeHandler<Boolean>() {
-          @Override
-          public void onValueChange(ValueChangeEvent<Boolean> event) {
-            saveProject.setEnabled(true);
-          }
-        };
-
     agreementsPanel = new VerticalPanel();
     agreementsPanel.add(new SmallHeading(Util.C.headingAgreements()));
 
     useContributorAgreements = new CheckBox(Util.C.useContributorAgreements());
-    useContributorAgreements.addValueChangeHandler(onChangeSave);
+    saveEnabler.listenTo(useContributorAgreements);
     agreementsPanel.add(useContributorAgreements);
 
     useSignedOffBy = new CheckBox(Util.C.useSignedOffBy(), true);
-    useSignedOffBy.addValueChangeHandler(onChangeSave);
+    saveEnabler.listenTo(useSignedOffBy);
     agreementsPanel.add(useSignedOffBy);
 
     add(agreementsPanel);
   }
 
   private void setSubmitType(final Project.SubmitType newSubmitType) {
+    int index = -1;
     if (submitType != null) {
       for (int i = 0; i < submitType.getItemCount(); i++) {
         if (newSubmitType.name().equals(submitType.getValue(i))) {
-          submitType.setSelectedIndex(i);
-          return;
+          index = i;
+          break;
         }
       }
-      submitType.setSelectedIndex(-1);
+      submitType.setSelectedIndex(index);
+      setEnabledForUseContentMerge();
     }
   }
 
@@ -168,7 +191,7 @@
 
     final boolean isall =
         Gerrit.getConfig().getWildProject().equals(project.getNameKey());
-    submitTypePanel.setVisible(!isall);
+    projectOptionsPanel.setVisible(!isall);
     agreementsPanel.setVisible(!isall);
     useContributorAgreements.setVisible(Gerrit.getConfig()
         .isUseContributorAgreements());
@@ -176,20 +199,25 @@
     descTxt.setText(project.getDescription());
     useContributorAgreements.setValue(project.isUseContributorAgreements());
     useSignedOffBy.setValue(project.isUseSignedOffBy());
+    useContentMerge.setValue(project.isUseContentMerge());
+    requireChangeID.setValue(project.isRequireChangeID());
     setSubmitType(project.getSubmitType());
+
+    saveProject.setEnabled(false);
   }
 
   private void doSave() {
     project.setDescription(descTxt.getText().trim());
     project.setUseContributorAgreements(useContributorAgreements.getValue());
     project.setUseSignedOffBy(useSignedOffBy.getValue());
+    project.setUseContentMerge(useContentMerge.getValue());
+    project.setRequireChangeID(requireChangeID.getValue());
     if (submitType.getSelectedIndex() >= 0) {
       project.setSubmitType(Project.SubmitType.valueOf(submitType
           .getValue(submitType.getSelectedIndex())));
     }
 
     enableForm(false, false, false);
-    saveProject.setEnabled(false);
 
     Util.PROJECT_SVC.changeProjectSettings(project,
         new GerritCallback<ProjectDetail>() {
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 e676718..758f4de 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
@@ -18,23 +18,18 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.client.ui.Hyperlink;
-import com.google.gerrit.client.ui.NavigationTable;
+import com.google.gerrit.client.ui.ProjectsTable;
 import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.client.ui.SmallHeading;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.reviewdb.Project;
-import com.google.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.VerticalPanel;
-import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
-import com.google.gwt.user.client.ui.HTMLTable.Cell;
 
 import java.util.List;
 
 public class ProjectListScreen extends Screen {
-  private ProjectTable projects;
+  private ProjectsTable projects;
 
   @Override
   protected void onLoad() {
@@ -53,7 +48,26 @@
     super.onInitUI();
     setPageTitle(Util.C.projectListTitle());
 
-    projects = new ProjectTable();
+    projects = new ProjectsTable() {
+      @Override
+      protected void onOpenRow(final int row) {
+        History.newItem(link(getRowItem(row)));
+      }
+
+      private String link(final Project item) {
+        return Dispatcher.toProjectAdmin(item.getNameKey(), ProjectScreen.INFO);
+      }
+
+      @Override
+      protected void populate(final int row, final Project k) {
+        table.setWidget(row, 1, new Hyperlink(k.getName(), link(k)));
+        table.setText(row, 2, k.getDescription());
+
+        setRowItem(row, k);
+      }
+    };
+    projects.setSavePointerId(PageLinks.ADMIN_PROJECTS);
+
     add(projects);
 
     final VerticalPanel fp = new VerticalPanel();
@@ -66,70 +80,4 @@
     super.registerKeys();
     projects.setRegisterKeys(true);
   }
-
-  private class ProjectTable extends NavigationTable<Project> {
-    ProjectTable() {
-      setSavePointerId(PageLinks.ADMIN_PROJECTS);
-      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()));
-
-      table.setText(0, 1, Util.C.columnProjectName());
-      table.setText(0, 2, Util.C.columnProjectDescription());
-      table.addClickHandler(new ClickHandler() {
-        @Override
-        public void onClick(final ClickEvent event) {
-          final Cell cell = table.getCellForEvent(event);
-          if (cell != null && cell.getCellIndex() != 1
-              && getRowItem(cell.getRowIndex()) != null) {
-            movePointerTo(cell.getRowIndex());
-          }
-        }
-      });
-
-      final FlexCellFormatter fmt = table.getFlexCellFormatter();
-      fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().dataHeader());
-      fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader());
-    }
-
-    @Override
-    protected Object getRowItemKey(final Project item) {
-      return item.getNameKey();
-    }
-
-    @Override
-    protected void onOpenRow(final int row) {
-      History.newItem(link(getRowItem(row)));
-    }
-
-    private String link(final Project item) {
-      return Dispatcher.toProjectAdmin(item.getNameKey(), ProjectScreen.INFO);
-    }
-
-    void display(final List<Project> result) {
-      while (1 < table.getRowCount())
-        table.removeRow(table.getRowCount() - 1);
-
-      for (final Project k : result) {
-        final int row = table.getRowCount();
-        table.insertRow(row);
-        applyDataRowStyle(row);
-        populate(row, k);
-      }
-    }
-
-    void populate(final int row, final Project k) {
-      table.setWidget(row, 1, new Hyperlink(k.getName(), link(k)));
-      table.setText(row, 2, k.getDescription());
-
-      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());
-
-      setRowItem(row, k);
-    }
-  }
 }
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
index 0713391..1e475ee 100644
--- 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
@@ -23,7 +23,6 @@
 }
 
 .logo {
-  width: 98%;
   text-align: right;
 }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/userpass/UserPassSignInDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/userpass/UserPassSignInDialog.java
index 46e16bb..c4709a5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/userpass/UserPassSignInDialog.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/userpass/UserPassSignInDialog.java
@@ -149,6 +149,7 @@
     buttons.add(login);
 
     close = new Button();
+    DOM.setStyleAttribute(close.getElement(), "marginLeft", "45px");
     close.setText(Gerrit.C.signInDialogClose());
     close.addClickHandler(new ClickHandler() {
       @Override
@@ -226,7 +227,7 @@
       @Override
       public void onFailure(final Throwable caught) {
         super.onFailure(caught);
-        enable(false);
+        enable(true);
       }
     });
   }
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 f850a33..23dd635 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
@@ -188,6 +188,10 @@
                     r.append(Util.M.accountNotFound(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;
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 6c68ba9..312cc10 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
@@ -49,6 +49,7 @@
   String changeQueryPageTitle(String query);
 
   String accountNotFound(String who);
+  String accountInactive(String who);
   String changeNotVisibleTo(String who);
 
   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 a4a1c31..625a9ec 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
@@ -1,6 +1,6 @@
 accountDashboardTitle = Code Review Dashboard for {0}
 changesStartedBy = Started by {0}
-changesReviewableBy = Reviewable by {0}
+changesReviewableBy = Review Requests for {0}
 changesOpenInProject = Open Changes In {0}
 changesMergedInProject = Merged Changes In {0}
 changesAbandonedInProject = Abandoned Changes In {0}
@@ -30,6 +30,7 @@
 changeQueryPageTitle = Search for {0}
 
 accountNotFound = {0} is not a registered user.
+accountInactive = {0} is not an active user.
 changeNotVisibleTo = {0} cannot access the change.
 
 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 83870c0..4db387f 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
@@ -29,9 +29,9 @@
 import com.google.gerrit.common.data.ToggleStarRequest;
 import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.Change.Status;
 import com.google.gerrit.reviewdb.ChangeMessage;
 import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gerrit.reviewdb.Change.Status;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.dom.client.KeyPressEvent;
@@ -39,6 +39,7 @@
 import com.google.gwt.i18n.client.LocaleInfo;
 import com.google.gwt.user.client.ui.DisclosurePanel;
 import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HorizontalPanel;
 import com.google.gwt.user.client.ui.Image;
 import com.google.gwt.user.client.ui.Label;
 import com.google.gwt.user.client.ui.Panel;
@@ -115,7 +116,6 @@
     super.registerKeys();
     regNavigation = GlobalKey.add(this, keysNavigation);
     regAction = GlobalKey.add(this, keysAction);
-    patchSetsBlock.setRegisterKeys(true);
   }
 
   public void refresh() {
@@ -125,6 +125,11 @@
           protected void preDisplay(final ChangeDetail r) {
             display(r);
           }
+
+          @Override
+          protected void postDisplay() {
+            patchSetsBlock.setRegisterKeys(true);
+          }
         });
   }
 
@@ -178,7 +183,7 @@
 
     dependencies = new ChangeTable() {
       {
-        table.setWidth("98%");
+        table.setWidth("auto");
       }
     };
     dependsOn = new ChangeTable.Section(Util.C.changeScreenDependsOn());
@@ -188,7 +193,6 @@
 
     dependenciesPanel = new DisclosurePanel(Util.C.changeScreenDependencies());
     dependenciesPanel.setContent(dependencies);
-    dependenciesPanel.setWidth("95%");
     add(dependenciesPanel);
 
     patchSetsBlock = new PatchSetsBlock(this);
@@ -217,7 +221,12 @@
     setPageTitle(titleBuf.toString());
   }
 
-  void display(final ChangeDetail detail) {
+  void update(final ChangeDetail detail) {
+    display(detail);
+    patchSetsBlock.setRegisterKeys(true);
+  }
+
+  private void display(final ChangeDetail detail) {
     displayTitle(detail.getChange().getKey(), detail.getChange().getSubject());
 
     if (starChange != null) {
@@ -263,15 +272,17 @@
   private void addComments(final ChangeDetail detail) {
     comments.clear();
 
-    final Label hdr = new Label(Util.C.changeScreenComments());
-    hdr.setStyleName(Gerrit.RESOURCES.css().blockHeader());
-    comments.add(hdr);
-
     final AccountInfoCache accts = detail.getAccounts();
     final List<ChangeMessage> msgList = detail.getMessages();
+
+    HorizontalPanel title = new HorizontalPanel();
+    title.setWidth("100%");
+    title.add(new Label(Util.C.changeScreenComments()));
     if (msgList.size() > 1) {
-      comments.add(messagesMenuBar());
+      title.add(messagesMenuBar());
     }
+    title.setStyleName(Gerrit.RESOURCES.css().blockHeader());
+    comments.add(title);
 
     final long AGE = 7 * 24 * 60 * 60 * 1000L;
     final Timestamp aged = new Timestamp(System.currentTimeMillis() - AGE);
@@ -307,24 +318,22 @@
       comments.add(cp);
     }
 
-    if (msgList.size() > 1) {
-      comments.add(messagesMenuBar());
-    }
     comments.setVisible(msgList.size() > 0);
   }
 
   private LinkMenuBar messagesMenuBar() {
     final Panel c = comments;
-    final LinkMenuBar m = new LinkMenuBar();
-    m.addItem(Util.C.messageExpandRecent(), new ExpandAllCommand(c, true) {
+    final LinkMenuBar menuBar = new LinkMenuBar();
+    menuBar.addItem(Util.C.messageExpandRecent(), new ExpandAllCommand(c, true) {
       @Override
       protected void expand(final CommentPanel w) {
         w.setOpen(w.isRecent());
       }
     });
-    m.addItem(Util.C.messageExpandAll(), new ExpandAllCommand(c, true));
-    m.addItem(Util.C.messageCollapseAll(), new ExpandAllCommand(c, false));
-    return m;
+    menuBar.addItem(Util.C.messageExpandAll(), new ExpandAllCommand(c, true));
+    menuBar.addItem(Util.C.messageCollapseAll(), new ExpandAllCommand(c, false));
+    menuBar.addStyleName(Gerrit.RESOURCES.css().commentPanelMenuBar());
+    return menuBar;
   }
 
   private void toggleStar() {
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 6a57feb..248e57d 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
@@ -25,8 +25,9 @@
 import com.google.gerrit.common.data.PatchSetDetail;
 import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.reviewdb.AccountGeneralPreferences;
+import com.google.gerrit.reviewdb.AccountGeneralPreferences.DownloadCommand;
+import com.google.gerrit.reviewdb.AccountGeneralPreferences.DownloadScheme;
 import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.Branch;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.ChangeMessage;
 import com.google.gerrit.reviewdb.Patch;
@@ -34,8 +35,6 @@
 import com.google.gerrit.reviewdb.PatchSetInfo;
 import com.google.gerrit.reviewdb.Project;
 import com.google.gerrit.reviewdb.UserIdentity;
-import com.google.gerrit.reviewdb.AccountGeneralPreferences.DownloadCommand;
-import com.google.gerrit.reviewdb.AccountGeneralPreferences.DownloadScheme;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
@@ -48,9 +47,9 @@
 import com.google.gwt.user.client.ui.DisclosurePanel;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Grid;
+import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
 import com.google.gwt.user.client.ui.InlineLabel;
 import com.google.gwt.user.client.ui.Panel;
-import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
 import com.google.gwtexpui.clippy.client.CopyableLabel;
 
 import java.util.Collections;
@@ -167,7 +166,6 @@
   }
 
   private void displayDownload() {
-    final Branch.NameKey branchKey = changeDetail.getChange().getDest();
     final Project.NameKey projectKey = changeDetail.getChange().getProject();
     final String projectName = projectKey.get();
     final CopyableLabel copyLabel = new CopyableLabel("");
@@ -405,7 +403,7 @@
           new AbandonChangeDialog(patchSet.getId(),
               new AsyncCallback<ChangeDetail>() {
                 public void onSuccess(ChangeDetail result) {
-                  changeScreen.display(result);
+                  changeScreen.update(result);
                 }
 
                 public void onFailure(Throwable caught) {
@@ -425,7 +423,7 @@
           new RestoreChangeDialog(patchSet.getId(),
               new AsyncCallback<ChangeDetail>() {
                 public void onSuccess(ChangeDetail result) {
-                  changeScreen.display(result);
+                  changeScreen.update(result);
                 }
 
                 public void onFailure(Throwable caught) {
@@ -516,7 +514,7 @@
         new SubmitFailureDialog(result, msg).center();
       }
     }
-    changeScreen.display(result);
+    changeScreen.update(result);
   }
 
   public PatchSet getPatchSet() {
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 b0f06a4..edfeb23 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
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.common.data.ChangeDetail;
+import com.google.gerrit.reviewdb.AccountGeneralPreferences;
 import com.google.gerrit.reviewdb.PatchSet;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
@@ -30,8 +31,10 @@
 import com.google.gwtexpui.globalkey.client.KeyCommand;
 import com.google.gwtexpui.globalkey.client.KeyCommandSet;
 
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 /**
  * Composite that displays the patch sets of a change. This composite ensures
@@ -39,7 +42,7 @@
  */
 public class PatchSetsBlock extends Composite {
 
-  private final HashMap<PatchSet.Id, PatchSetComplexDisclosurePanel> patchSetPanels =
+  private final Map<PatchSet.Id, PatchSetComplexDisclosurePanel> patchSetPanels =
       new HashMap<PatchSet.Id, PatchSetComplexDisclosurePanel>();
 
   private final ChangeScreen parent;
@@ -72,6 +75,14 @@
     currentPatchSetId = currps.getId();
     patchSets = detail.getPatchSets();
 
+    if (Gerrit.isSignedIn()) {
+      final AccountGeneralPreferences p =
+          Gerrit.getUserAccount().getGeneralPreferences();
+      if (p.isDisplayPatchSetsInReverseOrder()) {
+        Collections.reverse(patchSets);
+      }
+    }
+
     for (final PatchSet ps : patchSets) {
       if (ps == currps) {
         add(new PatchSetComplexDisclosurePanel(parent, detail, detail
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 34695dd..33b37c8 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
@@ -17,10 +17,12 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.patches.PatchScreen;
 import com.google.gerrit.client.ui.InlineHyperlink;
+import com.google.gerrit.client.ui.ListenableAccountDiffPreference;
 import com.google.gerrit.client.ui.NavigationTable;
 import com.google.gerrit.client.ui.PatchLink;
 import com.google.gerrit.common.data.PatchSetDetail;
 import com.google.gerrit.reviewdb.Patch;
+import com.google.gerrit.reviewdb.Patch.ChangeType;
 import com.google.gerrit.reviewdb.Patch.Key;
 import com.google.gerrit.reviewdb.Patch.PatchType;
 import com.google.gwt.core.client.GWT;
@@ -53,16 +55,22 @@
   private MyTable myTable;
   private String savePointerId;
   private List<Patch> patchList;
+  private ListenableAccountDiffPreference listenablePrefs;
 
   private List<ClickHandler> clickHandlers;
   private boolean active;
   private boolean registerKeys;
 
-  public PatchTable() {
+  public PatchTable(ListenableAccountDiffPreference prefs) {
+    listenablePrefs = prefs;
     myBody = new FlowPanel();
     initWidget(myBody);
   }
 
+  public PatchTable() {
+    this(new ListenableAccountDiffPreference());
+  }
+
   public int indexOf(Patch.Key patch) {
     for (int i = 0; i < patchList.size(); i++) {
       if (patchList.get(i).getKey().equals(patch)) {
@@ -166,9 +174,13 @@
    * @return a link to the previous file in this patch set, or null.
    */
   public InlineHyperlink getPreviousPatchLink(int index, PatchScreen.Type patchType) {
-    if (0 < index)
-      return createLink(index - 1, patchType, SafeHtml.asis(Util.C
+    for(index--; index > -1; index--) {
+      InlineHyperlink link = createLink(index, patchType, SafeHtml.asis(Util.C
           .prevPatchLinkIcon()), null);
+      if (link != null) {
+        return link;
+      }
+    }
     return null;
   }
 
@@ -176,9 +188,13 @@
    * @return a link to the next file in this patch set, or null.
    */
   public InlineHyperlink getNextPatchLink(int index, PatchScreen.Type patchType) {
-    if (index < patchList.size() - 1)
-      return createLink(index + 1, patchType, null, SafeHtml.asis(Util.C
+    for(index++; index < patchList.size(); index++) {
+      InlineHyperlink link = createLink(index, patchType, null, SafeHtml.asis(Util.C
           .nextPatchLinkIcon()));
+      if (link != null) {
+        return link;
+      }
+    }
     return null;
   }
 
@@ -192,6 +208,14 @@
   private PatchLink createLink(int index, PatchScreen.Type patchType,
       SafeHtml before, SafeHtml after) {
     Patch patch = patchList.get(index);
+    if (( listenablePrefs.get().isSkipDeleted() &&
+          patch.getChangeType().equals(ChangeType.DELETED) )
+        ||
+        ( listenablePrefs.get().isSkipUncommented() &&
+          patch.getCommentCount() == 0 ) ) {
+      return null;
+    }
+
     Key thisKey = patch.getKey();
     PatchLink link;
     if (patchType == PatchScreen.Type.SIDE_BY_SIDE
@@ -240,6 +264,14 @@
     }
   }
 
+  public ListenableAccountDiffPreference getPreferences() {
+    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;
@@ -756,5 +788,4 @@
       return System.currentTimeMillis() - start > 200;
     }
   }
-
 }
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 b184064..c8f16d0 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
@@ -23,7 +23,7 @@
 @external .gwt-TextBox;
 @external .gwt-Hyperlink;
 @external .gwt-CheckBox;
-@external .gwt-DisclosurePanel, .header;
+@external .gwt-DisclosurePanel, .header, .content;
 @external .gwt-InlineLabel;
 @external .gwt-InlineHyperlink;
 @external .gwt-RadioButton;
@@ -69,7 +69,6 @@
   font-size: 11pt;
   padding-left: 5px;
   padding-right: 5px;
-  width: 98%;
 }
 
 .version,
@@ -199,6 +198,9 @@
   padding-left: 0.5em;
   padding-right: 0.5em;
 }
+.commentPanelMenuBar {
+  float: right;
+}
 .commentPanelMessage p {
   margin-top: 0px;
   margin-bottom: 0px;
@@ -674,6 +676,9 @@
   background: #eeeeee;
   border-bottom: 1px solid #eeeeee;
 }
+.fileLineMode {
+  font-weight: bold;
+}
 .fileLineDELETE,
 .fileLineDELETE .wdc {
   background: #ffeeee;
@@ -753,6 +758,10 @@
   margin-bottom: 10px;
 }
 
+.gwt-DisclosurePanel .content {
+  margin-left: 10px;
+}
+
 .changeScreenDescription {
   white-space: pre;
   font-family: mono-font;
@@ -764,8 +773,7 @@
 }
 
 .changeComments {
-  margin-left: 0.5em;
-  margin-right: 0.5em;
+  padding-top: 1em;
   width: 60em;
 }
 
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 050dcb8..6452e08 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
@@ -30,8 +30,12 @@
 import com.google.gerrit.reviewdb.Patch;
 import com.google.gerrit.reviewdb.PatchLineComment;
 import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gwt.event.dom.client.BlurEvent;
+import com.google.gwt.event.dom.client.BlurHandler;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.FocusEvent;
+import com.google.gwt.event.dom.client.FocusHandler;
 import com.google.gwt.event.dom.client.KeyCodes;
 import com.google.gwt.event.dom.client.KeyPressEvent;
 import com.google.gwt.event.shared.HandlerRegistration;
@@ -52,7 +56,7 @@
 import java.util.List;
 
 public abstract class AbstractPatchContentTable extends NavigationTable<Object>
-    implements CommentEditorContainer {
+    implements CommentEditorContainer, FocusHandler, BlurHandler {
   protected PatchTable fileList;
   protected AccountInfoCache accountCache = AccountInfoCache.empty();
   protected Patch.Key patchKey;
@@ -62,6 +66,8 @@
 
   private final KeyCommandSet keysComment;
   private HandlerRegistration regComment;
+  private final KeyCommandSet keysOpenByEnter;
+  private HandlerRegistration regOpenByEnter;
 
   protected AbstractPatchContentTable() {
     keysNavigation.add(new PrevKeyCommand(0, 'k', PatchUtil.C.linePrev()));
@@ -72,8 +78,8 @@
     keysNavigation.add(new NextCommentCmd(0, 'N', PatchUtil.C.commentNext()));
 
     keysAction.add(new OpenKeyCommand(0, 'o', PatchUtil.C.expandComment()));
-    keysAction.add(new OpenKeyCommand(0, KeyCodes.KEY_ENTER, PatchUtil.C
-        .expandComment()));
+    keysOpenByEnter = new KeyCommandSet(Gerrit.C.sectionNavigation());
+    keysOpenByEnter.add(new OpenKeyCommand(0, KeyCodes.KEY_ENTER, PatchUtil.C.expandComment()));
 
     if (Gerrit.isSignedIn()) {
       keysAction.add(new InsertCommentCommand(0, 'c', PatchUtil.C
@@ -86,8 +92,6 @@
       keysComment = new KeyCommandSet(PatchUtil.C.commentEditorSet());
       keysComment.add(new NoOpKeyCommand(KeyCommand.M_CTRL, 's', PatchUtil.C
           .commentSaveDraft()));
-      keysComment.add(new NoOpKeyCommand(KeyCommand.M_CTRL, 'd', PatchUtil.C
-          .commentDiscard()));
       keysComment.add(new NoOpKeyCommand(0, KeyCodes.KEY_ESCAPE, PatchUtil.C
           .commentCancelEdit()));
     } else {
@@ -149,6 +153,13 @@
       regComment.removeHandler();
       regComment = null;
     }
+
+    if (on && keysOpenByEnter != null && regOpenByEnter == null) {
+      regOpenByEnter = GlobalKey.add(this, keysOpenByEnter);
+    } else if (!on && regOpenByEnter != null) {
+      regOpenByEnter.removeHandler();
+      regOpenByEnter = null;
+    }
   }
 
   public void display(final Patch.Key k, final PatchSet.Id a,
@@ -157,9 +168,6 @@
     idSideA = a;
     idSideB = b;
 
-    final String pathName = patchKey.get();
-    int ext = pathName.lastIndexOf('.');
-
     render(s);
   }
 
@@ -408,6 +416,8 @@
     }
 
     final CommentEditorPanel ed = new CommentEditorPanel(newComment);
+    ed.addFocusHandler(this);
+    ed.addBlurHandler(this);
     boolean isCommentRow = false;
     boolean needInsert = false;
     if (row < table.getRowCount()) {
@@ -522,6 +532,8 @@
       final PatchLineComment line, final boolean isLast) {
     if (line.getStatus() == PatchLineComment.Status.DRAFT) {
       final CommentEditorPanel plc = new CommentEditorPanel(line);
+      plc.addFocusHandler(this);
+      plc.addBlurHandler(this);
       table.setWidget(row, col, plc);
       styleLastCommentCell(row, col);
 
@@ -529,6 +541,8 @@
       final AccountInfo author = accountCache.get(line.getAuthor());
       final PublishedCommentPanel panel =
           new PublishedCommentPanel(author, line);
+      panel.addFocusHandler(this);
+      panel.addBlurHandler(this);
       table.setWidget(row, col, panel);
       styleLastCommentCell(row, col);
 
@@ -544,6 +558,29 @@
     styleCommentRow(row);
   }
 
+  @Override
+  public void onFocus(FocusEvent event) {
+    // when the comment panel gets focused (actually when a button inside the
+    // comment panel gets focused) we have to unregister the key binding for
+    // ENTER that expands/collapses the comment panel, if we don't do this the
+    // focused button in the comment panel cannot be triggered by pressing ENTER
+    // since ENTER would then be already consumed by this key binding
+    if (regOpenByEnter != null) {
+      regOpenByEnter.removeHandler();
+      regOpenByEnter = null;
+    }
+  }
+
+  @Override
+  public void onBlur(BlurEvent event) {
+    // when the comment panel gets blurred (actually when a button inside the
+    // comment panel gets blurred) we have to re-register the key binding for
+    // ENTER that expands/collapses the comment panel
+    if (keysOpenByEnter != null && regOpenByEnter == null) {
+      regOpenByEnter = GlobalKey.add(this, keysOpenByEnter);
+    }
+  }
+
   private void styleCommentRow(final int row) {
     final CellFormatter fmt = table.getCellFormatter();
     final Element iconCell = fmt.getElement(row, 0);
@@ -705,11 +742,11 @@
 
       reply = new Button(PatchUtil.C.buttonReply());
       reply.addClickHandler(this);
-      getButtonPanel().add(reply);
+      addButton(reply);
 
       replyDone = new Button(PatchUtil.C.buttonReplyDone());
       replyDone.addClickHandler(this);
-      getButtonPanel().add(replyDone);
+      addButton(replyDone);
     }
 
     @Override
@@ -738,18 +775,18 @@
         final PatchLineComment newComment = newComment();
         newComment.setMessage(message);
 
-        enable(false);
+        enableButtons(false);
         PatchUtil.DETAIL_SVC.saveDraft(newComment,
             new GerritCallback<PatchLineComment>() {
               public void onSuccess(final PatchLineComment result) {
-                enable(true);
+                enableButtons(true);
                 notifyDraftDelta(1);
                 createEditor(result).setOpen(false);
               }
 
               @Override
               public void onFailure(Throwable caught) {
-                enable(true);
+                enableButtons(true);
                 super.onFailure(caught);
               }
             });
@@ -775,13 +812,5 @@
       newComment.setSide(comment.getSide());
       return newComment;
     }
-
-    private void enable(boolean on) {
-      for (Widget w : getButtonPanel()) {
-        if (w instanceof Button) {
-          ((Button) w).setEnabled(on);
-        }
-      }
-    }
   }
 }
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 73a3ec2..98f0c95 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
@@ -102,18 +102,6 @@
               event.preventDefault();
               onSave(NULL_CALLBACK);
               return;
-
-            case 'd':
-            case 'D':
-              event.preventDefault();
-              if (isNew()) {
-                onDiscard();
-              } else if (Window.confirm(PatchUtil.C.confirmDiscard())) {
-                onDiscard();
-              } else {
-                text.setFocus(true);
-              }
-              return;
           }
         }
 
@@ -125,22 +113,22 @@
     edit = new Button();
     edit.setText(PatchUtil.C.buttonEdit());
     edit.addClickHandler(this);
-    getButtonPanel().add(edit);
+    addButton(edit);
 
     save = new Button();
     save.setText(PatchUtil.C.buttonSave());
     save.addClickHandler(this);
-    getButtonPanel().add(save);
+    addButton(save);
 
     cancel = new Button();
     cancel.setText(PatchUtil.C.buttonCancel());
     cancel.addClickHandler(this);
-    getButtonPanel().add(cancel);
+    addButton(cancel);
 
     discard = new Button();
     discard.setText(PatchUtil.C.buttonDiscard());
     discard.addClickHandler(this);
-    getButtonPanel().add(discard);
+    addButton(discard);
 
     setOpen(true);
     if (isNew()) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/HistoryTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/HistoryTable.java
index f7f99e1..cb85c24 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/HistoryTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/HistoryTable.java
@@ -38,7 +38,7 @@
   HistoryTable(final PatchScreen parent) {
     setStyleName(Gerrit.RESOURCES.css().patchHistoryTable());
     screen = parent;
-    table.setWidth("98%");
+    table.setWidth("auto");
     table.addStyleName(Gerrit.RESOURCES.css().changeTable());
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/NavLinks.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/NavLinks.java
index 6aae855..b8c52d8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/NavLinks.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/NavLinks.java
@@ -30,16 +30,31 @@
 import com.google.gwtexpui.safehtml.client.SafeHtml;
 
 class NavLinks extends Composite {
+  public enum Nav {
+    PREV (0, '[', PatchUtil.C.previousFileHelp(), 0),
+    NEXT (2, ']', PatchUtil.C.nextFileHelp(), 1);
+
+    public int col;      // Table Cell column to display link in
+    public int key;      // key code shortcut to activate link
+    public String help;  // help string for '?' popup
+    public int cmd;      // index into cmds array
+
+    Nav(int c, int k, String h, int i) {
+      this.col = c;
+      this.key = k;
+      this.help = h;
+      this.cmd = i;
+    }
+  }
+
+  private final Change.Id changeId;
   private final KeyCommandSet keys;
   private final Grid table;
 
-  private InlineHyperlink prev;
-  private InlineHyperlink next;
-
-  private KeyCommand prevKey;
-  private KeyCommand nextKey;
+  private KeyCommand cmds[] = new KeyCommand[2];
 
   NavLinks(KeyCommandSet kcs, Change.Id forChange) {
+    changeId = forChange;
     keys = kcs;
     table = new Grid(1, 3);
     initWidget(table);
@@ -50,56 +65,49 @@
     fmt.setHorizontalAlignment(0, 1, HasHorizontalAlignment.ALIGN_CENTER);
     fmt.setHorizontalAlignment(0, 2, HasHorizontalAlignment.ALIGN_RIGHT);
 
-    final ChangeLink up = new ChangeLink("", forChange);
+    final ChangeLink up = new ChangeLink("", changeId);
     SafeHtml.set(up, SafeHtml.asis(Util.C.upToChangeIconLink()));
     table.setWidget(0, 1, up);
   }
 
   void display(int patchIndex, PatchScreen.Type type, PatchTable fileList) {
     if (fileList != null) {
-      prev = fileList.getPreviousPatchLink(patchIndex, type);
-      next = fileList.getNextPatchLink(patchIndex, type);
+      setupNav(Nav.PREV, fileList.getPreviousPatchLink(patchIndex, type));
+      setupNav(Nav.NEXT, fileList.getNextPatchLink(patchIndex, type));
     } else {
-      prev = null;
-      next = null;
+      setupNav(Nav.PREV, null);
+      setupNav(Nav.NEXT, null);
+    }
+  }
+
+  protected void setupNav(final Nav nav, final InlineHyperlink link) {
+
+    /* setup the cells */
+    if (link != null) {
+      table.setWidget(0, nav.col, link);
+    } else {
+      table.clearCell(0, nav.col);
     }
 
-    if (prev != null) {
-      if (keys != null && prevKey == null) {
-        prevKey = new KeyCommand(0, '[', PatchUtil.C.previousFileHelp()) {
-          @Override
-          public void onKeyPress(KeyPressEvent event) {
-            prev.go();
-          }
-        };
-        keys.add(prevKey);
-      }
-      table.setWidget(0, 0, prev);
-    } else {
-      if (keys != null && prevKey != null) {
-        keys.remove(prevKey);
-        prevKey = null;
-      }
-      table.clearCell(0, 0);
-    }
+    /* setup the keys */
+    if (keys != null) {
 
-    if (next != null) {
-      if (keys != null && nextKey == null) {
-        nextKey = new KeyCommand(0, ']', PatchUtil.C.nextFileHelp()) {
+      if (cmds[nav.cmd] != null) {
+        keys.remove(cmds[nav.cmd]);
+      }
+
+      if (link != null) {
+        cmds[nav.cmd] = new KeyCommand(0, nav.key, nav.help) {
           @Override
           public void onKeyPress(KeyPressEvent event) {
-            next.go();
+            link.go();
           }
         };
-        keys.add(nextKey);
+      } else {
+        cmds[nav.cmd] = new UpToChangeCommand(changeId, 0, nav.key);
       }
-      table.setWidget(0, 2, next);
-    } else {
-      if (keys != null && nextKey != null) {
-        keys.remove(nextKey);
-        nextKey = null;
-      }
-      table.clearCell(0, 2);
+
+      keys.add(cmds[nav.cmd]);
     }
   }
 }
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 3b7c445..93e8deb 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
@@ -24,7 +24,6 @@
   String buttonSave();
   String buttonCancel();
   String buttonDiscard();
-  String confirmDiscard();
 
   String noDifference();
   String patchHeaderOld();
@@ -46,7 +45,6 @@
   String commentEditorSet();
   String commentInsert();
   String commentSaveDraft();
-  String commentDiscard();
   String commentCancelEdit();
 
   String whitespaceIGNORE_NONE();
@@ -62,4 +60,7 @@
 
   String buttonReplyDone();
   String cannedReplyDone();
+
+  String fileTypeSymlink();
+  String fileTypeGitlink();
 }
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 9826aba..90def1d 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
@@ -7,7 +7,6 @@
 buttonSave = Save
 buttonCancel = Cancel
 buttonDiscard = Discard
-confirmDiscard = Discard this comment?
 
 noDifference = No Differences
 patchHeaderOld = Old Version
@@ -28,7 +27,6 @@
 commentEditorSet = Comment Editing
 commentInsert = Create a new inline comment
 commentSaveDraft = Save draft comment
-commentDiscard = Discard draft comment
 commentCancelEdit = Cancel comment edit
 
 whitespaceIGNORE_NONE=None
@@ -41,3 +39,6 @@
 
 reviewed = Reviewed
 download = (Download)
+
+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 238a201..812a74e 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
@@ -17,15 +17,13 @@
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.RpcStatus;
-import com.google.gerrit.client.changes.ChangeScreen;
 import com.google.gerrit.client.changes.CommitMessageBlock;
 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.rpc.ScreenLoadCallback;
-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.PageLinks;
 import com.google.gerrit.common.data.PatchScript;
 import com.google.gerrit.common.data.PatchSetDetail;
 import com.google.gerrit.prettify.client.ClientSideFormatter;
@@ -137,17 +135,12 @@
 
   /** The index of the file we are currently looking at among the fileList */
   private int patchIndex;
+  private ListenableAccountDiffPreference prefs;
 
   /** Keys that cause an action on this screen */
   private KeyCommandSet keysNavigation;
   private HandlerRegistration regNavigation;
 
-  /** Link to the screen for the previous file, null if not applicable */
-  private InlineHyperlink previousFileLink;
-
-  /** Link to the screen for the next file, null if not applicable */
-  private InlineHyperlink nextFileLink;
-
   /**
    * How this patch should be displayed in the patch screen.
    */
@@ -175,14 +168,17 @@
     idSideB = diffSideB != null ? diffSideB : id.getParentKey();
     this.patchIndex = patchIndex;
 
-    settingsPanel = new PatchScriptSettingsPanel();
-    settingsPanel
-        .addValueChangeHandler(new ValueChangeHandler<AccountDiffPreference>() {
+    prefs = fileList != null ? fileList.getPreferences() :
+                               new ListenableAccountDiffPreference();
+    prefs.addValueChangeHandler(
+        new ValueChangeHandler<AccountDiffPreference>() {
           @Override
           public void onValueChange(ValueChangeEvent<AccountDiffPreference> event) {
             update(event.getValue());
           }
         });
+
+    settingsPanel = new PatchScriptSettingsPanel(prefs);
     settingsPanel.getReviewedCheckBox().addValueChangeHandler(
         new ValueChangeHandler<Boolean>() {
           @Override
@@ -252,8 +248,9 @@
   protected void onInitUI() {
     super.onInitUI();
 
+    final Change.Id ck = patchKey.getParentKey().getParentKey();
     keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation());
-    keysNavigation.add(new UpToChangeCommand(0, 'u', PatchUtil.C.upToChange()));
+    keysNavigation.add(new UpToChangeCommand(ck, 0, 'u'));
     keysNavigation.add(new FileListCmd(0, 'f', PatchUtil.C.fileList()));
 
     historyTable = new HistoryTable(this);
@@ -333,11 +330,9 @@
             public void onSuccess(PatchSetDetail result) {
               patchSetDetail = result;
               if (fileList == null) {
-                fileList = new PatchTable();
+                fileList = new PatchTable(prefs);
                 fileList.display(result);
                 patchIndex = fileList.indexOf(patchKey);
-                topNav.display(patchIndex, getPatchScreenType(), fileList);
-                bottomNav.display(patchIndex, getPatchScreenType(), fileList);
               }
               refresh(true);
             }
@@ -360,6 +355,10 @@
   public void registerKeys() {
     super.registerKeys();
     contentTable.setRegisterKeys(contentTable.isVisible());
+    if (regNavigation != null) {
+      regNavigation.removeHandler();
+      regNavigation = null;
+    }
     regNavigation = GlobalKey.add(this, keysNavigation);
   }
 
@@ -391,6 +390,7 @@
   }
 
   private void onResult(final PatchScript script, final boolean isFirst) {
+
     final Change.Key cid = script.getChangeId();
     final String path = PatchTable.getDisplayFileName(patchKey);
     String fileName = path;
@@ -411,6 +411,7 @@
           new GerritCallback<PatchSetDetail>() {
             @Override
             public void onSuccess(PatchSetDetail result) {
+              commitMessageBlock.setVisible(true);
               commitMessageBlock.display(result.getInfo().getMessage());
             }
           });
@@ -450,6 +451,11 @@
     settingsPanel.setEnabled(true);
     lastScript = script;
 
+    if (fileList != null) {
+      topNav.display(patchIndex, getPatchScreenType(), fileList);
+      bottomNav.display(patchIndex, getPatchScreenType(), fileList);
+    }
+
     // Mark this file reviewed as soon we display the diff screen
     if (Gerrit.isSignedIn() && isFirst) {
       settingsPanel.getReviewedCheckBox().setValue(true);
@@ -473,18 +479,6 @@
     diffSideB = patchSetId;
   }
 
-  public class UpToChangeCommand extends KeyCommand {
-    public UpToChangeCommand(int mask, int key, String help) {
-      super(mask, key, help);
-    }
-
-    @Override
-    public void onKeyPress(final KeyPressEvent event) {
-      final Change.Id ck = patchKey.getParentKey().getParentKey();
-      Gerrit.display(PageLinks.toChange(ck), new ChangeScreen(ck));
-    }
-  }
-
   public class FileListCmd extends KeyCommand {
     public FileListCmd(int mask, int key, String help) {
       super(mask, key, help);
@@ -494,7 +488,7 @@
     public void onKeyPress(final KeyPressEvent event) {
       if (fileList == null || fileList.isAttached()) {
         final PatchSet.Id psid = patchKey.getParentKey();
-        fileList = new PatchTable();
+        fileList = new PatchTable(prefs);
         fileList.setSavePointerId("PatchTable " + psid);
         Util.DETAIL_SVC.patchSetDetail(psid,
             new GerritCallback<PatchSetDetail>() {
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 758b0f0..3550229 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
@@ -17,6 +17,7 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.account.Util;
 import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.ListenableAccountDiffPreference;
 import com.google.gerrit.client.ui.NpIntTextBox;
 import com.google.gerrit.reviewdb.AccountDiffPreference;
 import com.google.gerrit.reviewdb.AccountDiffPreference.Whitespace;
@@ -48,7 +49,7 @@
   interface MyUiBinder extends UiBinder<Widget, PatchScriptSettingsPanel> {
   }
 
-  private AccountDiffPreference value;
+  private ListenableAccountDiffPreference listenablePrefs;
   private boolean enableIntralineDifference = true;
   private boolean enableSmallFileFeatures = true;
 
@@ -80,6 +81,13 @@
   CheckBox reviewed;
 
   @UiField
+  CheckBox skipDeleted;
+
+  @UiField
+  CheckBox skipUncommented;
+
+
+  @UiField
   Button update;
 
   /**
@@ -96,7 +104,8 @@
    */
   private int setEnabledCounter;
 
-  public PatchScriptSettingsPanel() {
+  public PatchScriptSettingsPanel(ListenableAccountDiffPreference prefs) {
+    listenablePrefs = prefs;
     initWidget(uiBinder.createAndBindUi(this));
     initIgnoreWhitespace(ignoreWhitespace);
     initContext(context);
@@ -115,11 +124,7 @@
     tabWidth.addKeyPressHandler(onEnter);
     colWidth.addKeyPressHandler(onEnter);
 
-    if (Gerrit.isSignedIn() && Gerrit.getAccountDiffPreference() != null) {
-      setValue(Gerrit.getAccountDiffPreference());
-    } else {
-      setValue(AccountDiffPreference.createDefault(null));
-    }
+    display();
   }
 
   @Override
@@ -147,7 +152,7 @@
   public void setEnableSmallFileFeatures(final boolean on) {
     enableSmallFileFeatures = on;
     if (enableSmallFileFeatures) {
-      syntaxHighlighting.setValue(value.isSyntaxHighlighting());
+      syntaxHighlighting.setValue(getValue().isSyntaxHighlighting());
     } else {
       syntaxHighlighting.setValue(false);
     }
@@ -157,7 +162,7 @@
   public void setEnableIntralineDifference(final boolean on) {
     enableIntralineDifference = on;
     if (enableIntralineDifference) {
-      intralineDifference.setValue(value.isIntralineDifference());
+      intralineDifference.setValue(getValue().isIntralineDifference());
     } else {
       intralineDifference.setValue(false);
     }
@@ -178,10 +183,16 @@
   }
 
   public AccountDiffPreference getValue() {
-    return value;
+    return listenablePrefs.get();
   }
 
   public void setValue(final AccountDiffPreference dp) {
+    listenablePrefs.set(dp);
+    display();
+  }
+
+  protected void display() {
+    final AccountDiffPreference dp = getValue();
     setIgnoreWhitespace(dp.getIgnoreWhitespace());
     if (enableSmallFileFeatures) {
       syntaxHighlighting.setValue(dp.isSyntaxHighlighting());
@@ -195,8 +206,8 @@
     intralineDifference.setValue(dp.isIntralineDifference());
     whitespaceErrors.setValue(dp.isShowWhitespaceErrors());
     showTabs.setValue(dp.isShowTabs());
-
-    value = dp;
+    skipDeleted.setValue(dp.isSkipDeleted());
+    skipUncommented.setValue(dp.isSkipUncommented());
   }
 
   @UiHandler("update")
@@ -205,7 +216,7 @@
   }
 
   private void update() {
-    AccountDiffPreference dp = new AccountDiffPreference(value);
+    AccountDiffPreference dp = new AccountDiffPreference(getValue());
     dp.setIgnoreWhitespace(getIgnoreWhitespace());
     dp.setContext(getContext());
     dp.setTabSize(tabWidth.getIntValue());
@@ -214,9 +225,10 @@
     dp.setIntralineDifference(intralineDifference.getValue());
     dp.setShowWhitespaceErrors(whitespaceErrors.getValue());
     dp.setShowTabs(showTabs.getValue());
+    dp.setSkipDeleted(skipDeleted.getValue());
+    dp.setSkipUncommented(skipUncommented.getValue());
 
-    value = dp;
-    fireEvent(new ValueChangeEvent<AccountDiffPreference>(dp) {});
+    listenablePrefs.set(dp);
 
     if (Gerrit.isSignedIn()) {
       persistDiffPreferences();
@@ -225,10 +237,11 @@
 
   private void persistDiffPreferences() {
     setEnabled(false);
-    Util.ACCOUNT_SVC.changeDiffPreferences(value, new GerritCallback<VoidResult>() {
+    Util.ACCOUNT_SVC.changeDiffPreferences(getValue(),
+        new GerritCallback<VoidResult>() {
       @Override
       public void onSuccess(VoidResult result) {
-        Gerrit.setAccountDiffPreference(value);
+        Gerrit.setAccountDiffPreference(getValue());
         setEnabled(true);
       }
 
@@ -267,7 +280,7 @@
     if (0 <= sel) {
       return Whitespace.valueOf(ignoreWhitespace.getValue(sel));
     }
-    return value.getIgnoreWhitespace();
+    return getValue().getIgnoreWhitespace();
   }
 
   private void setIgnoreWhitespace(Whitespace s) {
@@ -285,7 +298,7 @@
     if (0 <= sel) {
       return Short.parseShort(context.getValue(sel));
     }
-    return (short) value.getContext();
+    return (short) getValue().getContext();
   }
 
   private void setContext(int ctx) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.ui.xml
index 7bbc8fe..f0f1b4d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.ui.xml
@@ -115,12 +115,28 @@
       </g:CheckBox>
     </td>
 
+    <td rowspan='2'>
+      <g:CheckBox
+          ui:field='skipUncommented'
+          text='Skip Uncommented Files'
+          tabIndex='9'>
+        <ui:attribute name='text'/>
+      </g:CheckBox>
+      <br/>
+      <g:CheckBox
+          ui:field='skipDeleted'
+          text='Skip Deleted Files'
+          tabIndex='10'>
+        <ui:attribute name='text'/>
+      </g:CheckBox>
+    </td>
+
     <td valign='bottom' rowspan='2'>
       <g:Button
           ui:field='update'
           text='Update'
           styleName='{style.updateButton}'
-          tabIndex='9'>
+          tabIndex='11'>
         <ui:attribute name='text'/>
       </g:Button>
     </td>
@@ -129,7 +145,7 @@
       <g:CheckBox
           ui:field='reviewed'
           text='Reviewed'
-          tabIndex='10'>
+          tabIndex='12'>
         <ui:attribute name='text'/>
       </g:CheckBox>
     </td>
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 1bc4dd5..2939e71 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
@@ -22,6 +22,7 @@
 import com.google.gerrit.client.Gerrit;
 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.prettify.common.EditList;
 import com.google.gerrit.prettify.common.SparseHtmlFile;
 import com.google.gerrit.reviewdb.Patch;
@@ -81,6 +82,14 @@
     appendHeader(script, nc);
     lines.add(null);
 
+    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 lastB = 0;
     final boolean ignoreWS = script.isIgnoreWhitespace();
     for (final EditList.Hunk hunk : script.getHunks()) {
@@ -153,6 +162,29 @@
     }
   }
 
+  private void appendModeLine(final SafeHtmlBuilder nc, final FileMode mode) {
+    nc.openTd();
+    nc.setStyleName(Gerrit.RESOURCES.css().lineNumber());
+    nc.nbsp();
+    nc.closeTd();
+
+    nc.openTd();
+    nc.addStyleName(Gerrit.RESOURCES.css().fileLine());
+    nc.addStyleName(Gerrit.RESOURCES.css().fileLineMode());
+    switch(mode){
+      case FILE:
+        nc.nbsp();
+        break;
+      case SYMLINK:
+        nc.append(PatchUtil.C.fileTypeSymlink());
+        break;
+      case GITLINK:
+        nc.append(PatchUtil.C.fileTypeGitlink());
+        break;
+    }
+    nc.closeTd();
+  }
+
   @Override
   public void display(final CommentDetail cd) {
     if (cd.isEmpty()) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UpToChangeCommand.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UpToChangeCommand.java
new file mode 100644
index 0000000..709065d
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UpToChangeCommand.java
@@ -0,0 +1,36 @@
+// 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.patches;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeScreen;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.Change;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwtexpui.globalkey.client.KeyCommand;
+
+class UpToChangeCommand extends KeyCommand {
+  private final Change.Id changeId;
+
+  UpToChangeCommand(Change.Id changeId, int mask, int key) {
+    super(mask, key, PatchUtil.C.upToChange());
+    this.changeId = changeId;
+  }
+
+  @Override
+  public void onKeyPress(final KeyPressEvent event) {
+    Gerrit.display(PageLinks.toChange(changeId), new ChangeScreen(changeId));
+  }
+}
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 fa028d7..f29742a 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
@@ -17,6 +17,7 @@
 import com.google.gerrit.client.ErrorDialog;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.NotSignedInDialog;
+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;
@@ -37,6 +38,9 @@
     } else if (isNoSuchEntity(caught)) {
       new ErrorDialog(Gerrit.C.notFoundBody()).center();
 
+    } else if (isInactiveAccount(caught)) {
+      new ErrorDialog(Gerrit.C.inactiveAccountBody()).center();
+
     } else if (isNoSuchAccount(caught)) {
       final String msg = caught.getMessage();
       final String who = msg.substring(NoSuchAccountException.MESSAGE.length());
@@ -71,6 +75,11 @@
         && caught.getMessage().equals(NoSuchEntityException.MESSAGE);
   }
 
+  protected static boolean isInactiveAccount(final Throwable caught) {
+    return caught instanceof RemoteJsonException
+        && caught.getMessage().startsWith(InactiveAccountException.MESSAGE);
+  }
+
   private static boolean isNoSuchAccount(final Throwable caught) {
     return caught instanceof RemoteJsonException
         && caught.getMessage().startsWith(NoSuchAccountException.MESSAGE);
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 441878f..bcf6438 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
@@ -30,7 +30,8 @@
   public void onRequestSuggestions(final Request req, final Callback callback) {
     RpcStatus.hide(new Runnable() {
       public void run() {
-        SuggestUtil.SVC.suggestAccount(req.getQuery(), req.getLimit(),
+        SuggestUtil.SVC.suggestAccount(req.getQuery(), Boolean.TRUE,
+            req.getLimit(),
             new GerritCallback<List<AccountInfo>>() {
               public void onSuccess(final List<AccountInfo> result) {
                 final ArrayList<AccountSuggestion> r =
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 b5e3655..b2d25e4 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
@@ -16,12 +16,10 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.admin.Util;
-import com.google.gwt.event.dom.client.BlurEvent;
-import com.google.gwt.event.dom.client.BlurHandler;
+import com.google.gerrit.client.ui.HintTextBox;
+import com.google.gerrit.client.ui.RPCSuggestOracle;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.FocusEvent;
-import com.google.gwt.event.dom.client.FocusHandler;
 import com.google.gwt.event.dom.client.KeyCodes;
 import com.google.gwt.event.dom.client.KeyPressEvent;
 import com.google.gwt.event.dom.client.KeyPressHandler;
@@ -32,42 +30,23 @@
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.SuggestBox;
 import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
-import com.google.gwtexpui.globalkey.client.NpTextBox;
 
 public class AddMemberBox extends Composite {
   private final FlowPanel addPanel;
   private final Button addMember;
-  private final NpTextBox nameTxtBox;
+  private final HintTextBox nameTxtBox;
   private final SuggestBox nameTxt;
   private boolean submitOnSelection;
 
   public AddMemberBox() {
     addPanel = new FlowPanel();
     addMember = new Button(Util.C.buttonAddGroupMember());
-    nameTxtBox = new NpTextBox();
-    nameTxt = new SuggestBox(new AccountSuggestOracle(), nameTxtBox);
+    nameTxtBox = new HintTextBox();
+    nameTxt = new SuggestBox(new RPCSuggestOracle(
+        new AccountSuggestOracle()), nameTxtBox);
 
     nameTxtBox.setVisibleLength(50);
-    nameTxtBox.setText(Util.C.defaultAccountName());
-    nameTxtBox.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
-    nameTxtBox.addFocusHandler(new FocusHandler() {
-      @Override
-      public void onFocus(final FocusEvent event) {
-        if (Util.C.defaultAccountName().equals(nameTxtBox.getText())) {
-          nameTxtBox.setText("");
-          nameTxtBox.removeStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
-        }
-      }
-    });
-    nameTxtBox.addBlurHandler(new BlurHandler() {
-      @Override
-      public void onBlur(final BlurEvent event) {
-        if ("".equals(nameTxtBox.getText())) {
-          nameTxtBox.setText(Util.C.defaultAccountName());
-          nameTxtBox.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
-        }
-      }
-    });
+    nameTxtBox.setHintText(Util.C.defaultAccountName());
     nameTxtBox.addKeyPressHandler(new KeyPressHandler() {
       @Override
       public void onKeyPress(KeyPressEvent event) {
@@ -108,10 +87,7 @@
 
   public String getText() {
     String s = nameTxtBox.getText();
-    if (s == null || s.equals(Util.C.defaultAccountName())) {
-      s = "";
-    }
-    return s;
+    return s == null ? "" : s;
   }
 
   public void setEnabled(boolean enabled) {
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 b23bdba..06c17e5 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
@@ -17,12 +17,20 @@
 import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.common.data.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;
 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.HasBlurHandlers;
 import com.google.gwt.event.dom.client.HasDoubleClickHandlers;
+import com.google.gwt.event.dom.client.HasFocusHandlers;
+import com.google.gwt.event.shared.HandlerManager;
 import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.FlexTable;
 import com.google.gwt.user.client.ui.FlowPanel;
@@ -37,8 +45,10 @@
 
 import java.util.Date;
 
-public class CommentPanel extends Composite implements HasDoubleClickHandlers {
+public class CommentPanel extends Composite implements HasDoubleClickHandlers,
+    HasFocusHandlers, FocusHandler, HasBlurHandlers, BlurHandler {
   private static final int SUMMARY_LENGTH = 75;
+  private final HandlerManager handlerManager = new HandlerManager(this);
   private final FlexTable header;
   private final InlineLabel messageSummary;
   private final FlowPanel content;
@@ -132,7 +142,39 @@
     }
   }
 
-  protected Panel getButtonPanel() {
+  /**
+   * Registers a {@link FocusHandler} for this comment panel.
+   * The comment panel is considered as being focused whenever any button in the
+   * comment panel gets focused.
+   *
+   * @param handler the focus handler to be registered
+   */
+  @Override
+  public HandlerRegistration addFocusHandler(final FocusHandler handler) {
+    return handlerManager.addHandler(FocusEvent.getType(), handler);
+  }
+
+  /**
+   * Registers a {@link BlurHandler} for this comment panel.
+   * The comment panel is considered as being blurred whenever any button in the
+   * comment panel gets blurred.
+   *
+   * @param handler the blur handler to be registered
+   */
+  @Override
+  public HandlerRegistration addBlurHandler(final BlurHandler handler) {
+    return handlerManager.addHandler(BlurEvent.getType(), handler);
+  }
+
+  protected void addButton(final Button button) {
+    // register focus and blur handler for each button, so that we can fire
+    // focus and blur events for the comment panel
+    button.addFocusHandler(this);
+    button.addBlurHandler(this);
+    getButtonPanel().add(button);
+  }
+
+  private Panel getButtonPanel() {
     if (buttons == null) {
       buttons = new FlowPanel();
       buttons.setStyleName(Gerrit.RESOURCES.css().commentPanelButtons());
@@ -141,6 +183,26 @@
     return buttons;
   }
 
+  @Override
+  public void onFocus(final FocusEvent event) {
+    // a button was focused -> fire focus event for the comment panel
+    handlerManager.fireEvent(event);
+  }
+
+  @Override
+  public void onBlur(final BlurEvent event) {
+    // a button was blurred -> fire blur event for the comment panel
+    handlerManager.fireEvent(event);
+  }
+
+  protected void enableButtons(final boolean on) {
+    for (Widget w : getButtonPanel()) {
+      if (w instanceof Button) {
+        ((Button) w).setEnabled(on);
+      }
+    }
+  }
+
   private static String summarize(final String message) {
     if (message.length() < SUMMARY_LENGTH) {
       return message;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ComplexDisclosurePanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ComplexDisclosurePanel.java
index c303e1c..c93969d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ComplexDisclosurePanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ComplexDisclosurePanel.java
@@ -38,7 +38,8 @@
     // other modification of its header. We're stuck with injecting
     // into the DOM directly.
     //
-    main = new DisclosurePanel(text, isOpen);
+    main = new DisclosurePanel(text);
+    main.setOpen(isOpen);
     final Element headerParent;
     {
       final Element table = main.getElement();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/HintTextBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/HintTextBox.java
new file mode 100644
index 0000000..6575b61
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/HintTextBox.java
@@ -0,0 +1,211 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.ui;
+
+import com.google.gerrit.client.Gerrit;
+
+import com.google.gwt.event.dom.client.BlurEvent;
+import com.google.gwt.event.dom.client.BlurHandler;
+import com.google.gwt.event.dom.client.FocusEvent;
+import com.google.gwt.event.dom.client.FocusHandler;
+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.shared.HandlerRegistration;
+import com.google.gwtexpui.globalkey.client.NpTextBox;
+import com.google.gwt.user.client.ui.SuggestBox;
+import com.google.gwt.user.client.ui.Widget;
+
+
+public class HintTextBox extends NpTextBox {
+  private HandlerRegistration hintFocusHandler;
+  private HandlerRegistration hintBlurHandler;
+  private HandlerRegistration keyDownHandler;
+
+  private String hintText;
+  private String hintStyleName = Gerrit.RESOURCES.css().inputFieldTypeHint();
+
+  private String prevText;
+
+  private boolean hintOn;
+  private boolean isFocused;
+
+
+  public String getText() {
+    if (hintOn) {
+      return "";
+    }
+    return super.getText();
+  }
+
+  public void setText(String text) {
+    focusHint();
+
+    super.setText(text);
+    prevText = text;
+
+    if (! isFocused) {
+      blurHint();
+    }
+  }
+
+  public String getHintText() {
+    return hintText;
+  }
+
+  public void setHintText(String text) {
+    if (text == null) {
+      if (hintText == null) { // was not set, still not set, no change.
+        return;
+      }
+
+      // Clearing a previously set Hint
+      hintFocusHandler.removeHandler();
+      hintFocusHandler = null;
+      hintBlurHandler.removeHandler();
+      hintBlurHandler = null;
+      keyDownHandler.removeHandler();
+      keyDownHandler = null;
+      hintText = null;
+      focusHint();
+
+      return;
+    }
+
+    // Setting Hints
+
+    if (hintText == null) { // first time (was not already set)
+      hintText = text;
+
+      hintFocusHandler = addFocusHandler(new FocusHandler() {
+          @Override
+          public void onFocus(FocusEvent event) {
+            focusHint();
+            prevText = getText();
+            isFocused = true;
+          }
+        });
+
+      hintBlurHandler = addBlurHandler(new BlurHandler() {
+          @Override
+          public void onBlur(BlurEvent event) {
+            blurHint();
+            isFocused = false;
+          }
+        });
+
+      /*
+      * There seems to be a strange bug (at least on firefox 3.5.9 ubuntu) with
+      * the textbox under the following circumstances:
+      *  1) The field is not focused with BText in it.
+      *  2) The field receives focus and a focus listener changes the text to FText
+      *  3) The ESC key is pressed and the value of the field has not changed
+      *     (ever) from FText
+      *  4) BUG: The text value gets reset to BText!
+      *
+      *  A counter to this bug seems to be to force setFocus(false) on ESC.
+      */
+
+      /* Chrome does not create a KeyPressEvent on ESC, so use KeyDownEvents */
+      keyDownHandler = addKeyDownHandler(new KeyDownHandler() {
+          @Override
+          public void onKeyDown(final KeyDownEvent event) {
+            onKey(event.getNativeKeyCode());
+          }
+        });
+
+    } else { // Changing an already set Hint
+
+      focusHint();
+      hintText = text;
+    }
+
+    if (! isFocused) {
+      blurHint();
+    }
+  }
+
+  private void onKey(int key) {
+    if (key == KeyCodes.KEY_ESCAPE) {
+      setText(prevText);
+
+      Widget p = getParent();
+      if (p instanceof SuggestBox) {
+        // Since the text was changed, ensure that the SuggestBox is
+        // aware of this change so that it will refresh properly on
+        // the next keystroke.  Without this, if the first keystroke
+        // recreates the same string as before ESC was pressed, the
+        // SuggestBox will think that the string has not changed, and
+        // it will not yet provide any Suggestions.
+        ((SuggestBox)p).showSuggestionList();
+
+        // The suggestion list lingers if we don't hide it.
+        ((SuggestBox)p).hideSuggestionList();
+      }
+
+      setFocus(false);
+    }
+  }
+
+  public void setHintStyleName(String styleName) {
+    if (hintStyleName != null && hintOn) {
+      removeStyleName(hintStyleName);
+    }
+
+    hintStyleName = styleName;
+
+    if (styleName != null && hintOn) {
+      addStyleName(styleName);
+    }
+  }
+
+  public String getHintStyleName() {
+    return hintStyleName;
+  }
+
+  protected void blurHint() {
+    if (! hintOn && getHintText() != null && "".equals(super.getText())) {
+      hintOn = true;
+      super.setText(getHintText());
+      if (getHintStyleName() != null) {
+        addStyleName(getHintStyleName());
+      }
+    }
+  }
+
+  protected void focusHint() {
+    if (hintOn) {
+      super.setText("");
+      if (getHintStyleName() != null) {
+        removeStyleName(getHintStyleName());
+      }
+      hintOn = false;
+    }
+  }
+
+  public void setFocus(boolean focus) {
+    super.setFocus(focus);
+
+    if (focus != isFocused) {
+      if (focus) {
+        focusHint();
+      } else {
+        blurHint();
+      }
+    }
+
+    isFocused = focus;
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableAccountDiffPreference.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableAccountDiffPreference.java
new file mode 100644
index 0000000..195adf5
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableAccountDiffPreference.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.ui;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.reviewdb.AccountDiffPreference;
+
+public class ListenableAccountDiffPreference
+    extends ListenableValue<AccountDiffPreference> {
+
+  public ListenableAccountDiffPreference() {
+    if (Gerrit.isSignedIn() && Gerrit.getAccountDiffPreference() != null) {
+      set(Gerrit.getAccountDiffPreference());
+    } else {
+      set(AccountDiffPreference.createDefault(null));
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableValue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableValue.java
new file mode 100644
index 0000000..6dad875
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableValue.java
@@ -0,0 +1,48 @@
+// 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.ui;
+
+import com.google.gwt.event.logical.shared.HasValueChangeHandlers;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.event.logical.shared.ValueChangeHandler;
+import com.google.gwt.event.shared.GwtEvent;
+import com.google.gwt.event.shared.HandlerManager;
+import com.google.gwt.event.shared.HandlerRegistration;
+
+
+public class ListenableValue<T> implements HasValueChangeHandlers<T> {
+
+  private HandlerManager manager = new HandlerManager(this);
+
+  private T value;
+
+  public T get() {
+    return value;
+  }
+
+  public void set(final T value) {
+    this.value = value;
+    fireEvent(new ValueChangeEvent<T>(value) {});
+  }
+
+  public void fireEvent(GwtEvent<?> event) {
+    manager.fireEvent(event);
+  }
+
+  public HandlerRegistration addValueChangeHandler(
+      ValueChangeHandler<T> handler) {
+    return manager.addHandler(ValueChangeEvent.getType(), handler);
+  }
+}
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
new file mode 100644
index 0000000..932fb5e
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/OnEditEnabler.java
@@ -0,0 +1,169 @@
+// 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.ui;
+
+import com.google.gwt.event.dom.client.ChangeEvent;
+import com.google.gwt.event.dom.client.ChangeHandler;
+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.logical.shared.ValueChangeEvent;
+import com.google.gwt.event.logical.shared.ValueChangeHandler;
+import com.google.gwt.event.shared.GwtEvent;
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.DeferredCommand;
+import com.google.gwt.user.client.ui.CheckBox;
+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 java.util.HashMap;
+import java.util.Map;
+
+
+/** Enables a FocusWidget (e.g. a Button) if an edit is detected from any
+ *  registered input widget.
+ */
+public class OnEditEnabler implements KeyPressHandler, KeyDownHandler,
+   MouseUpHandler, ChangeHandler, ValueChangeHandler {
+
+  private final FocusWidget widget;
+  private Map<TextBoxBase, String> strings = new HashMap<TextBoxBase, String>();
+
+
+  // The first parameter to the contructors must be the FocusWidget to enable,
+  // subsequent parameters are widgets to listenTo.
+
+  public OnEditEnabler(final FocusWidget w, final TextBoxBase tb) {
+    this(w);
+    listenTo(tb);
+  }
+
+  public OnEditEnabler(final FocusWidget w, final ListBox lb) {
+    this(w);
+    listenTo(lb);
+  }
+
+  public OnEditEnabler(final FocusWidget w, final CheckBox cb) {
+    this(w);
+    listenTo(cb);
+  }
+
+  public OnEditEnabler(final FocusWidget w) {
+    widget = w;
+  }
+
+
+  // Register input widgets to be listened to
+
+  public void listenTo(final TextBoxBase tb) {
+    strings.put(tb, 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
+    // occured after enabling "widget".
+    tb.addFocusHandler(new FocusHandler() {
+        @Override
+        public void onFocus(FocusEvent event) {
+          strings.put(tb, tb.getText());
+        }
+      });
+
+    // CTRL-V Pastes in Chrome seem only detectable via BrowserEvents or
+    // KeyDownEvents, the latter is better.
+    tb.addKeyDownHandler(this);
+  }
+
+  public void listenTo(final ListBox lb) {
+    lb.addChangeHandler(this);
+  }
+
+  public void listenTo(final CheckBox cb) {
+    cb.addValueChangeHandler(this);
+  }
+
+
+  // Handlers
+
+  @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);
+  }
+
+  @Override
+  public void onChange(final ChangeEvent e) {
+    on(e);
+  }
+
+  @Override
+  public void onValueChange(final ValueChangeEvent e) {
+    on(e);
+  }
+
+  private void on(final GwtEvent e) {
+    if (widget.isEnabled() ||
+        ! (e.getSource() instanceof FocusWidget) ||
+        ! ((FocusWidget) e.getSource()).isEnabled() ) {
+      return;
+    }
+
+    if (e.getSource() instanceof TextBoxBase) {
+      onTextBoxBase((TextBoxBase) e.getSource());
+    } else {
+      // For many widgets, we can assume that a change is an edit. If
+      // a widget does not work that way, it should be special cased
+      // above.
+      widget.setEnabled(true);
+    }
+  }
+
+  private void onTextBoxBase(final TextBoxBase tb) {
+    // The text appears to not get updated until the handlers complete.
+    DeferredCommand.add(new Command() {
+      @Override
+      public void execute() {
+        String orig = strings.get(tb);
+        if (orig == null) {
+          orig = "";
+        }
+        if (! orig.equals(tb.getText())) {
+          widget.setEnabled(true);
+        }
+      }
+    });
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectConstants.java
new file mode 100644
index 0000000..8b13392
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectConstants.java
@@ -0,0 +1,25 @@
+// 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.ui;
+
+import com.google.gwt.i18n.client.Constants;
+
+public interface ProjectConstants extends Constants {
+  String projectName();
+  String projectDescription();
+  String projectListOpen();
+  String projectListPrev();
+  String projectListNext();
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectConstants.properties
new file mode 100644
index 0000000..15de117
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectConstants.properties
@@ -0,0 +1,5 @@
+projectName = Project Name
+projectDescription = Project Description
+projectListOpen = Select project
+projectListPrev = Previous project
+projectListNext = Next project
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
new file mode 100644
index 0000000..96089b9
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java
@@ -0,0 +1,122 @@
+// 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.ui;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.ui.NavigationTable;
+import com.google.gerrit.reviewdb.Project;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
+
+import java.util.List;
+
+public class ProjectsTable extends NavigationTable<Project> {
+
+  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()));
+
+    table.setText(0, 1, Util.C.projectName());
+    table.setText(0, 2, Util.C.projectDescription());
+
+    final FlexCellFormatter fmt = table.getFlexCellFormatter();
+    fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().dataHeader());
+    fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader());
+  }
+
+  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 Project item) {
+    return item.getNameKey();
+  }
+
+  @Override
+  protected void onOpenRow(final int row) {
+    if (row > 0) {
+      movePointerTo(row);
+    }
+  }
+
+  public void display(final List<Project> projects) {
+    while (1 < table.getRowCount())
+      table.removeRow(table.getRowCount() - 1);
+
+    for (final Project k : projects)
+      insert(table.getRowCount(), k);
+
+    finishDisplay();
+  }
+
+  protected void insert(final int row, final Project k) {
+    table.insertRow(row);
+
+    applyDataRowStyle(row);
+
+    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);
+  }
+
+  protected void populate(final int row, final Project k) {
+    table.setText(row, 1, k.getName());
+    table.setText(row, 2, k.getDescription());
+
+    setRowItem(row, k);
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RPCSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RPCSuggestOracle.java
new file mode 100644
index 0000000..cc3d510
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RPCSuggestOracle.java
@@ -0,0 +1,59 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.ui;
+
+import com.google.gwt.user.client.ui.SuggestOracle;
+import com.google.gwt.user.client.ui.SuggestOracle.Callback;
+import com.google.gwt.user.client.ui.SuggestOracle.Request;
+import com.google.gwt.user.client.ui.SuggestOracle.Response;
+
+/** This class will proxy SuggestOracle requests to another SuggestOracle
+ *  while keeping track of the latest request.  Any repsonse that belongs
+ *  to a request which is not the latest request will be dropped to prevent
+ *  invalid deliveries.
+ */
+
+public class RPCSuggestOracle extends SuggestOracle {
+
+  private SuggestOracle oracle;
+  private SuggestOracle.Request request;
+  private SuggestOracle.Callback callback;
+  private SuggestOracle.Callback myCallback = new SuggestOracle.Callback() {
+      public void onSuggestionsReady(SuggestOracle.Request req,
+            SuggestOracle.Response response) {
+          if (request == req) {
+            callback.onSuggestionsReady(req, response);
+            request = null;
+            callback = null;
+          }
+        }
+      };
+
+
+  public RPCSuggestOracle(SuggestOracle ora) {
+    oracle = ora;
+  }
+
+  public void requestSuggestions(SuggestOracle.Request req,
+      SuggestOracle.Callback cb) {
+    request = req;
+    callback = cb;
+    oracle.requestSuggestions(req, myCallback);
+  }
+
+  public boolean isDisplayStringHTML() {
+    return oracle.isDisplayStringHTML();
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/TextSaveButtonListener.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/TextSaveButtonListener.java
deleted file mode 100644
index 63270df..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/TextSaveButtonListener.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.client.ui;
-
-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.FocusWidget;
-import com.google.gwt.user.client.ui.TextBoxBase;
-
-/** Enables an action (e.g. a Button) if the text box is modified. */
-public class TextSaveButtonListener implements KeyPressHandler {
-  private final FocusWidget descAction;
-
-  public TextSaveButtonListener(final FocusWidget action) {
-    descAction = action;
-  }
-
-  public TextSaveButtonListener(final TextBoxBase text, final FocusWidget action) {
-    this(action);
-    text.addKeyPressHandler(this);
-  }
-
-  @Override
-  public void onKeyPress(final KeyPressEvent e) {
-    if (descAction.isEnabled()) {
-      // Do nothing, its already enabled.
-    } else if (e.isControlKeyDown() || e.isAltKeyDown() || e.isMetaKeyDown()) {
-      switch (e.getCharCode()) {
-        case 'v':
-        case 'x':
-          on(e);
-          break;
-      }
-    } else {
-      switch (e.getCharCode()) {
-        case KeyCodes.KEY_UP:
-        case KeyCodes.KEY_DOWN:
-        case KeyCodes.KEY_LEFT:
-        case KeyCodes.KEY_RIGHT:
-        case KeyCodes.KEY_HOME:
-        case KeyCodes.KEY_END:
-        case KeyCodes.KEY_PAGEUP:
-        case KeyCodes.KEY_PAGEDOWN:
-        case KeyCodes.KEY_ALT:
-        case KeyCodes.KEY_CTRL:
-        case KeyCodes.KEY_SHIFT:
-          break;
-        default:
-          on(e);
-          break;
-      }
-    }
-  }
-
-  private void on(final KeyPressEvent e) {
-    descAction.setEnabled(((TextBoxBase) e.getSource()).isEnabled());
-  }
-}
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
new file mode 100644
index 0000000..ebed764
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Util.java
@@ -0,0 +1,21 @@
+// 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.ui;
+
+import com.google.gwt.core.client.GWT;
+
+public class Util {
+  public static final ProjectConstants C = GWT.create(ProjectConstants.class);
+}
diff --git a/gerrit-httpd/pom.xml b/gerrit-httpd/pom.xml
index b02b142..39b2788 100644
--- a/gerrit-httpd/pom.xml
+++ b/gerrit-httpd/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.1.4-SNAPSHOT</version>
+    <version>2.1-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-httpd</artifactId>
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ChangeQueryServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ChangeQueryServlet.java
index b79971c..97debbc 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ChangeQueryServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ChangeQueryServlet.java
@@ -70,6 +70,7 @@
 
     p.setIncludeCurrentPatchSet(get(req, "current-patch-set", false));
     p.setIncludePatchSets(get(req, "patch-sets", false));
+    p.setIncludeApprovals(get(req, "all-approvals", 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/ProjectDigestFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java
index 9d9daec..f67f12f 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
@@ -133,7 +133,7 @@
     }
 
     final AccountState who = accountCache.getByUsername(username);
-    if (who == null) {
+    if (who == null || ! who.getAccount().isActive()) {
       rsp.sendError(SC_UNAUTHORIZED);
       return false;
     }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectServlet.java
index 3f7f68d..09e2cce 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectServlet.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.ReceiveCommits;
+import com.google.gerrit.server.git.TransferConfig;
 import com.google.gerrit.server.git.VisibleRefFilter;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
@@ -40,6 +41,7 @@
 import org.eclipse.jgit.http.server.resolver.ServiceNotEnabledException;
 import org.eclipse.jgit.http.server.resolver.UploadPackFactory;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.storage.pack.PackConfig;
 import org.eclipse.jgit.transport.ReceivePack;
 import org.eclipse.jgit.transport.UploadPack;
 import org.slf4j.Logger;
@@ -175,19 +177,26 @@
 
   static class Upload implements UploadPackFactory {
     private final Provider<ReviewDb> db;
+    private final PackConfig packConfig;
 
     @Inject
-    Upload(final Provider<ReviewDb> db) {
+    Upload(final Provider<ReviewDb> db, final TransferConfig tc) {
       this.db = db;
+      this.packConfig = tc.getPackConfig();
     }
 
     @Override
     public UploadPack create(HttpServletRequest req, Repository repo)
-        throws ServiceNotEnabledException {
+        throws ServiceNotEnabledException, ServiceNotAuthorizedException {
+      ProjectControl pc = getProjectControl(req);
+      if (!pc.canRunUploadPack()) {
+        throw new ServiceNotAuthorizedException();
+      }
+
       // The Resolver above already checked READ access for us.
       //
-      ProjectControl pc = getProjectControl(req);
       UploadPack up = new UploadPack(repo);
+      up.setPackConfig(packConfig);
       if (!pc.allRefsAreVisible()) {
         up.setRefFilter(new VisibleRefFilter(repo, pc, db.get()));
       }
@@ -207,6 +216,10 @@
     public ReceivePack create(HttpServletRequest req, Repository db)
         throws ServiceNotEnabledException, ServiceNotAuthorizedException {
       final ProjectControl pc = getProjectControl(req);
+      if (!pc.canRunReceivePack()) {
+        throw new ServiceNotAuthorizedException();
+      }
+
       if (pc.getCurrentUser() instanceof IdentifiedUser) {
         final IdentifiedUser user = (IdentifiedUser) pc.getCurrentUser();
         final ReceiveCommits rc = factory.create(pc, db);
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 93b6d09..cc2e144 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
@@ -19,6 +19,7 @@
 import com.google.gerrit.common.data.GerritConfig;
 import com.google.gerrit.httpd.auth.become.BecomeAnyAccountLoginServlet;
 import com.google.gerrit.httpd.auth.container.HttpAuthModule;
+import com.google.gerrit.httpd.auth.container.HttpsClientSslCertModule;
 import com.google.gerrit.httpd.auth.ldap.LdapAuthModule;
 import com.google.gerrit.httpd.auth.openid.OpenIdModule;
 import com.google.gerrit.httpd.gitweb.GitWebModule;
@@ -101,6 +102,10 @@
         install(new HttpAuthModule());
         break;
 
+      case CLIENT_SSL_CERT_LDAP:
+        install(new HttpsClientSslCertModule());
+        break;
+
       case LDAP:
       case LDAP_BIND:
         install(new LdapAuthModule());
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
new file mode 100644
index 0000000..381daa8
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertAuthFilter.java
@@ -0,0 +1,94 @@
+// 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.auth.container;
+
+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.AuthRequest;
+import com.google.gerrit.server.account.AuthResult;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.security.cert.X509Certificate;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+
+@Singleton
+class HttpsClientSslCertAuthFilter implements Filter {
+
+  private static final Pattern REGEX_USERID = Pattern.compile("CN=([^,]*),.*");
+  private static final Logger log =
+    LoggerFactory.getLogger(HttpsClientSslCertAuthFilter.class);
+
+  private final Provider<WebSession> webSession;
+  private final AccountManager accountManager;
+
+  @Inject
+  HttpsClientSslCertAuthFilter(final Provider<WebSession> webSession,
+      final AccountManager accountManager) {
+    this.webSession = webSession;
+    this.accountManager = accountManager;
+  }
+
+  @Override
+  public void destroy() {
+  }
+
+  @Override
+  public void doFilter(ServletRequest req, ServletResponse rsp,
+      FilterChain chain) throws IOException, ServletException {
+    X509Certificate[] certs = (X509Certificate[]) req.getAttribute("javax.servlet.request.X509Certificate");
+    if (certs == null || certs.length == 0) {
+      throw new ServletException(
+          "Couldn't get the attribute javax.servlet.request.X509Certificate from the request");
+    }
+    String name = certs[0].getSubjectDN().getName();
+    Matcher m = REGEX_USERID.matcher(name);
+    String userName;
+    if (m.matches()) {
+      userName = m.group(1);
+    } else {
+      throw new ServletException("Couldn't extract username from your certificate");
+    }
+    final AuthRequest areq = AuthRequest.forUser(userName);
+    final AuthResult arsp;
+    try {
+      arsp = accountManager.authenticate(areq);
+    } catch (AccountException e) {
+      String err = "Unable to authenticate user \"" + userName + "\"";
+      log.error(err, e);
+      throw new ServletException(err, e);
+    }
+    webSession.get().login(arsp, true);
+    chain.doFilter(req, rsp);
+  }
+
+  @Override
+  public void init(FilterConfig arg0) throws ServletException {
+  }
+}
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
new file mode 100644
index 0000000..f0976f3
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertModule.java
@@ -0,0 +1,25 @@
+// 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.auth.container;
+
+import com.google.inject.servlet.ServletModule;
+
+/** Servlets and support related to CLIENT_SSL_CERT_LDAP authentication. */
+public class HttpsClientSslCertModule extends ServletModule {
+  @Override
+  protected void configureServlets() {
+    filter("/").through(HttpsClientSslCertAuthFilter.class);
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/UserPassAuthServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/UserPassAuthServiceImpl.java
index a59d013..e4577c1 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/UserPassAuthServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/UserPassAuthServiceImpl.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.account.AccountUserNameException;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.AuthResult;
 import com.google.gwt.user.client.rpc.AsyncCallback;
@@ -55,6 +56,12 @@
     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 (AccountException e) {
       result.success = false;
       callback.onSuccess(result);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java
index a704476..8991ea9 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
@@ -344,11 +344,14 @@
       rsp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
       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");
-    exec(req, rsp, project, repo);
+    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");
+      exec(req, rsp, project, repo);
+    } finally {
+      repo.close();
+    }
   }
 
   private static Map<String, String> getParameters(final HttpServletRequest req)
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 3ecfa62..832ae99 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
@@ -33,14 +33,16 @@
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.lib.ObjectReader;
 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.eclipse.jgit.util.NB;
 
-import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.io.OutputStream;
 import java.io.UnsupportedEncodingException;
 import java.net.URLDecoder;
 import java.security.MessageDigest;
@@ -162,45 +164,50 @@
       return;
     }
 
-    final byte[] blobData;
+    final ObjectLoader blobLoader;
     final RevCommit fromCommit;
     final String suffix;
     final String path = patchKey.getFileName();
     try {
-      final RevWalk rw = new RevWalk(repo);
-      final RevCommit c;
-      final TreeWalk tw;
+      final ObjectReader reader = repo.newObjectReader();
+      try {
+        final RevWalk rw = new RevWalk(reader);
+        final RevCommit c;
+        final TreeWalk tw;
 
-      c = rw.parseCommit(ObjectId.fromString(patchSet.getRevision().get()));
-      if (side == 0) {
-        fromCommit = c;
-        suffix = "new";
+        c = rw.parseCommit(ObjectId.fromString(patchSet.getRevision().get()));
+        if (side == 0) {
+          fromCommit = c;
+          suffix = "new";
 
-      } else if (1 <= side && side - 1 < c.getParentCount()) {
-        fromCommit = rw.parseCommit(c.getParent(side - 1));
-        if (c.getParentCount() == 1) {
-          suffix = "old";
+        } else if (1 <= side && side - 1 < c.getParentCount()) {
+          fromCommit = rw.parseCommit(c.getParent(side - 1));
+          if (c.getParentCount() == 1) {
+            suffix = "old";
+          } else {
+            suffix = "old" + side;
+          }
+
         } else {
-          suffix = "old" + side;
+          rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
+          return;
         }
 
-      } else {
-        rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
-        return;
-      }
+        tw = TreeWalk.forPath(reader, path, fromCommit.getTree());
+        if (tw == null) {
+          rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
+          return;
+        }
 
-      tw = TreeWalk.forPath(repo, path, fromCommit.getTree());
-      if (tw == null) {
-        rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
-        return;
-      }
+        if (tw.getFileMode(0).getObjectType() == Constants.OBJ_BLOB) {
+          blobLoader = reader.open(tw.getObjectId(0), Constants.OBJ_BLOB);
 
-      if (tw.getFileMode(0).getObjectType() == Constants.OBJ_BLOB) {
-        blobData = repo.openBlob(tw.getObjectId(0)).getCachedBytes();
-
-      } else {
-        rsp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
-        return;
+        } else {
+          rsp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+          return;
+        }
+      } finally {
+        reader.release();
       }
     } catch (IOException e) {
       getServletContext().log("Cannot read repository", e);
@@ -214,14 +221,20 @@
       repo.close();
     }
 
+    final byte[] raw =
+        blobLoader.isLarge() ? null : blobLoader.getCachedBytes();
     final long when = fromCommit.getCommitTime() * 1000L;
-    MimeType contentType = registry.getMimeType(path, blobData);
-    final byte[] outData;
 
-    if (registry.isSafeInline(contentType)) {
-      outData = blobData;
+    rsp.setDateHeader("Last-Modified", when);
+    rsp.setDateHeader("Expires", 0L);
+    rsp.setHeader("Pragma", "no-cache");
+    rsp.setHeader("Cache-Control", "no-cache, must-revalidate");
 
-    } else {
+    OutputStream out;
+    ZipOutputStream zo;
+
+    final MimeType contentType = registry.getMimeType(path, raw);
+    if (!registry.isSafeInline(contentType)) {
       // The content may not be safe to transmit inline, as a browser might
       // interpret it as HTML or JavaScript hosted by this site. Such code
       // might then run in the site's security domain, and may be able to use
@@ -230,31 +243,38 @@
       // Usually, wrapping the content into a ZIP file forces the browser to
       // save the content to the local system instead.
       //
-      final ByteArrayOutputStream zip = new ByteArrayOutputStream();
-      final ZipOutputStream zo = new ZipOutputStream(zip);
-      final ZipEntry e = new ZipEntry(safeFileName(path, rand(req, suffix)));
-      e.setComment(fromCommit.name() + ":" + path);
-      e.setSize(blobData.length);
-      e.setTime(when);
-      zo.putNextEntry(e);
-      zo.write(blobData);
-      zo.closeEntry();
-      zo.close();
 
-      outData = zip.toByteArray();
-      contentType = ZIP;
-
+      rsp.setContentType(ZIP.toString());
       rsp.setHeader("Content-Disposition", "attachment; filename=\""
           + safeFileName(path, suffix) + ".zip" + "\"");
+
+      zo = new ZipOutputStream(rsp.getOutputStream());
+
+      final ZipEntry e = new ZipEntry(safeFileName(path, rand(req, suffix)));
+      e.setComment(fromCommit.name() + ":" + path);
+      e.setSize(blobLoader.getSize());
+      e.setTime(when);
+      zo.putNextEntry(e);
+      out = zo;
+
+    } else {
+      rsp.setContentType(contentType.toString());
+      rsp.setHeader("Content-Length", "" + blobLoader.getSize());
+
+      out = rsp.getOutputStream();
+      zo = null;
     }
 
-    rsp.setContentType(contentType.toString());
-    rsp.setContentLength(outData.length);
-    rsp.setDateHeader("Last-Modified", when);
-    rsp.setDateHeader("Expires", 0L);
-    rsp.setHeader("Pragma", "no-cache");
-    rsp.setHeader("Cache-Control", "no-cache, must-revalidate");
-    rsp.getOutputStream().write(outData);
+    if (raw != null) {
+      out.write(raw);
+    } else {
+      blobLoader.copyTo(out);
+    }
+
+    if (zo != null) {
+      zo.closeEntry();
+    }
+    out.close();
   }
 
   private static String safeFileName(String fileName, final String suffix) {
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 f9721f8..864c550 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,6 +33,7 @@
 import java.util.ArrayList;
 import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 
 class SuggestServiceImpl extends BaseServiceImplementation implements
     SuggestService {
@@ -74,8 +75,8 @@
     });
   }
 
-  public void suggestAccount(final String query, final int limit,
-      final AsyncCallback<List<AccountInfo>> callback) {
+  public void suggestAccount(final String query, final Boolean active,
+      final int limit, final AsyncCallback<List<AccountInfo>> callback) {
     run(callback, new Action<List<AccountInfo>>() {
       public List<AccountInfo> run(final ReviewDb db) throws OrmException {
         final String a = query;
@@ -86,12 +87,12 @@
         final LinkedHashMap<Account.Id, AccountInfo> r =
             new LinkedHashMap<Account.Id, AccountInfo>();
         for (final Account p : db.accounts().suggestByFullName(a, b, n)) {
-          r.put(p.getId(), new AccountInfo(p));
+          addSuggestion(r, p, new AccountInfo(p), active);
         }
         if (r.size() < n) {
           for (final Account p : db.accounts().suggestByPreferredEmail(a, b,
               n - r.size())) {
-            r.put(p.getId(), new AccountInfo(p));
+            addSuggestion(r, p, new AccountInfo(p), active);
           }
         }
         if (r.size() < n) {
@@ -101,7 +102,7 @@
               final Account p = accountCache.get(e.getAccountId()).getAccount();
               final AccountInfo info = new AccountInfo(p);
               info.setPreferredEmail(e.getEmailAddress());
-              r.put(e.getAccountId(), info);
+              addSuggestion(r, p, info, active);
             }
           }
         }
@@ -110,6 +111,13 @@
     });
   }
 
+  private void addSuggestion(Map map, Account account, AccountInfo info,
+      Boolean active) {
+    if (active == null || active == account.isActive()) {
+      map.put(account.getId(), info);
+    }
+  }
+
   public void suggestAccountGroup(final String query, final int limit,
       final AsyncCallback<List<AccountGroupName>> callback) {
     run(callback, new Action<List<AccountGroupName>>() {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupAdminServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupAdminServiceImpl.java
index 4e4f82e..870d77c 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupAdminServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupAdminServiceImpl.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.common.data.GroupAdminService;
 import com.google.gerrit.common.data.GroupDetail;
+import com.google.gerrit.common.errors.InactiveAccountException;
 import com.google.gerrit.common.errors.NameAlreadyUsedException;
 import com.google.gerrit.common.errors.NoSuchAccountException;
 import com.google.gerrit.common.errors.NoSuchEntityException;
@@ -221,6 +222,9 @@
         }
 
         final Account a = findAccount(nameOrEmail);
+        if (!a.isActive()) {
+          throw new Failure(new InactiveAccountException(a.getFullName()));
+        }
         if (!control.canAdd(a.getId())) {
           throw new Failure(new NoSuchEntityException());
         }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/IncludedInDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/IncludedInDetailFactory.java
index ec0f225..7d6f764 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/IncludedInDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/IncludedInDetailFactory.java
@@ -80,22 +80,26 @@
         repoManager.openRepository(control.getProject().getName());
     try {
       final RevWalk rw = new RevWalk(repo);
-      rw.setRetainBody(false);
-
-      final RevCommit rev;
       try {
-        rev = rw.parseCommit(ObjectId.fromString(patch.getRevision().get()));
-      } catch (IncorrectObjectTypeException err) {
-        throw new InvalidRevisionException();
-      } catch (MissingObjectException err) {
-        throw new InvalidRevisionException();
+        rw.setRetainBody(false);
+
+        final RevCommit rev;
+        try {
+          rev = rw.parseCommit(ObjectId.fromString(patch.getRevision().get()));
+        } catch (IncorrectObjectTypeException err) {
+          throw new InvalidRevisionException();
+        } catch (MissingObjectException err) {
+          throw new InvalidRevisionException();
+        }
+
+        detail = new IncludedInDetail();
+        detail.setBranches(includedIn(repo, rw, rev, Constants.R_HEADS));
+        detail.setTags(includedIn(repo, rw, rev, Constants.R_TAGS));
+
+        return detail;
+      } finally {
+        rw.release();
       }
-
-      detail = new IncludedInDetail();
-      detail.setBranches(includedIn(repo, rw, rev, Constants.R_HEADS));
-      detail.setTags(includedIn(repo, rw, rev, Constants.R_TAGS));
-
-      return detail;
     } finally {
       repo.close();
     }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/AddReviewer.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/AddReviewer.java
index ae36388..9d508e2 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/AddReviewer.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/AddReviewer.java
@@ -94,6 +94,11 @@
             ReviewerResult.Error.Type.ACCOUNT_NOT_FOUND, nameOrEmail));
         continue;
       }
+      if (!account.isActive()) {
+        result.addError(new ReviewerResult.Error(
+            ReviewerResult.Error.Type.ACCOUNT_INACTIVE, nameOrEmail));
+        continue;
+      }
 
       final IdentifiedUser user = identifiedUserFactory.create(account.getId());
       if (!control.forUser(user).isVisible()) {
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 0f557ee..730d269 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
@@ -24,7 +24,6 @@
 import com.google.gerrit.reviewdb.Patch;
 import com.google.gerrit.reviewdb.PatchLineComment;
 import com.google.gerrit.reviewdb.AccountDiffPreference.Whitespace;
-import com.google.gerrit.reviewdb.Patch.PatchType;
 import com.google.gerrit.server.FileTypeRegistry;
 import com.google.gerrit.server.patch.PatchListEntry;
 import com.google.gerrit.server.patch.Text;
@@ -40,7 +39,7 @@
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevTree;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -64,6 +63,7 @@
   };
 
   private Repository db;
+  private ObjectReader reader;
   private Change change;
   private AccountDiffPreference diffPrefs;
   private boolean againstParent;
@@ -112,16 +112,17 @@
   PatchScript toPatchScript(final PatchListEntry content,
       final boolean intralineDifference, final CommentDetail comments,
       final List<Patch> history) throws IOException {
-    if (content.getPatchType() == PatchType.N_WAY) {
-      // For a diff --cc format we don't support converting it into
-      // a patch script. Instead treat everything as a file header.
-      //
-      return new PatchScript(change.getKey(), content.getChangeType(), content
-          .getOldName(), content.getNewName(), content.getHeaderLines(),
-          diffPrefs, a.dst, b.dst, Collections.<Edit> emptyList(),
-          a.displayMethod, b.displayMethod, comments, history, false, false);
+    reader = db.newObjectReader();
+    try {
+      return build(content, intralineDifference, comments, history);
+    } finally {
+      reader.release();
     }
+  }
 
+  private PatchScript build(final PatchListEntry content,
+      final boolean intralineDifference, final CommentDetail comments,
+      final List<Patch> history) throws IOException {
     a.path = oldName(content);
     b.path = newName(content);
 
@@ -167,9 +168,9 @@
     }
 
     return new PatchScript(change.getKey(), content.getChangeType(), content
-        .getOldName(), content.getNewName(), content.getHeaderLines(),
-        diffPrefs, a.dst, b.dst, edits, a.displayMethod, b.displayMethod,
-        comments, history, hugeFile, intralineDifference);
+        .getOldName(), content.getNewName(), a.fileMode, b.fileMode, content
+        .getHeaderLines(), diffPrefs, a.dst, b.dst, edits, a.displayMethod,
+        b.displayMethod, comments, history, hugeFile, intralineDifference);
   }
 
   private static String oldName(final PatchListEntry entry) {
@@ -318,14 +319,14 @@
     for (final EditList.Hunk hunk : list.getHunks()) {
       while (hunk.next()) {
         if (hunk.isContextLine()) {
-          final String lineA = a.src.getLine(hunk.getCurA());
+          final String lineA = a.src.getString(hunk.getCurA());
           a.dst.addLine(hunk.getCurA(), lineA);
 
           if (ignoredWhitespace) {
             // If we ignored whitespace in some form, also get the line
             // from b when it does not exactly match the line from a.
             //
-            final String lineB = b.src.getLine(hunk.getCurB());
+            final String lineB = b.src.getString(hunk.getCurB());
             if (!lineA.equals(lineB)) {
               b.dst.addLine(hunk.getCurB(), lineB);
             }
@@ -355,6 +356,7 @@
     Text src;
     MimeType mimeType = MimeUtil2.UNKNOWN_MIME_TYPE;
     DisplayMethod displayMethod = DisplayMethod.DIFF;
+    PatchScript.FileMode fileMode = PatchScript.FileMode.FILE;
     final SparseFileContent dst = new SparseFileContent();
 
     int size() {
@@ -362,7 +364,7 @@
     }
 
     void addLine(int line) {
-      dst.addLine(line, src.getLine(line));
+      dst.addLine(line, src.getString(line));
     }
 
     void resolve(final Side other, final ObjectId within) throws IOException {
@@ -377,7 +379,7 @@
             displayMethod = DisplayMethod.NONE;
           } else {
             id = within;
-            src = Text.forCommit(db, within);
+            src = Text.forCommit(db, reader, within);
             srcContent = src.getContent();
             if (src == Text.EMPTY) {
               mode = FileMode.MISSING;
@@ -399,14 +401,7 @@
             srcContent = other.srcContent;
 
           } else if (mode.getObjectType() == Constants.OBJ_BLOB) {
-            final ObjectLoader ldr = db.openObject(id);
-            if (ldr == null) {
-              throw new MissingObjectException(id, Constants.TYPE_BLOB);
-            }
-            srcContent = ldr.getCachedBytes();
-            if (ldr.getType() != Constants.OBJ_BLOB) {
-              throw new IncorrectObjectTypeException(id, Constants.TYPE_BLOB);
-            }
+            srcContent = Text.asByteArray(db.open(id, Constants.OBJ_BLOB));
 
           } else {
             srcContent = Text.NO_BYTES;
@@ -443,6 +438,12 @@
         }
         dst.setSize(size());
         dst.setPath(path);
+
+        if (mode == FileMode.SYMLINK) {
+          fileMode = PatchScript.FileMode.SYMLINK;
+        } else if (mode == FileMode.GITLINK) {
+          fileMode = PatchScript.FileMode.GITLINK;
+        }
       } catch (IOException err) {
         throw new IOException("Cannot read " + within.name() + ":" + path, err);
       }
@@ -450,12 +451,12 @@
 
     private TreeWalk find(final ObjectId within) throws MissingObjectException,
         IncorrectObjectTypeException, CorruptObjectException, IOException {
-      if (path == null) {
+      if (path == null || within == null) {
         return null;
       }
-      final RevWalk rw = new RevWalk(db);
+      final RevWalk rw = new RevWalk(reader);
       final RevTree tree = rw.parseTree(within);
-      return TreeWalk.forPath(db, path, tree);
+      return TreeWalk.forPath(reader, path, tree);
     }
   }
 }
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 0472dd4..d4698c2 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddBranch.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddBranch.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.httpd.rpc.project;
 
+import com.google.gerrit.common.ChangeHookRunner;
 import com.google.gerrit.common.data.ListBranchesResult;
 import com.google.gerrit.common.errors.InvalidNameException;
 import com.google.gerrit.common.errors.InvalidRevisionException;
@@ -58,6 +59,7 @@
   private final IdentifiedUser identifiedUser;
   private final GitRepositoryManager repoManager;
   private final ReplicationQueue replication;
+  private final ChangeHookRunner hooks;
 
   private final Project.NameKey projectName;
   private final String branchName;
@@ -69,6 +71,7 @@
       final IdentifiedUser identifiedUser,
       final GitRepositoryManager repoManager,
       final ReplicationQueue replication,
+      final ChangeHookRunner hooks,
 
       @Assisted Project.NameKey projectName,
       @Assisted("branchName") String branchName,
@@ -78,6 +81,7 @@
     this.identifiedUser = identifiedUser;
     this.repoManager = repoManager;
     this.replication = replication;
+    this.hooks = hooks;
 
     this.projectName = projectName;
     this.branchName = branchName;
@@ -136,11 +140,9 @@
           case NEW:
           case NO_CHANGE:
             replication.scheduleUpdate(name.getParentKey(), refname);
+            hooks.doRefUpdatedHook(name, u, identifiedUser.getAccount());
             break;
           default: {
-            final String msg =
-                "Cannot create branch " + name + ": " + result.name();
-            log.error(msg);
             throw new IOException(result.name());
           }
         }
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 e8c8904..fffc126 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteBranches.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteBranches.java
@@ -14,9 +14,11 @@
 
 package com.google.gerrit.httpd.rpc.project;
 
+import com.google.gerrit.common.ChangeHookRunner;
 import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.Branch;
 import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.ReplicationQueue;
 import com.google.gerrit.server.project.NoSuchProjectException;
@@ -46,6 +48,8 @@
   private final ProjectControl.Factory projectControlFactory;
   private final GitRepositoryManager repoManager;
   private final ReplicationQueue replication;
+  private final IdentifiedUser identifiedUser;
+  private final ChangeHookRunner hooks;
 
   private final Project.NameKey projectName;
   private final Set<Branch.NameKey> toRemove;
@@ -54,11 +58,15 @@
   DeleteBranches(final ProjectControl.Factory projectControlFactory,
       final GitRepositoryManager repoManager,
       final ReplicationQueue replication,
+      final IdentifiedUser identifiedUser,
+      final ChangeHookRunner hooks,
 
       @Assisted Project.NameKey name, @Assisted Set<Branch.NameKey> toRemove) {
     this.projectControlFactory = projectControlFactory;
     this.repoManager = repoManager;
     this.replication = replication;
+    this.identifiedUser = identifiedUser;
+    this.hooks = hooks;
 
     this.projectName = name;
     this.toRemove = toRemove;
@@ -85,8 +93,9 @@
       for (final Branch.NameKey branchKey : toRemove) {
         final String refname = branchKey.get();
         final RefUpdate.Result result;
+        final RefUpdate u;
         try {
-          final RefUpdate u = r.updateRef(refname);
+          u = r.updateRef(refname);
           u.setForceUpdate(true);
           result = u.delete();
         } catch (IOException e) {
@@ -101,6 +110,7 @@
           case FORCED:
             deleted.add(branchKey);
             replication.scheduleUpdate(projectName, refname);
+            hooks.doRefUpdatedHook(branchKey, u, identifiedUser.getAccount());
             break;
 
           case REJECTED_CURRENT_BRANCH:
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteRefRights.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteRefRights.java
index 92154c4..ae8b98b 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteRefRights.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteRefRights.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.server.project.NoSuchRefException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.server.project.RefControl;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -71,8 +70,12 @@
       if (!projectName.equals(k.getProjectNameKey())) {
         throw new IllegalArgumentException("All keys must be from same project");
       }
-      if (!projectControl.controlForRef(k.getRefPattern()).isOwner()) {
-        throw new NoSuchRefException(k.getRefPattern());
+      String refPattern = k.getRefPattern();
+      if (refPattern.startsWith("-")) {
+        refPattern = refPattern.substring(1);
+      }
+      if (!projectControl.controlForRef(refPattern).isOwner()) {
+        throw new NoSuchRefException(refPattern);
       }
     }
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListBranches.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListBranches.java
index c02af37..7b22de0 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListBranches.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListBranches.java
@@ -60,15 +60,20 @@
   }
 
   @Override
-  public ListBranchesResult call() throws NoSuchProjectException,
-      RepositoryNotFoundException {
+  public ListBranchesResult call() throws NoSuchProjectException {
     final ProjectControl pctl = projectControlFactory.validateFor( //
         projectName, //
         ProjectControl.OWNER | ProjectControl.VISIBLE);
 
     final List<Branch> branches = new ArrayList<Branch>();
     Branch headBranch = null;
-    final Repository db = repoManager.openRepository(projectName.get());
+
+    final Repository db;
+    try {
+      db = repoManager.openRepository(projectName.get());
+    } catch (RepositoryNotFoundException noGitRepository) {
+      return new ListBranchesResult(branches, false, true);
+    }
     try {
       final Map<String, Ref> all = db.getAllRefs();
 
@@ -139,7 +144,7 @@
     if (headBranch != null) {
       branches.add(0, headBranch);
     }
-    return new ListBranchesResult(branches, pctl.canAddRefs());
+    return new ListBranchesResult(branches, pctl.canAddRefs(), false);
   }
 
   private Branch createBranch(final String name) {
diff --git a/gerrit-launcher/pom.xml b/gerrit-launcher/pom.xml
index 1000777..3c9c979 100644
--- a/gerrit-launcher/pom.xml
+++ b/gerrit-launcher/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.1.4-SNAPSHOT</version>
+    <version>2.1-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-launcher</artifactId>
diff --git a/gerrit-main/pom.xml b/gerrit-main/pom.xml
index 10cb14a..a46aa4f 100644
--- a/gerrit-main/pom.xml
+++ b/gerrit-main/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.1.4-SNAPSHOT</version>
+    <version>2.1-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-main</artifactId>
diff --git a/gerrit-patch-commonsnet/pom.xml b/gerrit-patch-commonsnet/pom.xml
index df64d13..5bbae03 100644
--- a/gerrit-patch-commonsnet/pom.xml
+++ b/gerrit-patch-commonsnet/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.1.4-SNAPSHOT</version>
+    <version>2.1-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-patch-commonsnet</artifactId>
diff --git a/gerrit-patch-jgit/pom.xml b/gerrit-patch-jgit/pom.xml
index 68a08c3..f30eace 100644
--- a/gerrit-patch-jgit/pom.xml
+++ b/gerrit-patch-jgit/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.1.4-SNAPSHOT</version>
+    <version>2.1-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-patch-jgit</artifactId>
diff --git a/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/lib/WindowCacheStatAccessor.java b/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/storage/file/WindowCacheStatAccessor.java
similarity index 95%
rename from gerrit-patch-jgit/src/main/java/org/eclipse/jgit/lib/WindowCacheStatAccessor.java
rename to gerrit-patch-jgit/src/main/java/org/eclipse/jgit/storage/file/WindowCacheStatAccessor.java
index 7d12e47..7e29536 100644
--- a/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/lib/WindowCacheStatAccessor.java
+++ b/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/storage/file/WindowCacheStatAccessor.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package org.eclipse.jgit.lib;
+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.
diff --git a/gerrit-pgm/pom.xml b/gerrit-pgm/pom.xml
index 66c5349..f658f52 100644
--- a/gerrit-pgm/pom.xml
+++ b/gerrit-pgm/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.1.4-SNAPSHOT</version>
+    <version>2.1-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-pgm</artifactId>
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 445c10a..b10e1db 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
@@ -52,7 +52,6 @@
 
 import org.kohsuke.args4j.Option;
 
-import java.io.Console;
 import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ScanTrackingIds.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ScanTrackingIds.java
index e62fe40..16c72d4 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ScanTrackingIds.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ScanTrackingIds.java
@@ -140,8 +140,12 @@
 
   private RevCommit parse(final Repository git, PatchSet ps)
       throws MissingObjectException, IncorrectObjectTypeException, IOException {
-    return new RevWalk(git).parseCommit(ObjectId.fromString(ps.getRevision()
-        .get()));
+    RevWalk rw = new RevWalk(git);
+    try {
+      return rw.parseCommit(ObjectId.fromString(ps.getRevision().get()));
+    } finally {
+      rw.release();
+    }
   }
 
   private Change next() {
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 e9d0f2e..7ab8d30 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
@@ -19,7 +19,9 @@
 
 import com.google.gerrit.launcher.GerritLauncher;
 import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.reviewdb.AuthType;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.Inject;
@@ -139,6 +141,7 @@
     final URI[] listenUrls = listenURLs(cfg);
     final boolean reuseAddress = cfg.getBoolean("httpd", "reuseaddress", true);
     final int acceptors = cfg.getInt("httpd", "acceptorThreads", 2);
+    final AuthType authType = ConfigUtil.getEnum(cfg, "auth", null, "type", AuthType.OPENID);
 
     reverseProxy = true;
     final Connector[] connectors = new Connector[listenUrls.length];
@@ -147,11 +150,17 @@
       final int defaultPort;
       final SelectChannelConnector c;
 
+      if (AuthType.CLIENT_SSL_CERT_LDAP.equals(authType) && ! "https".equals(u.getScheme())) {
+        throw new IllegalArgumentException("Protocol '" + u.getScheme()
+            + "' " + " not supported in httpd.listenurl '" + u
+            + "' when auth.type = '" + AuthType.CLIENT_SSL_CERT_LDAP.name()
+            + "'; only 'https' is supported");
+      }
+
       if ("http".equals(u.getScheme())) {
         reverseProxy = false;
         defaultPort = 80;
         c = new SelectChannelConnector();
-
       } else if ("https".equals(u.getScheme())) {
         final SslSelectChannelConnector ssl = new SslSelectChannelConnector();
         final File keystore = getFile(cfg, "sslkeystore", "etc/keystore");
@@ -164,6 +173,10 @@
         ssl.setKeyPassword(password);
         ssl.setTrustPassword(password);
 
+        if (AuthType.CLIENT_SSL_CERT_LDAP.equals(authType)) {
+          ssl.setNeedClientAuth(true);
+        }
+
         reverseProxy = false;
         defaultPort = 443;
         c = ssl;
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 ef95d87..7063f54 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
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.pgm.init;
 
-import static com.google.gerrit.pgm.init.InitUtil.copy;
 import static com.google.gerrit.pgm.init.InitUtil.die;
 import static com.google.gerrit.pgm.init.InitUtil.username;
 
@@ -24,10 +23,14 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
+import org.eclipse.jgit.storage.file.LockFile;
+import org.eclipse.jgit.util.FS;
+
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.io.OutputStream;
 
 /** Initialize the {@code container} configuration section. */
 @Singleton
@@ -84,7 +87,39 @@
           System.err.format("Copying gerrit.war to %s", siteWar.getPath());
           System.err.println();
         }
-        copy(siteWar, new FileInputStream(myWar));
+
+        FileInputStream in = new FileInputStream(myWar);
+        try {
+          siteWar.getParentFile().mkdirs();
+
+          LockFile lf = new LockFile(siteWar, FS.DETECTED);
+          if (!lf.lock()) {
+            throw new IOException("Cannot lock " + siteWar);
+          }
+
+          try {
+            final OutputStream out = lf.getOutputStream();
+            try {
+              final byte[] tmp = new byte[4096];
+              for (;;) {
+                int n = in.read(tmp);
+                if (n < 0) {
+                  break;
+                }
+                out.write(tmp, 0, n);
+              }
+            } finally {
+              out.close();
+            }
+            if (!lf.commit()) {
+              throw new IOException("Cannot commit " + siteWar);
+            }
+          } finally {
+            lf.unlock();
+          }
+        } finally {
+          in.close();
+        }
       }
     }
   }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitFlags.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitFlags.java
index 5ca7e41..992c616 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitFlags.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitFlags.java
@@ -19,7 +19,8 @@
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.FileBasedConfig;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
 
 import java.io.IOException;
 
@@ -40,8 +41,8 @@
 
   @Inject
   InitFlags(final SitePaths site) throws IOException, ConfigInvalidException {
-    cfg = new FileBasedConfig(site.gerrit_config);
-    sec = new FileBasedConfig(site.secure_config);
+    cfg = new FileBasedConfig(site.gerrit_config, FS.DETECTED);
+    sec = new FileBasedConfig(site.secure_config, FS.DETECTED);
 
     cfg.load();
     sec.load();
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 17f0146..1382f65 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
@@ -17,19 +17,23 @@
 import com.google.gerrit.pgm.util.Die;
 
 import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.FileBasedConfig;
-import org.eclipse.jgit.lib.LockFile;
+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;
 
-import java.io.OutputStream;
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
 import java.net.InetAddress;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
 
 /** Utility functions to help initialize a site. */
 class InitUtil {
@@ -42,27 +46,43 @@
   }
 
   static void savePublic(final FileBasedConfig sec) throws IOException {
-    sec.save();
+    if (modified(sec)) {
+      sec.save();
+    }
   }
 
   static void saveSecure(final FileBasedConfig sec) throws IOException {
-    final byte[] out = Constants.encode(sec.toText());
-    final File path = sec.getFile();
-    final LockFile lf = new LockFile(path);
-    if (!lf.lock()) {
-      throw new IOException("Cannot lock " + path);
-    }
-    try {
-      chmod(0600, new File(path.getParentFile(), path.getName() + ".lock"));
-      lf.write(out);
-      if (!lf.commit()) {
-        throw new IOException("Cannot commit write to " + path);
+    if (modified(sec)) {
+      final byte[] out = Constants.encode(sec.toText());
+      final File path = sec.getFile();
+      final LockFile lf = new LockFile(path, FS.DETECTED);
+      if (!lf.lock()) {
+        throw new IOException("Cannot lock " + path);
       }
-    } finally {
-      lf.unlock();
+      try {
+        chmod(0600, new File(path.getParentFile(), path.getName() + ".lock"));
+        lf.write(out);
+        if (!lf.commit()) {
+          throw new IOException("Cannot commit write to " + path);
+        }
+      } finally {
+        lf.unlock();
+      }
     }
   }
 
+  private static boolean modified(FileBasedConfig cfg) throws IOException {
+    byte[] curVers;
+    try {
+      curVers = IO.readFully(cfg.getFile());
+    } catch (FileNotFoundException notFound) {
+      return true;
+    }
+
+    byte[] newVers = Constants.encode(cfg.toText());
+    return !Arrays.equals(curVers, newVers);
+  }
+
   static void mkdir(final File path) {
     if (!path.isDirectory() && !path.mkdir()) {
       throw die("Cannot make directory " + path);
@@ -144,7 +164,8 @@
       final String name) throws IOException {
     final InputStream in = open(sibling, name);
     if (in != null) {
-      copy(dst, in);
+      ByteBuffer buf = IO.readWholeStream(in, 8192);
+      copy(dst, buf);
     }
   }
 
@@ -165,34 +186,41 @@
     return in;
   }
 
-  static void copy(final File dst, final InputStream in)
+  static void copy(final File dst, final ByteBuffer buf)
       throws FileNotFoundException, IOException {
+    // If the file already has the content we want to put there,
+    // don't attempt to overwrite the file.
+    //
     try {
-      dst.getParentFile().mkdirs();
-      LockFile lf = new LockFile(dst);
-      if (!lf.lock()) {
-        throw new IOException("Cannot lock " + dst);
+      if (buf.equals(ByteBuffer.wrap(IO.readFully(dst)))) {
+        return;
       }
-      try {
+    } catch (FileNotFoundException notFound) {
+      // Fall through and write the file.
+    }
 
-        final OutputStream out = lf.getOutputStream();
-        try {
-          final byte[] buf = new byte[4096];
-          int n;
-          while (0 < (n = in.read(buf))) {
-            out.write(buf, 0, n);
-          }
-        } finally {
-          out.close();
-        }
-        if (!lf.commit()) {
-          throw new IOException("Cannot commit " + dst);
+    dst.getParentFile().mkdirs();
+    LockFile lf = new LockFile(dst, FS.DETECTED);
+    if (!lf.lock()) {
+      throw new IOException("Cannot lock " + dst);
+    }
+    try {
+      final OutputStream out = lf.getOutputStream();
+      try {
+        final byte[] tmp = new byte[4096];
+        while (0 < buf.remaining()) {
+          int n = Math.min(buf.remaining(), tmp.length);
+          buf.get(tmp, 0, n);
+          out.write(tmp, 0, n);
         }
       } finally {
-        lf.unlock();
+        out.close();
+      }
+      if (!lf.commit()) {
+        throw new IOException("Cannot commit " + dst);
       }
     } finally {
-      in.close();
+      lf.unlock();
     }
   }
 
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 74e7548..dae0893 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
@@ -25,11 +25,13 @@
 import com.google.gerrit.pgm.Init;
 import com.google.gerrit.pgm.util.ConsoleUI;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.mail.OutgoingEmail;
 import com.google.inject.Binding;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 import com.google.inject.TypeLiteral;
 
+import java.io.File;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -66,6 +68,7 @@
     mkdir(site.etc_dir);
     mkdir(site.lib_dir);
     mkdir(site.logs_dir);
+    mkdir(site.mail_dir);
     mkdir(site.static_dir);
 
     for (InitStep step : steps) {
@@ -82,11 +85,27 @@
     extract(site.gerrit_sh, Init.class, "gerrit.sh");
     chmod(0755, site.gerrit_sh);
 
+    extractMailExample("Abandoned.vm");
+    extractMailExample("ChangeFooter.vm");
+    extractMailExample("ChangeSubject.vm");
+    extractMailExample("Comment.vm");
+    extractMailExample("Merged.vm");
+    extractMailExample("MergeFail.vm");
+    extractMailExample("NewChange.vm");
+    extractMailExample("RegisterNewEmail.vm");
+    extractMailExample("ReplacePatchSet.vm");
+
     if (!ui.isBatch()) {
       System.err.println();
     }
   }
 
+  private void extractMailExample(String orig) throws Exception {
+    File ex = new File(site.mail_dir, orig + ".example");
+    extract(ex, OutgoingEmail.class, orig);
+    chmod(0444, ex);
+  }
+
   private static List<InitStep> stepsOf(final Injector injector) {
     final ArrayList<InitStep> r = new ArrayList<InitStep>();
     for (Binding<InitStep> b : all(injector)) {
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 fad5878..9f62fc5 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
@@ -26,7 +26,7 @@
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.FileBasedConfig;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
 
 import java.io.File;
 import java.io.FileInputStream;
diff --git a/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/InitTestCase.java b/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/InitTestCase.java
index 2964f50..4d7370b 100644
--- a/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/InitTestCase.java
+++ b/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/InitTestCase.java
@@ -21,6 +21,6 @@
 
 public abstract class InitTestCase extends LocalDiskRepositoryTestCase {
   protected File newSitePath() throws IOException {
-    return new File(createWorkRepository().getWorkDir(), "test_site");
+    return new File(createWorkRepository().getWorkTree(), "test_site");
   }
 }
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 72b02d5..00185581 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
@@ -24,7 +24,8 @@
 import com.google.gerrit.server.config.SitePaths;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.FileBasedConfig;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.IO;
 
 import java.io.File;
@@ -50,7 +51,9 @@
       }
     }
 
-    FileBasedConfig old = new FileBasedConfig(new File(p, "gerrit.config"));
+    FileBasedConfig old =
+        new FileBasedConfig(new File(p, "gerrit.config"), FS.DETECTED);
+
     old.setString("ldap", null, "username", "ldap.user");
     old.setString("ldap", null, "password", "ldap.s3kr3t");
 
@@ -84,8 +87,8 @@
           new String(IO.readFully(new File(site.etc_dir, n)), "UTF-8"));
     }
 
-    FileBasedConfig cfg = new FileBasedConfig(site.gerrit_config);
-    FileBasedConfig sec = new FileBasedConfig(site.secure_config);
+    FileBasedConfig cfg = new FileBasedConfig(site.gerrit_config, FS.DETECTED);
+    FileBasedConfig sec = new FileBasedConfig(site.secure_config, FS.DETECTED);
     cfg.load();
     sec.load();
 
diff --git a/gerrit-prettify/pom.xml b/gerrit-prettify/pom.xml
index 99eece6..060ffdd 100644
--- a/gerrit-prettify/pom.xml
+++ b/gerrit-prettify/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.1.4-SNAPSHOT</version>
+    <version>2.1-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-prettify</artifactId>
diff --git a/gerrit-reviewdb/pom.xml b/gerrit-reviewdb/pom.xml
index 5626739..d81b068 100644
--- a/gerrit-reviewdb/pom.xml
+++ b/gerrit-reviewdb/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.1.4-SNAPSHOT</version>
+    <version>2.1-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-reviewdb</artifactId>
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Account.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Account.java
index f428a22..43b7b17 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Account.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Account.java
@@ -129,6 +129,10 @@
   @Column(id = 6, name = Column.NONE)
   protected AccountGeneralPreferences generalPreferences;
 
+  /** Is this user active */
+  @Column(id = 7)
+  protected boolean inactive;
+
   /** <i>computed</i> the username selected from the identities. */
   protected String userName;
 
@@ -198,6 +202,14 @@
     contactFiledOn = new Timestamp(System.currentTimeMillis());
   }
 
+  public boolean isActive() {
+    return ! inactive;
+  }
+
+  public void setActive(boolean active) {
+    inactive = ! active;
+  }
+
   /** @return the computed user name for this account */
   public String getUserName() {
     return userName;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountDiffPreference.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountDiffPreference.java
index 4a3dd18..38a2359 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountDiffPreference.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountDiffPreference.java
@@ -96,6 +96,12 @@
   @Column(id = 9)
   protected short context;
 
+  @Column(id = 10)
+  protected boolean skipDeleted;
+
+  @Column(id = 11)
+  protected boolean skipUncommented;
+
   protected AccountDiffPreference() {
   }
 
@@ -112,6 +118,8 @@
     this.showWhitespaceErrors = p.showWhitespaceErrors;
     this.intralineDifference = p.intralineDifference;
     this.showTabs = p.showTabs;
+    this.skipDeleted = p.skipDeleted;
+    this.skipUncommented = p.skipUncommented;
     this.context = p.context;
   }
 
@@ -185,4 +193,20 @@
     assert 0 <= context || context == WHOLE_FILE_CONTEXT;
     this.context = context;
   }
+
+  public boolean isSkipDeleted() {
+    return skipDeleted;
+  }
+
+  public void setSkipDeleted(boolean skip) {
+    skipDeleted = skip;
+  }
+
+  public boolean isSkipUncommented() {
+    return skipUncommented;
+  }
+
+  public void setSkipUncommented(boolean skip) {
+    skipUncommented = skip;
+  }
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGeneralPreferences.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGeneralPreferences.java
index c69e785..b90129c 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGeneralPreferences.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGeneralPreferences.java
@@ -35,6 +35,51 @@
     REPO_DOWNLOAD, PULL, CHECKOUT, CHERRY_PICK, FORMAT_PATCH;
   }
 
+  public static enum DateFormat {
+    /** US style dates: Apr 27, Feb 14, 2010 */
+    STD("MMM d", "MMM d, yyyy"),
+
+    /** US style dates: 04/27, 02/14/10 */
+    US("MM/dd", "MM/dd/yy"),
+
+    /** ISO style dates: 2010-02-14 */
+    ISO("MM-dd", "yyyy-MM-dd");
+
+    private final String shortFormat;
+    private final String longFormat;
+
+    DateFormat(String shortFormat, String longFormat) {
+      this.shortFormat = shortFormat;
+      this.longFormat = longFormat;
+    }
+
+    public String getShortFormat() {
+      return shortFormat;
+    }
+
+    public String getLongFormat() {
+      return longFormat;
+    }
+  }
+
+  public static enum TimeFormat {
+    /** 12-hour clock: 1:15 am, 2:13 pm */
+    HHMM_12("h:mm a"),
+
+    /** 24-hour clock: 01:15, 14:13 */
+    HHMM_24("HH:mm");
+
+    private final String format;
+
+    TimeFormat(String format) {
+      this.format = format;
+    }
+
+    public String getFormat() {
+      return format;
+    }
+  }
+
   /** Number of changes to show in a screen. */
   @Column(id = 2)
   protected short maximumPageSize;
@@ -59,6 +104,19 @@
   @Column(id = 7)
   protected boolean copySelfOnEmail;
 
+  @Column(id = 8, length = 10, notNull = false)
+  protected String dateFormat;
+
+  @Column(id = 9, length = 10, notNull = false)
+  protected String timeFormat;
+
+  /**
+   * If true display the patch sets in the ChangeScreen in reverse order
+   * (show latest patch set on top).
+   */
+  @Column(id = 10)
+  protected boolean displayPatchSetsInReverseOrder;
+
   public AccountGeneralPreferences() {
   }
 
@@ -124,12 +182,45 @@
     copySelfOnEmail = includeSelfOnEmail;
   }
 
+  public boolean isDisplayPatchSetsInReverseOrder() {
+    return displayPatchSetsInReverseOrder;
+  }
+
+  public void setDisplayPatchSetsInReverseOrder(final boolean displayPatchSetsInReverseOrder) {
+    this.displayPatchSetsInReverseOrder = displayPatchSetsInReverseOrder;
+  }
+
+  public DateFormat getDateFormat() {
+    if (dateFormat == null) {
+      return DateFormat.STD;
+    }
+    return DateFormat.valueOf(dateFormat);
+  }
+
+  public void setDateFormat(DateFormat fmt) {
+    dateFormat = fmt.name();
+  }
+
+  public TimeFormat getTimeFormat() {
+    if (timeFormat == null) {
+      return TimeFormat.HHMM_12;
+    }
+    return TimeFormat.valueOf(timeFormat);
+  }
+
+  public void setTimeFormat(TimeFormat fmt) {
+    timeFormat = fmt.name();
+  }
+
   public void resetToDefaults() {
     maximumPageSize = DEFAULT_PAGESIZE;
     showSiteHeader = true;
     useFlashClipboard = true;
     copySelfOnEmail = false;
+    displayPatchSetsInReverseOrder = false;
     downloadUrl = null;
     downloadCommand = null;
+    dateFormat = null;
+    timeFormat = null;
   }
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountProjectWatch.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountProjectWatch.java
index 52bef2b..6713d8f 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountProjectWatch.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountProjectWatch.java
@@ -20,6 +20,11 @@
 
 /** An {@link Account} interested in a {@link Project}. */
 public final class AccountProjectWatch {
+
+  public enum Type {
+    NEW_CHANGES, SUBMITS, COMMENTS
+  }
+
   public static final String FILTER_ALL = "*";
 
   public static class Key extends CompoundKey<Account.Id> {
@@ -142,4 +147,24 @@
   public void setNotifySubmittedChanges(final boolean a) {
     notifySubmittedChanges = a;
   }
+
+  public boolean isNotify(final Type type) {
+    switch(type) {
+      case NEW_CHANGES: return notifySubmittedChanges;
+      case SUBMITS:     return notifyNewChanges;
+      case COMMENTS:    return notifyAllComments;
+    }
+    return false;
+  }
+
+  public void setNotify(final Type type, final boolean v) {
+    switch(type) {
+      case NEW_CHANGES: notifySubmittedChanges = v;
+        break;
+      case SUBMITS:     notifyNewChanges = v;
+        break;
+      case COMMENTS:    notifyAllComments = v;
+        break;
+    }
+  }
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AuthType.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AuthType.java
index 46b435c..5d69e21 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AuthType.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AuthType.java
@@ -40,6 +40,21 @@
   HTTP_LDAP,
 
   /**
+   * Login via client SSL certificate.
+   * <p>
+   * This authentication type is actually kind of SSO. Gerrit will configure
+   * Jetty's SSL channel to request client's SSL certificate. For this
+   * authentication to work a Gerrit administrator has to import the root
+   * certificate of the trust chain used to issue the client's certificate
+   * into the <review-site>/etc/keystore.
+   * <p>
+   * After the authentication is done Gerrit will obtain basic user
+   * registration (name and email) from LDAP, and some group memberships.
+   * Therefore, the "_LDAP" suffix in the name of this authentication type.
+   */
+  CLIENT_SSL_CERT_LDAP,
+
+  /**
    * Login collects username and password through a web form, and binds to LDAP.
    * <p>
    * Unlike {@link #HTTP_LDAP}, Gerrit presents a sign-in dialog to the user and
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Patch.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Patch.java
index 3a922fb..0153755 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Patch.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Patch.java
@@ -136,29 +136,7 @@
      * the only information it can display is the old and new file content
      * hashes.
      */
-    BINARY('B'),
-
-    /**
-     * Difference of three or more textual contents.
-     *
-     * <p>
-     * Git can produce an n-way unified diff, showing how a merge conflict was
-     * resolved when two or more conflicting branches were merged together in a
-     * single merge commit.
-     *
-     * <p>
-     * This type of patch can only appear if there are two or more
-     * {@link PatchSetAncestor} entities for the same parent {@link PatchSet},
-     * as that denotes that the patch set is a merge commit.
-     *
-     * <p>
-     * Gerrit can only render an N_WAY file in a PatchScreen.Unified view, as it
-     * does not have code to split the n-way unified diff into multiple edit
-     * lists, one per pre-image. However, a logical way to display this format
-     * would be an n-way table, with n+1 columns displayed (n pre-images, +1
-     * post-image).
-     */
-    N_WAY('N');
+    BINARY('B');
 
     private final char code;
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Project.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Project.java
index e37c78d..409547a 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Project.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Project.java
@@ -99,6 +99,12 @@
   @Column(id = 6, notNull = false, name = "parent_name")
   protected NameKey parent;
 
+  @Column(id = 7)
+  protected boolean requireChangeID;
+
+  @Column(id = 8)
+  protected boolean useContentMerge;
+
   protected Project() {
   }
 
@@ -136,10 +142,26 @@
     return useSignedOffBy;
   }
 
+  public boolean isUseContentMerge() {
+    return useContentMerge;
+  }
+
+  public boolean isRequireChangeID() {
+    return requireChangeID;
+  }
+
   public void setUseSignedOffBy(final boolean sbo) {
     useSignedOffBy = sbo;
   }
 
+  public void setUseContentMerge(final boolean cm) {
+    useContentMerge = cm;
+  }
+
+  public void setRequireChangeID(final boolean cid) {
+    requireChangeID = cid;
+  }
+
   public SubmitType getSubmitType() {
     return SubmitType.forCode(submitType);
   }
@@ -152,6 +174,8 @@
     description = update.description;
     useContributorAgreements = update.useContributorAgreements;
     useSignedOffBy = update.useSignedOffBy;
+    useContentMerge = update.useContentMerge;
+    requireChangeID = update.requireChangeID;
     submitType = update.submitType;
   }
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/RefRight.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/RefRight.java
index ec70051..97ee219 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/RefRight.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/RefRight.java
@@ -96,6 +96,10 @@
       return refPattern.get();
     }
 
+    public void setGroupId(AccountGroup.Id groupId) {
+      this.groupId = groupId;
+    }
+
     @Override
     public com.google.gwtorm.client.Key<?>[] members() {
       return new com.google.gwtorm.client.Key<?>[] {refPattern, categoryId,
@@ -119,6 +123,13 @@
     this.key = key;
   }
 
+  public RefRight(final RefRight refRight, final AccountGroup.Id groupId) {
+    this(new RefRight.Key(refRight.getKey().projectName,
+        refRight.getKey().refPattern, refRight.getKey().categoryId, groupId));
+    setMinValue(refRight.getMinValue());
+    setMaxValue(refRight.getMaxValue());
+  }
+
   public RefRight.Key getKey() {
     return key;
   }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/SystemConfig.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/SystemConfig.java
index f9b6a2d..6ff23ed 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/SystemConfig.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/SystemConfig.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.reviewdb;
 
-import com.google.gerrit.reviewdb.AccountGroup.Id;
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.StringKey;
 
@@ -83,6 +82,10 @@
   @Column(id = 8)
   public AccountGroup.Id batchUsersGroupId;
 
+  /** Identity of the owner group, which permits any project owner. */
+  @Column(id = 9)
+  public AccountGroup.Id ownerGroupId;
+
   protected SystemConfig() {
   }
 }
diff --git a/gerrit-server/pom.xml b/gerrit-server/pom.xml
index db8093c..0a9ece3 100644
--- a/gerrit-server/pom.xml
+++ b/gerrit-server/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.1.4-SNAPSHOT</version>
+    <version>2.1-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-server</artifactId>
@@ -34,6 +34,16 @@
 
   <dependencies>
     <dependency>
+      <groupId>log4j</groupId>
+      <artifactId>log4j</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>org.apache.velocity</groupId>
+      <artifactId>velocity</artifactId>
+    </dependency>
+
+    <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit</artifactId>
     </dependency>
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 d29bc23..c7c51b6 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
@@ -19,6 +19,7 @@
 import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.reviewdb.ApprovalCategory;
 import com.google.gerrit.reviewdb.ApprovalCategoryValue;
+import com.google.gerrit.reviewdb.Branch;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.PatchSet;
 import com.google.gerrit.reviewdb.Project;
@@ -35,6 +36,7 @@
 import com.google.gerrit.server.events.CommentAddedEvent;
 import com.google.gerrit.server.events.EventFactory;
 import com.google.gerrit.server.events.PatchSetCreatedEvent;
+import com.google.gerrit.server.events.RefUpdatedEvent;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.project.ProjectCache;
@@ -45,6 +47,8 @@
 
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -96,6 +100,9 @@
     /** Filename of the change abandoned hook. */
     private final File changeRestoredHook;
 
+    /** Filename of the ref updated hook. */
+    private final File refUpdatedHook;
+
     /** Repository Manager. */
     private final GitRepositoryManager repoManager;
 
@@ -141,6 +148,7 @@
         changeMergedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "changeMergedHook", "change-merged")).getPath());
         changeAbandonedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "changeAbandonedHook", "change-abandoned")).getPath());
         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());
     }
 
     public void addChangeListener(ChangeListener listener, IdentifiedUser user) {
@@ -172,7 +180,16 @@
      * @return Repository or null.
      */
     private Repository openRepository(final Change change) {
-        Project.NameKey name = change.getProject();
+        return openRepository(change.getProject());
+    }
+
+    /**
+     * Get the Repository for the given project name, or null on error.
+     *
+     * @param name Project to get repo for,
+     * @return Repository or null.
+     */
+    private Repository openRepository(final Project.NameKey name) {
         try {
             return repoManager.openRepository(name.get());
         } catch (RepositoryNotFoundException err) {
@@ -335,6 +352,44 @@
         runHook(openRepository(change), changeRestoredHook, args);
     }
 
+    /**
+     * Fire the Ref Updated Hook
+     * @param project The project the ref update occured on
+     * @param refUpdate An actual RefUpdate object
+     * @param account The gerrit user who moved the ref
+     */
+    public void doRefUpdatedHook(final Branch.NameKey refName, final RefUpdate refUpdate, final Account account) {
+      doRefUpdatedHook(refName, refUpdate.getOldObjectId(), refUpdate.getNewObjectId(), account);
+    }
+
+    /**
+     * Fire the Ref Updated Hook
+     * @param refName The Branch.NameKey of the ref that was updated
+     * @param oldId The ref's old id
+     * @param newId The ref's new id
+     * @param account The gerrit user who moved the ref
+     */
+    public void doRefUpdatedHook(final Branch.NameKey refName, final ObjectId oldId, final ObjectId newId, final Account account) {
+      final RefUpdatedEvent event = new RefUpdatedEvent();
+
+      if (account != null) {
+        event.submitter = eventFactory.asAccountAttribute(account);
+      }
+      event.refUpdate = eventFactory.asRefUpdateAttribute(oldId, newId, refName);
+      fireEvent(refName, event);
+
+      final List<String> args = new ArrayList<String>();
+      addArg(args, "--oldrev", event.refUpdate.oldRev);
+      addArg(args, "--newrev", event.refUpdate.newRev);
+      addArg(args, "--refname", event.refUpdate.refName);
+      addArg(args, "--project", event.refUpdate.project);
+      if (account != null) {
+        addArg(args, "--submitter", getDisplayName(account));
+      }
+
+      runHook(openRepository(refName.getParentKey()), refUpdatedHook, args);
+    }
+
     private void fireEvent(final Change change, final ChangeEvent event) {
       for (ChangeListenerHolder holder : listeners.values()) {
           if (isVisibleTo(change, holder.user)) {
@@ -343,6 +398,14 @@
       }
     }
 
+    private void fireEvent(Branch.NameKey branchName, final ChangeEvent event) {
+      for (ChangeListenerHolder holder : listeners.values()) {
+          if (isVisibleTo(branchName, holder.user)) {
+              holder.listener.onChangeEvent(event);
+          }
+      }
+    }
+
     private boolean isVisibleTo(Change change, IdentifiedUser user) {
         final ProjectState pe = projectCache.get(change.getProject());
         if (pe == null) {
@@ -352,6 +415,15 @@
         return pc.controlFor(change).isVisible();
     }
 
+    private boolean isVisibleTo(Branch.NameKey branchName, IdentifiedUser user) {
+        final ProjectState pe = projectCache.get(branchName.getParentKey());
+        if (pe == null) {
+          return false;
+        }
+        final ProjectControl pc = pe.controlFor(user);
+        return pc.controlForRef(branchName).isVisible();
+    }
+
     /**
      * Create an ApprovalAttribute for the given approval suitable for serialization to JSON.
      * @param approval
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/CollectionsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/common/CollectionsUtil.java
new file mode 100644
index 0000000..6e635cb
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/CollectionsUtil.java
@@ -0,0 +1,43 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License
+
+package com.google.gerrit.common;
+
+import java.util.Collection;
+
+/** Utilities for manipulating Collections . */
+public class CollectionsUtil {
+  /**
+   * Checks if any of the elements in the first collection can be found in the
+   * second collection.
+   *
+   * @param findAnyOfThese which elements to look for.
+   * @param inThisCollection where to look for them.
+   * @param <E> type of the elements in question.
+   * @return {@code true} if any of the elements in {@code findAnyOfThese} can
+   *         be found in {@code inThisCollection}, {@code false} otherwise.
+   */
+  public static <E> boolean isAnyIncludedIn(Collection<E> findAnyOfThese,
+      Collection<E> inThisCollection) {
+    for (E findThisItem : findAnyOfThese) {
+      if (inThisCollection.contains(findThisItem)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private CollectionsUtil() {
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ReplicationUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/ReplicationUser.java
index 3293324..5c44bfe 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ReplicationUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ReplicationUser.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.reviewdb.AccountGroup;
 import com.google.gerrit.reviewdb.AccountProjectWatch;
 import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.Project.NameKey;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -47,13 +46,7 @@
       effectiveGroups = EVERYTHING_VISIBLE;
 
     } else if (authGroups.isEmpty()) {
-      // Only include the registered groups if no specific groups
-      // were provided. This allows an administrator to configure
-      // a replication user with a narrower view of the system than
-      // all other users, such as when replicating from an internal
-      // company server to a public open source distribution site.
-      //
-      effectiveGroups = authConfig.getRegisteredGroups();
+      effectiveGroups = Collections.emptySet();
 
     } else {
       effectiveGroups = copy(authGroups);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
index 24ddd27..5cb8f36 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
@@ -100,7 +100,7 @@
    * @param who identity of the user, with any details we received about them.
    * @return the result of authenticating the user.
    * @throws AccountException the account does not exist, and cannot be created,
-   *         or exists, but cannot be located.
+   *         or exists, but cannot be located, or is inactive.
    */
   public AuthResult authenticate(AuthRequest who) throws AccountException {
     who = realm.authenticate(who);
@@ -114,9 +114,14 @@
           //
           return create(db, who);
 
-        } else {
-          // Account exists, return the identity to the caller.
-          //
+        } else { // Account exists
+
+          Account act = db.accounts().get(id.getAccountId());
+          if (act == null || !act.isActive()) {
+            throw new AccountException("Authentication error, account inactive");
+          }
+
+          // return the identity to the caller.
           update(db, who, id);
           return new AuthResult(id.getAccountId(), key, false);
         }
@@ -285,13 +290,18 @@
       try {
         changeUserNameFactory.create(db, user, who.getUserName()).call();
       } catch (NameAlreadyUsedException e) {
-        log.error("Cannot assign user name \"" + who.getUserName()
-            + "\" to account " + newId + "; name already in use.");
+        final String message =
+            "Cannot assign user name \"" + who.getUserName() + "\" to account "
+                + newId + "; name already in use.";
+        handleSettingUserNameFailure(db, account, extId, message, e, false);
       } catch (InvalidUserNameException e) {
-        log.error("Cannot assign user name \"" + who.getUserName()
-            + "\" to account " + newId + "; name does not conform.");
+        final String message =
+            "Cannot assign user name \"" + who.getUserName() + "\" to account "
+                + newId + "; name does not conform.";
+        handleSettingUserNameFailure(db, account, extId, message, e, false);
       } catch (OrmException e) {
-        log.error("Cannot assign user name", e);
+        final String message = "Cannot assign user name";
+        handleSettingUserNameFailure(db, account, extId, message, e, true);
       }
     }
 
@@ -300,6 +310,49 @@
     return new AuthResult(newId, extId.getKey(), true);
   }
 
+  /**
+   * This method handles an exception that occurred during the setting of the
+   * user name for a newly created account. If the realm does not allow the user
+   * to set a user name manually this method deletes the newly created account
+   * and throws an {@link AccountUserNameException}. In any case the error
+   * message is logged.
+   *
+   * @param db the database
+   * @param account the newly created account
+   * @param extId the newly created external id
+   * @param errorMessage the error message
+   * @param e the exception that occurred during the setting of the user name
+   *        for the new account
+   * @param logException flag that decides whether the exception should be
+   *        included into the log
+   * @throws AccountUserNameException thrown if the realm does not allow the
+   *         user to manually set the user name
+   * @throws OrmException thrown if cleaning the database failed
+   */
+  private void handleSettingUserNameFailure(final ReviewDb db,
+      final Account account, final AccountExternalId extId,
+      final String errorMessage, final Exception e, final boolean logException)
+      throws AccountUserNameException, OrmException {
+    if (logException) {
+      log.error(errorMessage, e);
+    } else {
+      log.error(errorMessage);
+    }
+    if (!realm.allowsEdit(Account.FieldName.USER_NAME)) {
+      // setting the given user name has failed, but the realm does not
+      // allow the user to manually set a user name,
+      // this means we would end with an account without user name
+      // (without 'username:<USERNAME>' entry in
+      // account_external_ids table),
+      // such an account cannot be used for uploading changes,
+      // this is why the best we can do here is to fail early and cleanup
+      // the database
+      db.accounts().delete(Collections.singleton(account));
+      db.accountExternalIds().delete(Collections.singleton(extId));
+      throw new AccountUserNameException(errorMessage, e);
+    }
+  }
+
   private static AccountExternalId createId(final Account.Id newId,
       final AuthRequest who) {
     final String ext = who.getExternalId();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountUserNameException.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountUserNameException.java
new file mode 100644
index 0000000..1cf8be8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountUserNameException.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+/**
+ * Thrown by {@link AccountManager} if the user name for a newly created account
+ * could not be set and the realm does not allow the user to set a user name
+ * manually.
+ */
+public class AccountUserNameException extends AccountException {
+  private static final long serialVersionUID = 1L;
+
+  public AccountUserNameException(final String message, final Throwable why) {
+    super(message, why);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java
index 6f6a4d4..675202c 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
@@ -33,7 +33,9 @@
 import java.util.Properties;
 import java.util.Set;
 
+import javax.naming.CompositeName;
 import javax.naming.Context;
+import javax.naming.Name;
 import javax.naming.NamingEnumeration;
 import javax.naming.NamingException;
 import javax.naming.directory.Attribute;
@@ -196,8 +198,9 @@
       // Recursively identify the groups it is a member of.
       //
       try {
+        final Name compositeGroupName = new CompositeName().add(groupDN);
         final Attribute in =
-            ctx.getAttributes(groupDN).get(schema.accountMemberField);
+            ctx.getAttributes(compositeGroupName).get(schema.accountMemberField);
         if (in != null) {
           final NamingEnumeration<?> groups = in.getAll();
           while (groups.hasMore()) {
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 e66746d..6396431 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
@@ -146,6 +146,7 @@
       case HTTP_LDAP:
       case LDAP:
       case LDAP_BIND:
+      case CLIENT_SSL_CERT_LDAP:
         // Its safe to assume yes for an HTTP authentication type, as the
         // only way in is through some external system that the admin trusts
         //
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java
index 73320ad..927e88ee 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
@@ -21,6 +21,7 @@
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gwtorm.client.OrmException;
 import com.google.gwtorm.client.SchemaFactory;
+
 import org.eclipse.jgit.lib.Config;
 import org.slf4j.Logger;
 
@@ -31,6 +32,8 @@
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 public class ConfigUtil {
   /**
@@ -226,7 +229,7 @@
   /**
    * Parse a numerical time unit, such as "1 minute", from a string.
    *
-   * @param s the string to parse.
+   * @param valueString the string to parse.
    * @param defaultValue default value to return if no value was set in the
    *        configuration file.
    * @param wantUnit the units of {@code defaultValue} and the return value, as
@@ -235,26 +238,16 @@
    * @return the setting, or {@code defaultValue} if not set, expressed in
    *         {@code units}.
    */
-  public static long getTimeUnit(String s, long defaultValue, TimeUnit wantUnit) {
-    final String valueString = s;
-    final String unitName;
-    final int sp = s.indexOf(' ');
-    if (sp > 0) {
-      unitName = s.substring(sp + 1).trim();
-      s = s.substring(0, sp);
-    } else {
-      final char last = s.charAt(s.length() - 1);
-      if ('0' <= last && last <= '9') {
-        unitName = "";
-      } else {
-        unitName = String.valueOf(last);
-        s = s.substring(0, s.length() - 1).trim();
-      }
-    }
-    if (s.length() == 0) {
+  public static long getTimeUnit(final String valueString, long defaultValue,
+      TimeUnit wantUnit) {
+    Matcher m = Pattern.compile("^([1-9][0-9]*)\\s*(.*)$").matcher(valueString);
+    if (!m.matches()) {
       return defaultValue;
     }
 
+    String digits = m.group(1);
+    String unitName = m.group(2).trim();
+
     TimeUnit inputUnit;
     int inputMul;
 
@@ -299,7 +292,7 @@
     }
 
     try {
-      return wantUnit.convert(Long.parseLong(s) * inputMul, inputUnit);
+      return wantUnit.convert(Long.parseLong(digits) * inputMul, inputUnit);
     } catch (NumberFormatException nfe) {
       throw notTimeUnit(valueString);
     }
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 fa2aaad..f77550e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -17,6 +17,7 @@
 import static com.google.inject.Scopes.SINGLETON;
 
 import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.lifecycle.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.reviewdb.AccountGroup;
 import com.google.gerrit.reviewdb.AuthType;
@@ -37,6 +38,7 @@
 import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.auth.ldap.LdapModule;
 import com.google.gerrit.server.cache.CachePool;
+import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.events.EventFactory;
 import com.google.gerrit.server.git.ChangeMergeQueue;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -46,6 +48,7 @@
 import com.google.gerrit.server.git.PushReplication;
 import com.google.gerrit.server.git.ReloadSubmitQueueOp;
 import com.google.gerrit.server.git.ReplicationQueue;
+import com.google.gerrit.server.git.TransferConfig;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.mail.EmailSender;
 import com.google.gerrit.server.mail.FromAddressGenerator;
@@ -53,25 +56,66 @@
 import com.google.gerrit.server.mail.SmtpEmailSender;
 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.ProjectCacheImpl;
 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.tools.ToolsCatalog;
 import com.google.gerrit.server.util.IdGenerator;
 import com.google.gerrit.server.workflow.FunctionState;
 import com.google.inject.Inject;
 import com.google.inject.TypeLiteral;
 
+import org.apache.velocity.app.Velocity;
+import org.apache.velocity.runtime.RuntimeConstants;
+
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.PersonIdent;
 
+import java.util.Properties;
 import java.util.Set;
 
+
 /** Starts global state with standard dependencies. */
 public class GerritGlobalModule extends FactoryModule {
   private final AuthType loginType;
 
+  public static class VelocityLifecycle implements LifecycleListener {
+    private final SitePaths site;
+
+    @Inject
+    VelocityLifecycle(final SitePaths site) {
+      this.site = site;
+    }
+
+    @Override
+    public void start() {
+      String rl = "resource.loader";
+      String pkg = "org.apache.velocity.runtime.resource.loader";
+      Properties p = new Properties();
+
+      p.setProperty(rl, "file, class");
+      p.setProperty("file." + rl + ".class", pkg + ".FileResourceLoader");
+      p.setProperty("file." + rl + ".path", site.mail_dir.getAbsolutePath());
+      p.setProperty("class." + rl + ".class", pkg + ".ClasspathResourceLoader");
+      p.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM_CLASS,
+              "org.apache.velocity.runtime.log.SimpleLog4JLogSystem" );
+      p.setProperty("runtime.log.logsystem.log4j.category", "velocity");
+
+      try {
+        Velocity.init(p);
+      } catch(Exception e) {
+        throw new RuntimeException(e);
+      }
+    }
+
+    @Override
+    public void stop() {
+    }
+  }
+
   @Inject
   GerritGlobalModule(final AuthConfig authConfig,
       @GerritServerConfig final Config config) {
@@ -84,6 +128,7 @@
       case HTTP_LDAP:
       case LDAP:
       case LDAP_BIND:
+      case CLIENT_SSL_CERT_LDAP:
         install(new LdapModule());
         break;
 
@@ -94,10 +139,6 @@
 
     bind(Project.NameKey.class).annotatedWith(WildProjectName.class)
         .toProvider(WildProjectNameProvider.class).in(SINGLETON);
-    bind(new TypeLiteral<Set<AccountGroup.Id>>(){}).annotatedWith(ProjectCreatorGroups.class)
-        .toProvider(ProjectCreatorGroupsProvider.class).in(SINGLETON);
-    bind(new TypeLiteral<Set<AccountGroup.Id>>(){}).annotatedWith(ProjectOwnerGroups.class)
-        .toProvider(ProjectOwnerGroupsProvider.class).in(SINGLETON);
     bind(ApprovalTypes.class).toProvider(ApprovalTypesProvider.class).in(
         SINGLETON);
     bind(EmailExpander.class).toProvider(EmailExpanderProvider.class).in(
@@ -114,15 +155,18 @@
     install(GroupCacheImpl.module());
     install(PatchListCacheImpl.module());
     install(ProjectCacheImpl.module());
+    install(new AccessControlModule());
 
     factory(AccountInfoCacheFactory.Factory.class);
     factory(ProjectState.Factory.class);
+    factory(RefControl.Factory.class);
 
     bind(GitRepositoryManager.class).to(LocalDiskRepositoryManager.class);
     bind(FileTypeRegistry.class).to(MimeUtilFileTypeRegistry.class);
     bind(WorkQueue.class);
     bind(ToolsCatalog.class);
     bind(EventFactory.class);
+    bind(TransferConfig.class);
 
     bind(ReplicationQueue.class).to(PushReplication.class).in(SINGLETON);
     factory(PushAllProjectsOp.Factory.class);
@@ -147,6 +191,7 @@
         listener().to(LocalDiskRepositoryManager.Lifecycle.class);
         listener().to(CachePool.Lifecycle.class);
         listener().to(WorkQueue.Lifecycle.class);
+        listener().to(VelocityLifecycle.class);
       }
     });
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerConfigProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerConfigProvider.java
index 6592ac7..92a2614 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerConfigProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerConfigProvider.java
@@ -20,7 +20,8 @@
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.FileBasedConfig;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -40,7 +41,7 @@
 
   @Override
   public Config get() {
-    FileBasedConfig cfg = new FileBasedConfig(site.gerrit_config);
+    FileBasedConfig cfg = new FileBasedConfig(site.gerrit_config, FS.DETECTED);
 
     if (!cfg.getFile().exists()) {
       log.info("No " + site.gerrit_config.getAbsolutePath()
@@ -57,7 +58,7 @@
     }
 
     if (site.secure_config.exists()) {
-      cfg = new FileBasedConfig(cfg, site.secure_config);
+      cfg = new FileBasedConfig(cfg, site.secure_config, FS.DETECTED);
       try {
         cfg.load();
       } catch (IOException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroups.java
new file mode 100644
index 0000000..35ea9e6
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroups.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License
+
+package com.google.gerrit.server.config;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.Retention;
+
+/**
+ * Used to populate the groups of users that are allowed to run
+ * receive-pack on the server.
+ *
+ * Gerrit.config example:
+ *
+ * <pre>
+ * [receive]
+ *     allowGroup = RECEIVE_GROUP_ALLOWED
+ * </pre>
+ */
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface GitReceivePackGroups {
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java
new file mode 100644
index 0000000..9af6d62
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License
+
+package com.google.gerrit.server.config;
+
+import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gwtorm.client.SchemaFactory;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.util.Collections;
+import java.util.HashSet;
+
+public class GitReceivePackGroupsProvider extends GroupSetProvider {
+  @Inject
+  public GitReceivePackGroupsProvider(@GerritServerConfig Config config,
+      AuthConfig authConfig, SchemaFactory<ReviewDb> db) {
+    super(config, db, "receive", null, "allowGroup");
+
+    // If no group was set, default to "registered users"
+    //
+    if (groupIds.isEmpty()) {
+      HashSet<AccountGroup.Id> all = new HashSet<AccountGroup.Id>();
+      all.addAll(authConfig.getRegisteredGroups());
+      all.removeAll(authConfig.getAnonymousGroups());
+      groupIds = Collections.unmodifiableSet(all);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroups.java
new file mode 100644
index 0000000..fa8ccb7
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroups.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License
+
+package com.google.gerrit.server.config;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.Retention;
+
+/**
+ * Used to populate the groups of users that are allowed to run
+ * upload-pack on the server.
+ *
+ * Gerrit.config example:
+ *
+ * <pre>
+ * [upload]
+ *     allowGroup = UPLOAD_GROUP_ALLOWED
+ * </pre>
+ */
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface GitUploadPackGroups {
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java
new file mode 100644
index 0000000..bfb09a5
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License
+
+package com.google.gerrit.server.config;
+
+import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gwtorm.client.SchemaFactory;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.util.Collections;
+import java.util.HashSet;
+
+public class GitUploadPackGroupsProvider extends GroupSetProvider {
+  @Inject
+  public GitUploadPackGroupsProvider(@GerritServerConfig Config config,
+      AuthConfig authConfig, SchemaFactory<ReviewDb> db) {
+    super(config, db, "upload", null, "allowGroup");
+
+    // If no group was set, default to "registered users" and "anonymous"
+    //
+    if (groupIds.isEmpty()) {
+      HashSet<AccountGroup.Id> all = new HashSet<AccountGroup.Id>();
+      all.addAll(authConfig.getRegisteredGroups());
+      all.addAll(authConfig.getAnonymousGroups());
+      groupIds = Collections.unmodifiableSet(all);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java
new file mode 100644
index 0000000..373fdb5
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java
@@ -0,0 +1,50 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import static com.google.gerrit.server.config.ConfigUtil.groupsFor;
+import static java.util.Collections.unmodifiableSet;
+
+import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gwtorm.client.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Set;
+
+public abstract class GroupSetProvider implements
+    Provider<Set<AccountGroup.Id>> {
+  private static final Logger log =
+      LoggerFactory.getLogger(GroupSetProvider.class);
+
+  protected Set<AccountGroup.Id> groupIds;
+
+  @Inject
+  protected GroupSetProvider(@GerritServerConfig Config config,
+      SchemaFactory<ReviewDb> db, String section, String subsection, String name) {
+    String[] groupNames = config.getStringList(section, subsection, name);
+    groupIds = unmodifiableSet(groupsFor(db, groupNames, log));
+  }
+
+  @Override
+  public Set<AccountGroup.Id> get() {
+    return groupIds;
+  }
+}
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 d2c5005..26c76c5 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
@@ -20,6 +20,8 @@
 import com.google.gerrit.server.git.ReloadSubmitQueueOp;
 import com.google.inject.Inject;
 
+import org.eclipse.jgit.lib.Config;
+
 import java.util.concurrent.TimeUnit;
 
 /** Configuration for a master node in a cluster of servers. */
@@ -32,17 +34,24 @@
   static class OnStart implements LifecycleListener {
     private final PushAllProjectsOp.Factory pushAll;
     private final ReloadSubmitQueueOp.Factory submit;
+    private final boolean replicateOnStartup;
 
     @Inject
     OnStart(final PushAllProjectsOp.Factory pushAll,
-        final ReloadSubmitQueueOp.Factory submit) {
+        final ReloadSubmitQueueOp.Factory submit,
+        final @GerritServerConfig Config cfg) {
       this.pushAll = pushAll;
       this.submit = submit;
+
+      replicateOnStartup = cfg.getBoolean("gerrit", "replicateOnStartup", true);
     }
 
     @Override
     public void start() {
-      pushAll.create(null).start(30, TimeUnit.SECONDS);
+      if (replicateOnStartup) {
+        pushAll.create(null).start(30, TimeUnit.SECONDS);
+      }
+
       submit.create().start(15, TimeUnit.SECONDS);
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectCreatorGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectCreatorGroupsProvider.java
index 616e691..381c914 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectCreatorGroupsProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectCreatorGroupsProvider.java
@@ -14,19 +14,14 @@
 
 package com.google.gerrit.server.config;
 
-import com.google.gerrit.reviewdb.AccountGroup;
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.reviewdb.SystemConfig;
 import com.google.gwtorm.client.SchemaFactory;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.util.Collections;
-import java.util.Set;
 
 /**
  * Provider of the group(s) which are allowed to create new projects. Currently
@@ -39,27 +34,14 @@
  *     createGroup = Administrators
  * </pre>
  */
-public class ProjectCreatorGroupsProvider implements
-    Provider<Set<AccountGroup.Id>> {
-  private static final Logger log =
-      LoggerFactory.getLogger(ProjectCreatorGroupsProvider.class);
-
-  private final Set<AccountGroup.Id> groupIds;
-
+public class ProjectCreatorGroupsProvider extends GroupSetProvider {
   @Inject
-  ProjectCreatorGroupsProvider(@GerritServerConfig final Config config,
-      SchemaFactory<ReviewDb> db, final SystemConfig systemConfig) {
-    String[] names = config.getStringList("repository", "*", "createGroup");
-    Set<AccountGroup.Id> createGroups = ConfigUtil.groupsFor(db, names, log);
+  public ProjectCreatorGroupsProvider(@GerritServerConfig final Config config,
+      final SystemConfig systemConfig, final SchemaFactory<ReviewDb> db) {
+    super(config, db, "repository", "*", "createGroup");
 
-    if (createGroups.isEmpty()) {
+    if (groupIds.isEmpty()) {
       groupIds = Collections.singleton(systemConfig.adminGroupId);
-    } else {
-      groupIds = createGroups;
     }
   }
-
-  public Set<AccountGroup.Id> get() {
-    return groupIds;
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
index 30234c0..c457d73 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
@@ -18,11 +18,8 @@
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gwtorm.client.SchemaFactory;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.util.Set;
 
@@ -37,28 +34,15 @@
  *     ownerGroup = Administrators
  * </pre>
  */
-public class ProjectOwnerGroupsProvider implements
-    Provider<Set<AccountGroup.Id>> {
-  private static final Logger log =
-      LoggerFactory.getLogger(ProjectOwnerGroupsProvider.class);
-
-  private final Set<AccountGroup.Id> groupIds;
-
+public class ProjectOwnerGroupsProvider extends GroupSetProvider {
   @Inject
-  ProjectOwnerGroupsProvider(@GerritServerConfig final Config config,
-      SchemaFactory<ReviewDb> db,
-      @ProjectCreatorGroups Set<AccountGroup.Id> creatorGroups) {
-    String[] names = config.getStringList("repository", "*", "ownerGroup");
-    Set<AccountGroup.Id> ownerGroups = ConfigUtil.groupsFor(db, names, log);
+  public ProjectOwnerGroupsProvider(
+      @ProjectCreatorGroups final Set<AccountGroup.Id> creatorGroups,
+      @GerritServerConfig final Config config, final SchemaFactory<ReviewDb> db) {
+    super(config, db, "repository", "*", "ownerGroup");
 
-    if (ownerGroups.isEmpty()) {
+    if (groupIds.isEmpty()) {
       groupIds = creatorGroups;
-    } else {
-      groupIds = ownerGroups;
     }
   }
-
-  public Set<AccountGroup.Id> get() {
-    return groupIds;
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
index 1faa672..c3a5fb7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
@@ -28,6 +28,7 @@
   public final File etc_dir;
   public final File lib_dir;
   public final File logs_dir;
+  public final File mail_dir;
   public final File hooks_dir;
   public final File static_dir;
 
@@ -61,6 +62,7 @@
     etc_dir = new File(site_path, "etc");
     lib_dir = new File(site_path, "lib");
     logs_dir = new File(site_path, "logs");
+    mail_dir = new File(etc_dir, "mail");
     hooks_dir = new File(site_path, "hooks");
     static_dir = new File(site_path, "static");
 
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 3ad397e..573e6bb 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
@@ -17,6 +17,7 @@
 import com.google.gerrit.common.data.ApprovalType;
 import com.google.gerrit.common.data.ApprovalTypes;
 import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.Branch;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.PatchSet;
 import com.google.gerrit.reviewdb.PatchSetApproval;
@@ -28,8 +29,11 @@
 import com.google.inject.Singleton;
 import com.google.inject.internal.Nullable;
 
+import org.eclipse.jgit.lib.ObjectId;
+
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Map;
 
 @Singleton
 public class EventFactory {
@@ -67,6 +71,23 @@
   }
 
   /**
+   * Create a RefUpdateAttribute for the given old ObjectId, new ObjectId, and
+   * branch that is suitable for serialization to JSON.
+   *
+   * @param refUpdate
+   * @param refName
+   * @return object suitable for serialization to JSON
+   */
+  public RefUpdateAttribute asRefUpdateAttribute(final ObjectId oldId, final ObjectId newId, final Branch.NameKey refName) {
+    RefUpdateAttribute ru = new RefUpdateAttribute();
+    ru.newRev = newId != null ? newId.getName() : ObjectId.zeroId().getName();
+    ru.oldRev = oldId != null ? oldId.getName() : ObjectId.zeroId().getName();
+    ru.project = refName.getParentKey().get();
+    ru.refName = refName.getShortName();
+    return ru;
+  }
+
+  /**
    * Extend the existing ChangeAttribute with additional fields.
    *
    * @param a
@@ -89,10 +110,19 @@
   }
 
   public void addPatchSets(ChangeAttribute a, Collection<PatchSet> ps) {
+    addPatchSets(a, ps, null);
+  }
+
+  public void addPatchSets(ChangeAttribute ca, Collection<PatchSet> ps,
+      Map<PatchSet.Id,Collection<PatchSetApproval>> approvals) {
     if (!ps.isEmpty()) {
-      a.patchSets = new ArrayList<PatchSetAttribute>(ps.size());
+      ca.patchSets = new ArrayList<PatchSetAttribute>(ps.size());
       for (PatchSet p : ps) {
-        a.patchSets.add(asPatchSetAttribute(p));
+        PatchSetAttribute psa = asPatchSetAttribute(p);
+        if (approvals != null) {
+          addApprovals(psa, p.getId(), approvals);
+        }
+        ca.patchSets.add(psa);
       }
     }
   }
@@ -120,6 +150,14 @@
     return p;
   }
 
+  public void addApprovals(PatchSetAttribute p, PatchSet.Id id,
+      Map<PatchSet.Id,Collection<PatchSetApproval>> all) {
+    Collection<PatchSetApproval> list = all.get(id);
+    if (list != null) {
+      addApprovals(p, list);
+    }
+  }
+
   public void addApprovals(PatchSetAttribute p,
       Collection<PatchSetApproval> list) {
     if (!list.isEmpty()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdateAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdateAttribute.java
new file mode 100644
index 0000000..e4d715a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdateAttribute.java
@@ -0,0 +1,22 @@
+// 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.events;
+
+public class RefUpdateAttribute {
+  public String oldRev;
+  public String newRev;
+  public String refName;
+  public String project;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdatedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdatedEvent.java
new file mode 100644
index 0000000..f90bc81
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdatedEvent.java
@@ -0,0 +1,21 @@
+// 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.events;
+
+public class RefUpdatedEvent extends ChangeEvent {
+  public final String type = "ref-updated";
+  public AccountAttribute submitter;
+  public RefUpdateAttribute refUpdate;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitProjectImporter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitProjectImporter.java
index 676e4b6..33661ab 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitProjectImporter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitProjectImporter.java
@@ -102,6 +102,8 @@
         p.setSubmitType(SubmitType.MERGE_IF_NECESSARY);
         p.setUseContributorAgreements(false);
         p.setUseSignedOffBy(false);
+        p.setUseContentMerge(false);
+        p.setRequireChangeID(false);
         db.projects().insert(Collections.singleton(p));
 
       } else if (f.isDirectory()) {
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 e8df230..c644ca8 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
@@ -23,12 +23,12 @@
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.LockFile;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.RepositoryCache;
-import org.eclipse.jgit.lib.WindowCache;
-import org.eclipse.jgit.lib.WindowCacheConfig;
 import org.eclipse.jgit.lib.RepositoryCache.FileKey;
+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.util.FS;
 import org.eclipse.jgit.util.IO;
 import org.eclipse.jgit.util.RawParseUtils;
@@ -135,25 +135,29 @@
   public String getProjectDescription(final String name)
       throws RepositoryNotFoundException, IOException {
     final Repository e = openRepository(name);
-    final File d = new File(e.getDirectory(), "description");
-
-    String description;
     try {
-      description = RawParseUtils.decode(IO.readFully(d));
-    } catch (FileNotFoundException err) {
-      return null;
-    }
+      final File d = new File(e.getDirectory(), "description");
 
-    if (description != null) {
-      description = description.trim();
-      if (description.isEmpty()) {
-        description = null;
+      String description;
+      try {
+        description = RawParseUtils.decode(IO.readFully(d));
+      } catch (FileNotFoundException err) {
+        return null;
       }
-      if (UNNAMED.equals(description)) {
-        description = null;
+
+      if (description != null) {
+        description = description.trim();
+        if (description.isEmpty()) {
+          description = null;
+        }
+        if (UNNAMED.equals(description)) {
+          description = null;
+        }
       }
+      return description;
+    } finally {
+      e.close();
     }
-    return description;
   }
 
   public void setProjectDescription(final String name, final String description) {
@@ -164,21 +168,24 @@
       final LockFile f;
 
       e = openRepository(name);
-      f = new LockFile(new File(e.getDirectory(), "description"));
-      if (f.lock()) {
-        String d = description;
-        if (d != null) {
-          d = d.trim();
-          if (d.length() > 0) {
-            d += "\n";
+      try {
+        f = new LockFile(new File(e.getDirectory(), "description"), FS.DETECTED);
+        if (f.lock()) {
+          String d = description;
+          if (d != null) {
+            d = d.trim();
+            if (d.length() > 0) {
+              d += "\n";
+            }
+          } else {
+            d = "";
           }
-        } else {
-          d = "";
+          f.write(Constants.encode(d));
+          f.commit();
         }
-        f.write(Constants.encode(d));
-        f.commit();
+      } finally {
+        e.close();
       }
-      e.close();
     } catch (RepositoryNotFoundException e) {
       log.error("Cannot update description for " + name, e);
     } catch (IOException e) {
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 a5b739e..2efb1f4 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
@@ -56,9 +56,10 @@
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.AnyObjectId;
-import org.eclipse.jgit.lib.Commit;
+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.Ref;
 import org.eclipse.jgit.lib.RefUpdate;
@@ -76,6 +77,7 @@
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.io.UnsupportedEncodingException;
 import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -202,6 +204,9 @@
     try {
       mergeImpl();
     } finally {
+      if (rw != null) {
+        rw.release();
+      }
       if (db != null) {
         db.close();
       }
@@ -267,7 +272,7 @@
         branchTip = null;
       }
 
-      for (final Ref r : rw.getRepository().getAllRefs().values()) {
+      for (final Ref r : db.getAllRefs().values()) {
         if (r.getName().startsWith(Constants.R_HEADS)
             || r.getName().startsWith(Constants.R_TAGS)) {
           try {
@@ -431,7 +436,18 @@
   }
 
   private void mergeOneCommit(final CodeReviewCommit n) throws MergeException {
-    final Merger m = MergeStrategy.SIMPLE_TWO_WAY_IN_CORE.newMerger(db);
+    final ThreeWayMerger m;
+    if (destProject.isUseContentMerge()) {
+      // Settings for this project allow us to try and
+      // automatically resolve conflicts within files if needed.
+      // Use ResolveMerge and instruct to operate in core.
+      m = MergeStrategy.RESOLVE.newMerger(db, true);
+    } else {
+      // No auto conflict resolving allowed. If any of the
+      // affected files was modified, merge will fail.
+      m = MergeStrategy.SIMPLE_TWO_WAY_IN_CORE.newMerger(db);
+    }
+
     try {
       if (m.merge(new AnyObjectId[] {mergeTip, n})) {
         writeMergeCommit(m, n);
@@ -547,15 +563,14 @@
       authorIdent = myIdent;
     }
 
-    final Commit mergeCommit = new Commit(db);
+    final CommitBuilder mergeCommit = new CommitBuilder();
     mergeCommit.setTreeId(m.getResultTreeId());
-    mergeCommit.setParentIds(new ObjectId[] {mergeTip, n});
+    mergeCommit.setParentIds(mergeTip, n);
     mergeCommit.setAuthor(authorIdent);
     mergeCommit.setCommitter(myIdent);
     mergeCommit.setMessage(msgbuf.toString());
 
-    final ObjectId id = m.getObjectWriter().writeCommit(mergeCommit);
-    mergeTip = (CodeReviewCommit) rw.parseCommit(id);
+    mergeTip = (CodeReviewCommit) rw.parseCommit(commit(m, mergeCommit));
   }
 
   private void markCleanMerges() throws MergeException {
@@ -602,7 +617,17 @@
       final CodeReviewCommit n = toMerge.remove(0);
       final ThreeWayMerger m;
 
-      m = MergeStrategy.SIMPLE_TWO_WAY_IN_CORE.newMerger(db);
+      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(db, true);
+      } else {
+        // No auto conflict resolving allowed. If any of the
+        // affected files was modified, merge will fail.
+        m = MergeStrategy.SIMPLE_TWO_WAY_IN_CORE.newMerger(db);
+      }
+
       try {
         if (mergeTip == null) {
           // The branch is unborn. Take a fast-forward resolution to
@@ -789,14 +814,14 @@
       log.error("Can't read approval records for " + n.patchsetId, e);
     }
 
-    final Commit mergeCommit = new Commit(db);
+    final CommitBuilder mergeCommit = new CommitBuilder();
     mergeCommit.setTreeId(m.getResultTreeId());
-    mergeCommit.setParentIds(new ObjectId[] {mergeTip});
+    mergeCommit.setParentId(mergeTip);
     mergeCommit.setAuthor(n.getAuthorIdent());
     mergeCommit.setCommitter(toCommitterIdent(submitAudit));
     mergeCommit.setMessage(msgbuf.toString());
 
-    final ObjectId id = m.getObjectWriter().writeCommit(mergeCommit);
+    final ObjectId id = commit(m, mergeCommit);
     final CodeReviewCommit newCommit = (CodeReviewCommit) rw.parseCommit(id);
     newCommit.copyFrom(n);
     newCommit.statusCode = CommitMergeStatus.CLEAN_PICK;
@@ -806,6 +831,18 @@
     setRefLogIdent(submitAudit);
   }
 
+  private ObjectId commit(final Merger m, final CommitBuilder mergeCommit)
+      throws IOException, UnsupportedEncodingException {
+    ObjectInserter oi = m.getObjectInserter();
+    try {
+      ObjectId id = oi.insert(mergeCommit);
+      oi.flush();
+      return id;
+    } finally {
+      oi.release();
+    }
+  }
+
   private boolean contains(List<FooterLine> footers, FooterKey key, String val) {
     for (final FooterLine line : footers) {
       if (line.matches(key) && val.equals(line.getValue())) {
@@ -844,6 +881,13 @@
           case FAST_FORWARD:
             replication.scheduleUpdate(destBranch.getParentKey(), branchUpdate
                 .getName());
+
+            Account account = null;
+            final PatchSetApproval submitter = getSubmitter(mergeTip.patchsetId);
+            if (submitter != null) {
+              account = accountCache.get(submitter.getAccountId()).getAccount();
+            }
+            hooks.doRefUpdatedHook(destBranch, branchUpdate, account);
             break;
 
           default:
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/PushOp.java
index ac53830..1c100df 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/PushOp.java
@@ -78,6 +78,13 @@
 
   private Repository db;
 
+  /**
+   * It indicates if the current instance is in fact retrying to push.
+   */
+  private boolean retrying;
+
+  private boolean canceled;
+
   @Inject
   PushOp(final GitRepositoryManager grm, final SchemaFactory<ReviewDb> s,
       final PushReplication.ReplicationConfig p, final RemoteConfig c,
@@ -90,6 +97,22 @@
     uri = u;
   }
 
+  public boolean isRetrying() {
+    return retrying;
+  }
+
+  public void setToRetry() {
+    retrying = true;
+  }
+
+  public void cancel() {
+    canceled = true;
+  }
+
+  public boolean wasCanceled() {
+    return canceled;
+  }
+
   URIish getURI() {
     return uri;
   }
@@ -103,45 +126,73 @@
     }
   }
 
-  public void run() {
-    try {
-      // Lock the queue, and remove ourselves, so we can't be modified once
-      // we start replication (instead a new instance, with the same URI, is
-      // created and scheduled for a future point in time.)
-      //
-      pool.notifyStarting(this);
-      db = repoManager.openRepository(projectName.get());
-      runImpl();
-    } catch (RepositoryNotFoundException e) {
-      log.error("Cannot replicate " + projectName + "; " + e.getMessage());
+  public Set<String> getRefs() {
+    final Set<String> refs;
 
-    } catch (NoRemoteRepositoryException e) {
-      log.error("Cannot replicate to " + uri + "; repository not found");
+    if (mirror) {
+      refs = new HashSet<String>(1);
+      refs.add(MIRROR_ALL);
+    } else {
+      refs = delta;
+    }
 
-    } catch (NotSupportedException e) {
-      log.error("Cannot replicate to " + uri, e);
+    return refs;
+  }
 
-    } catch (TransportException e) {
-      final Throwable cause = e.getCause();
-      if (cause instanceof JSchException
-          && cause.getMessage().startsWith("UnknownHostKey:")) {
-        log.error("Cannot replicate to " + uri + ": " + cause.getMessage());
-      } else {
-        log.error("Cannot replicate to " + uri, e);
+  public void addRefs(Set<String> refs) {
+    if (!mirror) {
+      for (String ref : refs) {
+        addRef(ref);
       }
+    }
+  }
 
-    } catch (IOException e) {
-      log.error("Cannot replicate to " + uri, e);
+  public void run() {
+    // Lock the queue, and remove ourselves, so we can't be modified once
+    // we start replication (instead a new instance, with the same URI, is
+    // created and scheduled for a future point in time.)
+    //
+    pool.notifyStarting(this);
 
-    } catch (RuntimeException e) {
-      log.error("Unexpected error during replication to " + uri, e);
+    // It should only verify if it was canceled after calling notifyStarting,
+    // since the canceled flag would be set locking the queue.
+    if (!canceled) {
+      try {
+        db = repoManager.openRepository(projectName.get());
+        runImpl();
+      } catch (RepositoryNotFoundException e) {
+        log.error("Cannot replicate " + projectName + "; " + e.getMessage());
 
-    } catch (Error e) {
-      log.error("Unexpected error during replication to " + uri, e);
+      } catch (NoRemoteRepositoryException e) {
+        log.error("Cannot replicate to " + uri + "; repository not found");
 
-    } finally {
-      if (db != null) {
-        db.close();
+      } catch (NotSupportedException e) {
+        log.error("Cannot replicate to " + uri, e);
+
+      } catch (TransportException e) {
+        final Throwable cause = e.getCause();
+        if (cause instanceof JSchException
+            && cause.getMessage().startsWith("UnknownHostKey:")) {
+          log.error("Cannot replicate to " + uri + ": " + cause.getMessage());
+        } else {
+          log.error("Cannot replicate to " + uri, e);
+        }
+
+        // The remote push operation should be retried.
+        pool.reschedule(this);
+      } catch (IOException e) {
+        log.error("Cannot replicate to " + uri, e);
+
+      } catch (RuntimeException e) {
+        log.error("Unexpected error during replication to " + uri, e);
+
+      } catch (Error e) {
+        log.error("Unexpected error during replication to " + uri, e);
+
+      } finally {
+        if (db != null) {
+          db.close();
+        }
       }
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushReplication.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/PushReplication.java
index f26bf65..382a739 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushReplication.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/PushReplication.java
@@ -37,7 +37,7 @@
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.FileBasedConfig;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
 import org.eclipse.jgit.transport.OpenSshConfig;
 import org.eclipse.jgit.transport.RefSpec;
 import org.eclipse.jgit.transport.RemoteConfig;
@@ -55,7 +55,6 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -129,7 +128,8 @@
 
   private List<ReplicationConfig> allConfigs(final SitePaths site)
       throws ConfigInvalidException, IOException {
-    final FileBasedConfig cfg = new FileBasedConfig(site.replication_config);
+    final FileBasedConfig cfg =
+        new FileBasedConfig(site.replication_config, FS.DETECTED);
 
     if (!cfg.getFile().exists()) {
       log.warn("No " + cfg.getFile() + "; not replicating");
@@ -306,6 +306,7 @@
   static class ReplicationConfig {
     private final RemoteConfig remote;
     private final int delay;
+    private final int retryDelay;
     private final WorkQueue.Executor pool;
     private final Map<URIish, PushOp> pending = new HashMap<URIish, PushOp>();
     private final PushOp.Factory opFactory;
@@ -317,6 +318,7 @@
 
       remote = rc;
       delay = Math.max(0, getInt(rc, cfg, "replicationdelay", 15));
+      retryDelay = Math.max(0, getInt(rc, cfg, "replicationretry", 1));
 
       final int poolSize = Math.max(0, getInt(rc, cfg, "threads", 1));
       final String poolName = "ReplicateTo-" + rc.getName();
@@ -382,6 +384,96 @@
       }
     }
 
+    /**
+     * It schedules again a PushOp instance.
+     * <p>
+     * It is assumed to be previously scheduled and found a
+     * transport exception. It will schedule it as a push
+     * operation to be retried after the minutes count
+     * determined by class attribute retryDelay.
+     * <p>
+     * In case the PushOp instance to be scheduled has same
+     * URI than one also pending for retry, it adds to the one
+     * pending the refs list of the parameter instance.
+     * <p>
+     * In case the PushOp instance to be scheduled has same
+     * URI than one pending, but not pending for retry, it
+     * indicates the one pending should be canceled when it
+     * starts executing, removes it from pending list, and
+     * adds its refs to the parameter instance. The parameter
+     * instance is scheduled for retry.
+     * <p>
+     * Notice all operations to indicate a PushOp should be
+     * canceled, or it is retrying, or remove/add it from/to
+     * pending Map should be protected by the lock on pending
+     * Map class instance attribute.
+     *
+     * @param pushOp The PushOp instance to be scheduled.
+     */
+    void reschedule(final PushOp pushOp) {
+      try {
+        if (!controlFor(pushOp.getProjectNameKey()).isVisible()) {
+          return;
+        }
+      } catch (NoSuchProjectException e1) {
+        log.error("Internal error: project " + pushOp.getProjectNameKey()
+            + " not found during replication");
+        return;
+      }
+
+      // It locks access to pending variable.
+      synchronized (pending) {
+        PushOp pendingPushOp = pending.get(pushOp.getURI());
+
+        if (pendingPushOp != null) {
+          // There is one PushOp instance already pending to same URI.
+
+          if (pendingPushOp.isRetrying()) {
+            // The one pending is one already retrying, so it should
+            // maintain it and add to it the refs of the one passed
+            // as parameter to the method.
+
+            // This scenario would happen if a PushOp has started running
+            // and then before it failed due transport exception, another
+            // one to same URI started. The first one would fail and would
+            // be rescheduled, being present in pending list. When the
+            // second one fails, it will also be rescheduled and then,
+            // here, find out replication to its URI is already pending
+            // for retry (blocking).
+            pendingPushOp.addRefs(pushOp.getRefs());
+
+          } else {
+            // The one pending is one that is NOT retrying, it was just
+            // scheduled believing no problem would happen. The one pending
+            // should be canceled, and this is done by setting its canceled
+            // flag, removing it from pending list, and adding its refs to
+            // the pushOp instance that should then, later, in this method,
+            // be scheduled for retry.
+
+            // Notice that the PushOp found pending will start running and,
+            // when notifying it is starting (with pending lock protection),
+            // it will see it was canceled and then it will do nothing with
+            // pending list and it will not execute its run implementation.
+
+            pendingPushOp.cancel();
+            pending.remove(pendingPushOp);
+
+            pushOp.addRefs(pendingPushOp.getRefs());
+          }
+        }
+
+        if (pendingPushOp == null || !pendingPushOp.isRetrying()) {
+          // The PushOp method param instance should be scheduled for retry.
+          // Remember when retrying it should be used different delay.
+
+          pushOp.setToRetry();
+
+          pending.put(pushOp.getURI(), pushOp);
+          pool.schedule(pushOp, retryDelay, TimeUnit.MINUTES);
+        }
+      }
+    }
+
     ProjectControl controlFor(final Project.NameKey project)
         throws NoSuchProjectException {
       return projectControlFactory.controlFor(project);
@@ -389,7 +481,9 @@
 
     void notifyStarting(final PushOp op) {
       synchronized (pending) {
-        pending.remove(op.getURI());
+        if (!op.wasCanceled()) {
+          pending.remove(op.getURI());
+        }
       }
     }
 
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 a3140ec..8b6f397 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
@@ -55,9 +55,12 @@
 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.Constants;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefUpdate;
@@ -225,11 +228,33 @@
 
   /** Determine if the user can upload commits. */
   public Capable canUpload() {
-    if (!projectControl.canUploadToAtLeastOneRef()) {
+    if (!projectControl.canPushToAtLeastOneRef()) {
       String reqName = project.getName();
       return new Capable("Upload denied for project '" + reqName + "'");
     }
 
+    // Don't permit receive-pack to be executed if a refs/for/branch_name
+    // reference exists in the destination repository. These block the
+    // client from being able to even send us a pack file, as it is very
+    // unlikely the user passed the --force flag and the new commit is
+    // probably not going to fast-forward the branch.
+    //
+    Map<String, Ref> blockingFors;
+    try {
+      blockingFors = repo.getRefDatabase().getRefs("refs/for/");
+    } catch (IOException err) {
+      String projName = project.getName();
+      log.warn("Cannot scan refs in '" + projName + "'", err);
+      return new Capable("Server process cannot read '" + projName + "'");
+    }
+    if (!blockingFors.isEmpty()) {
+      String projName = project.getName();
+      log.error("Repository '" + projName
+          + "' needs the following refs removed to receive changes: "
+          + blockingFors.keySet());
+      return new Capable("One or more refs/for/ names blocks change upload");
+    }
+
     if (project.isUseContributorAgreements()) {
       try {
         return verifyActiveContributorAgreement();
@@ -277,6 +302,8 @@
           // Change refs are scheduled when they are created.
           //
           replication.scheduleUpdate(project.getNameKey(), c.getRefName());
+          Branch.NameKey destBranch = new Branch.NameKey(project.getNameKey(), c.getRefName());
+          hooks.doRefUpdatedHook(destBranch, c.getOldId(), c.getNewId(), currentUser.getAccount());
         }
       }
     }
@@ -537,18 +564,11 @@
   }
 
   private void parseRewind(final ReceiveCommand cmd) {
-    final RevObject oldObject, newObject;
+    RevCommit newObject;
     try {
-      oldObject = rp.getRevWalk().parseAny(cmd.getOldId());
-    } catch (IOException err) {
-      log.error("Invalid object " + cmd.getOldId().name() + " for "
-          + cmd.getRefName() + " forced update", err);
-      reject(cmd, "invalid object");
-      return;
-    }
-
-    try {
-      newObject = rp.getRevWalk().parseAny(cmd.getNewId());
+      newObject = rp.getRevWalk().parseCommit(cmd.getNewId());
+    } catch (IncorrectObjectTypeException notCommit) {
+      newObject = null;
     } catch (IOException err) {
       log.error("Invalid object " + cmd.getNewId().name() + " for "
           + cmd.getRefName() + " forced update", err);
@@ -557,9 +577,14 @@
     }
 
     RefControl ctl = projectControl.controlForRef(cmd.getRefName());
-    if (oldObject instanceof RevCommit && newObject instanceof RevCommit
-        && ctl.canForceUpdate()) {
+    if (newObject != null) {
       validateNewCommits(ctl, cmd);
+      if (cmd.getResult() != ReceiveCommand.Result.NOT_ATTEMPTED) {
+        return;
+      }
+    }
+
+    if (ctl.canForceUpdate()) {
       // Let the core receive process handle it
     } else {
       cmd.setResult(ReceiveCommand.Result.REJECTED_NONFASTFORWARD);
@@ -1061,19 +1086,30 @@
         if (priorPatchSet.equals(ps.getId()) && c.getTree() == prior.getTree()) {
           rp.getRevWalk().parseBody(prior);
           final boolean messageEq =
-              c.getFullMessage().equals(prior.getFullMessage());
+              eq(c.getFullMessage(), prior.getFullMessage());
           final boolean parentsEq = parentsEqual(c, prior);
+          final boolean authorEq = authorEqual(c, prior);
 
-          if (messageEq && parentsEq) {
+          if (messageEq && parentsEq && authorEq) {
             reject(request.cmd, "no changes made");
             return null;
           } else {
-            rp.sendMessage("(W) " + c.abbreviate(repo, 6).name() + ":" //
-                + " no files changed, but" //
-                + (!messageEq ? " message updated" : "") //
-                + (!messageEq && !parentsEq ? " and" : "") //
-                + (!parentsEq ? " was rebased" : "") //
-            );
+            ObjectReader reader = rp.getRevWalk().getObjectReader();
+            StringBuilder msg = new StringBuilder();
+            msg.append("(W) ");
+            msg.append(reader.abbreviate(c).name());
+            msg.append(":");
+            msg.append(" no files changed");
+            if (!authorEq) {
+              msg.append(", author changed");
+            }
+            if (!messageEq) {
+              msg.append(", message updated");
+            }
+            if (!parentsEq) {
+              msg.append(", was rebased");
+            }
+            rp.sendMessage(msg.toString());
           }
         }
       } catch (IOException e) {
@@ -1256,6 +1292,30 @@
     return true;
   }
 
+  static boolean authorEqual(RevCommit a, RevCommit b) {
+    PersonIdent aAuthor = a.getAuthorIdent();
+    PersonIdent bAuthor = b.getAuthorIdent();
+
+    if (aAuthor == null && bAuthor == null) {
+      return true;
+    } else if (aAuthor == null || bAuthor == null) {
+      return false;
+    }
+
+    return eq(aAuthor.getName(), bAuthor.getName())
+        && eq(aAuthor.getEmailAddress(), bAuthor.getEmailAddress());
+  }
+
+  static boolean eq(String a, String b) {
+    if (a == null && b == null) {
+      return true;
+    } else if (a == null || b == null) {
+      return false;
+    } else {
+      return a.equals(b);
+    }
+  }
+
   private void insertDummyApproval(final ReplaceResult result,
       final Account.Id forAccount, final ApprovalCategory.Id catId,
       final ReviewDb db) throws OrmException {
@@ -1402,12 +1462,38 @@
       }
     }
 
+    if (project.isRequireChangeID()) {
+      final List<String> idList = c.getFooterLines(CHANGE_ID);
+      if (idList.isEmpty()) {
+        reject(cmd, "missing Change-Id in commit message ");
+        return false;
+      }
+
+      if (idList.size() > 1) {
+        reject(cmd, "multiple Change-Id lines in commit message ");
+        return false;
+      }
+
+      final String v = idList.get(idList.size() - 1).trim();
+      if (!v.matches("^I[0-9a-f]{8,}.*$")) {
+        reject(cmd, "invalid Change-Id line format in commit message ");
+        return false;
+      }
+    }
+
     return true;
   }
 
   private void warnMalformedMessage(RevCommit c) {
+    ObjectReader reader = rp.getRevWalk().getObjectReader();
     if (65 < c.getShortMessage().length()) {
-      rp.sendMessage("(W) " + c.abbreviate(repo, 6).name()
+      AbbreviatedObjectId id;
+      try {
+        id = reader.abbreviate(c);
+      } catch (IOException err) {
+        id = c.abbreviate(6);
+      }
+      rp.sendMessage("(W) " + id.name() //
           + ": commit subject >65 characters; use shorter first paragraph");
     }
 
@@ -1422,7 +1508,13 @@
     }
 
     if (0 < longLineCnt && 33 < longLineCnt * 100 / nonEmptyCnt) {
-      rp.sendMessage("(W) " + c.abbreviate(repo, 6).name()
+      AbbreviatedObjectId id;
+      try {
+        id = reader.abbreviate(c);
+      } catch (IOException err) {
+        id = c.abbreviate(6);
+      }
+      rp.sendMessage("(W) " + id.name() //
           + ": commit message lines >70 characters; manually wrap lines");
     }
   }
@@ -1462,10 +1554,6 @@
         final PatchSet.Id psi = doReplace(req);
         if (psi != null) {
           closeChange(req.cmd, psi, req.newCommit);
-        } else {
-          log.warn("Replacement of Change-Id " + req.ontoChange
-              + " with commit " + req.newCommit.name()
-              + " did not import the new patch set.");
         }
       }
     } catch (IOException e) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/TransferConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/TransferConfig.java
similarity index 78%
rename from gerrit-sshd/src/main/java/com/google/gerrit/sshd/TransferConfig.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/git/TransferConfig.java
index c976e7f..de4130d 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/TransferConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/TransferConfig.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.sshd;
+package com.google.gerrit.server.git;
 
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -20,21 +20,32 @@
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.storage.pack.PackConfig;
 
 import java.util.concurrent.TimeUnit;
 
 @Singleton
 public class TransferConfig {
   private final int timeout;
+  private final PackConfig packConfig;
 
   @Inject
   TransferConfig(@GerritServerConfig final Config cfg) {
     timeout = (int) ConfigUtil.getTimeUnit(cfg, "transfer", null, "timeout", //
         0, TimeUnit.SECONDS);
+
+    packConfig = new PackConfig();
+    packConfig.setDeltaCompress(false);
+    packConfig.setThreads(1);
+    packConfig.fromConfig(cfg);
   }
 
   /** @return configured timeout, in seconds. 0 if the timeout is infinite. */
   public int getTimeout() {
     return timeout;
   }
+
+  public PackConfig getPackConfig() {
+    return packConfig;
+  }
 }
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 05da2be..ffc3fd8 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
@@ -118,16 +118,20 @@
   private void addVisibleTags(final Map<String, Ref> result,
       final List<Ref> tags) {
     final RevWalk rw = new RevWalk(db);
-    final RevFlag VISIBLE = rw.newFlag("VISIBLE");
-    final List<RevCommit> starts;
+    try {
+      final RevFlag VISIBLE = rw.newFlag("VISIBLE");
+      final List<RevCommit> starts;
 
-    rw.carry(VISIBLE);
-    starts = lookupVisibleCommits(result, rw, VISIBLE);
+      rw.carry(VISIBLE);
+      starts = lookupVisibleCommits(result, rw, VISIBLE);
 
-    for (Ref tag : tags) {
-      if (isTagVisible(rw, VISIBLE, starts, tag)) {
-        result.put(tag.getName(), tag);
+      for (Ref tag : tags) {
+        if (isTagVisible(rw, VISIBLE, starts, tag)) {
+          result.put(tag.getName(), tag);
+        }
       }
+    } finally {
+      rw.release();
     }
   }
 
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 d2b5c29..e679849 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
@@ -30,7 +30,7 @@
   }
 
   @Override
-  protected void init() {
+  protected void init() throws EmailException {
     super.init();
 
     ccAllApprovals();
@@ -39,10 +39,7 @@
   }
 
   @Override
-  protected void formatChange() {
-    appendText(getNameFor(fromId));
-    appendText(" has abandoned change " + change.getKey().abbreviate() + ":\n");
-    appendText("\n");
-    formatCoverLetter();
+  protected void formatChange() throws EmailException {
+    appendText(velocifyFile("Abandoned.vm"));
   }
 }
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 be62ba0..e5437cf 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
@@ -32,7 +32,7 @@
   }
 
   @Override
-  protected void init() {
+  protected void init() throws EmailException {
     super.init();
 
     ccExistingReviewers();
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 b3ddaeb..71712af 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
@@ -54,7 +54,6 @@
 
   private ProjectState projectState;
   protected ChangeData changeData;
-  private boolean inFooter;
 
   protected ChangeEmail(EmailArguments ea, final Change c, final String mc) {
     super(ea, mc);
@@ -76,31 +75,9 @@
   }
 
   /** Format the message body by calling {@link #appendText(String)}. */
-  protected void format() {
+  protected void format() throws EmailException {
     formatChange();
-    if (getChangeUrl() != null) {
-      openFooter();
-      appendText("To view visit ");
-      appendText(getChangeUrl());
-      appendText("\n");
-    }
-    if (getSettingsUrl() != null) {
-      openFooter();
-      appendText("To unsubscribe, visit ");
-      appendText(getSettingsUrl());
-      appendText("\n");
-    }
-
-    if (inFooter) {
-      appendText("\n");
-    } else {
-      openFooter();
-    }
-    appendText("Gerrit-MessageType: " + messageClass + "\n");
-    appendText("Gerrit-Project: " + projectName + "\n");
-    appendText("Gerrit-Branch: " + change.getDest().getShortName() + "\n");
-    appendText("Gerrit-Owner: " + getNameEmailFor(change.getOwner()) + "\n");
-
+    appendText(velocifyFile("ChangeFooter.vm"));
     try {
       HashSet<Account.Id> reviewers = new HashSet<Account.Id>();
       for (PatchSetApproval p : args.db.get().patchSetApprovals().byChange(
@@ -121,11 +98,10 @@
   }
 
   /** Format the message body by calling {@link #appendText(String)}. */
-  protected abstract void formatChange();
+  protected abstract void formatChange() throws EmailException;
 
   /** Setup the message headers and envelope (TO, CC, BCC). */
-  protected void init() {
-    super.init();
+  protected void init() throws EmailException {
     if (args.projectCache != null) {
       projectState = args.projectCache.get(change.getProject());
       projectName =
@@ -151,6 +127,8 @@
       }
     }
 
+    super.init();
+
     if (changeMessage != null && changeMessage.getWrittenOn() != null) {
       setHeader("Date", new Date(changeMessage.getWrittenOn().getTime()));
     }
@@ -159,27 +137,21 @@
     setListIdHeader();
     setChangeUrlHeader();
     setCommitIdHeader();
-
-    inFooter = false;
   }
 
-  private void setListIdHeader() {
+  private void setListIdHeader() throws EmailException {
     // Set a reasonable list id so that filters can be used to sort messages
-    //
-    final StringBuilder listid = new StringBuilder();
-    listid.append("gerrit-");
-    listid.append(projectName.replace('/', '-'));
-    listid.append("@");
-    listid.append(getGerritHost());
-
-    final String listidStr = listid.toString();
-    setHeader("Mailing-List", "list " + listidStr);
-    setHeader("List-Id", "<" + listidStr.replace('@', '.') + ">");
+    setVHeader("Mailing-List", "list $email.listId");
+    setVHeader("List-Id", "<$email.listId.replace('@', '.')>");
     if (getSettingsUrl() != null) {
-      setHeader("List-Unsubscribe", "<" + getSettingsUrl() + ">");
+      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) {
@@ -195,27 +167,12 @@
     }
   }
 
-  private void setChangeSubjectHeader() {
-    final StringBuilder subj = new StringBuilder();
-    subj.append("[");
-    subj.append(change.getDest().getShortName());
-    subj.append("] ");
-    subj.append("Change ");
-    subj.append(change.getKey().abbreviate());
-    subj.append(": (");
-    subj.append(projectName);
-    subj.append(") ");
-    if (change.getSubject().length() > 60) {
-      subj.append(change.getSubject().substring(0, 60));
-      subj.append("...");
-    } else {
-      subj.append(change.getSubject());
-    }
-    setHeader("Subject", subj.toString());
+  private void setChangeSubjectHeader() throws EmailException {
+    setHeader("Subject", velocifyFile("ChangeSubject.vm"));
   }
 
   /** Get a link to the change; null if the server doesn't know its own address. */
-  protected String getChangeUrl() {
+  public String getChangeUrl() {
     if (change != null && getGerritUrl() != null) {
       final StringBuilder r = new StringBuilder();
       r.append(getGerritUrl());
@@ -225,25 +182,9 @@
     return null;
   }
 
-  protected String getChangeMessageThreadId() {
-    final StringBuilder r = new StringBuilder();
-    r.append('<');
-    r.append("gerrit");
-    r.append('.');
-    r.append(change.getCreatedOn().getTime());
-    r.append('.');
-    r.append(change.getKey().get());
-    r.append('@');
-    r.append(getGerritHost());
-    r.append('>');
-    return r.toString();
-  }
-
-  private void openFooter() {
-    if (!inFooter) {
-      inFooter = true;
-      appendText("-- \n");
-    }
+  public String getChangeMessageThreadId() throws EmailException {
+    return velocify("<gerrit.${change.createdOn.time}.$change.key.get()" +
+                    "@$email.gerritHost>");
   }
 
   /** Format the sender's "cover letter", {@link #getCoverLetter()}. */
@@ -256,7 +197,7 @@
   }
 
   /** Get the text of the "cover letter", from {@link ChangeMessage}. */
-  protected String getCoverLetter() {
+  public String getCoverLetter() {
     if (changeMessage != null) {
       final String txt = changeMessage.getMessage();
       if (txt != null) {
@@ -268,34 +209,41 @@
 
   /** Format the change message and the affected file list. */
   protected void formatChangeDetail() {
+    appendText(getChangeDetail());
+  }
+
+  /** Create the change message and the affected file list. */
+  public String getChangeDetail() {
+    StringBuilder detail = new StringBuilder();
+
     if (patchSetInfo != null) {
-      appendText(patchSetInfo.getMessage().trim());
-      appendText("\n");
+      detail.append(patchSetInfo.getMessage().trim() + "\n");
     } else {
-      appendText(change.getSubject().trim());
-      appendText("\n");
+      detail.append(change.getSubject().trim() + "\n");
     }
 
     if (patchSet != null) {
-      appendText("---\n");
+      detail.append("---\n");
       PatchList patchList = getPatchList();
       for (PatchListEntry p : patchList.getPatches()) {
         if (Patch.COMMIT_MSG.equals(p.getNewName())) {
           continue;
         }
-        appendText(p.getChangeType().getCode() + " " + p.getNewName() + "\n");
+        detail.append(p.getChangeType().getCode() + " " + p.getNewName() + "\n");
       }
-      appendText(MessageFormat.format("" //
+      detail.append(MessageFormat.format("" //
           + "{0,choice,0#0 files|1#1 file|1<{0} files} changed, " //
           + "{1,choice,0#0 insertions|1#1 insertion|1<{1} insertions}(+), " //
           + "{2,choice,0#0 deletions|1#1 deletion|1<{2} deletions}(-)" //
           + "\n", patchList.getPatches().size() - 1, //
           patchList.getInsertions(), //
           patchList.getDeletions()));
-      appendText("\n");
+      detail.append("\n");
     }
+    return detail.toString();
   }
 
+
   /** Get the patch list corresponding to this patch set. */
   protected PatchList getPatchList() {
     if (patchSet != null) {
@@ -441,4 +389,17 @@
         || projectState.controlFor(args.identifiedUserFactory.create(to))
             .controlFor(change).isVisible();
   }
+
+  @Override
+  protected void setupVelocityContext() {
+    super.setupVelocityContext();
+    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", projectName);
+    velocityContext.put("patchSet", patchSet);
+    velocityContext.put("patchSetInfo", patchSetInfo);
+  }
 }
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 e501a3e..0425121 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
@@ -61,7 +61,7 @@
   }
 
   @Override
-  protected void init() {
+  protected void init() throws EmailException {
     super.init();
 
     ccAllApprovals();
@@ -70,20 +70,13 @@
   }
 
   @Override
-  protected void formatChange() {
-    if (!"".equals(getCoverLetter()) || !inlineComments.isEmpty()) {
-      appendText("Comments on Patch Set " + patchSet.getPatchSetId() + ":\n");
-      appendText("\n");
-      formatCoverLetter();
-      formatInlineComments();
-      if (getChangeUrl() != null) {
-        appendText("To respond, visit " + getChangeUrl() + "\n");
-        appendText("\n");
-      }
-    }
+  public void formatChange() throws EmailException {
+    appendText(velocifyFile("Comment.vm"));
   }
 
-  private void formatInlineComments() {
+  public String getInlineComments() {
+    StringBuilder  cmts = new StringBuilder();
+
     final Repository repo = getRepository();
     try {
       final PatchList patchList = repo != null ? getPatchList() : null;
@@ -96,13 +89,13 @@
         final short side = c.getSide();
 
         if (!pk.equals(currentFileKey)) {
-          appendText("....................................................\n");
+          cmts.append("....................................................\n");
           if (Patch.COMMIT_MSG.equals(pk.get())) {
-            appendText("Commit Message\n");
+            cmts.append("Commit Message\n");
           } else {
-            appendText("File ");
-            appendText(pk.get());
-            appendText("\n");
+            cmts.append("File ");
+            cmts.append(pk.get());
+            cmts.append("\n");
           }
           currentFileKey = pk;
 
@@ -118,26 +111,27 @@
           }
         }
 
-        appendText("Line " + lineNbr);
+        cmts.append("Line " + lineNbr);
         if (currentFileData != null) {
           try {
             final String lineStr = currentFileData.getLine(side, lineNbr);
-            appendText(": ");
-            appendText(lineStr);
+            cmts.append(": ");
+            cmts.append(lineStr);
           } catch (Throwable cce) {
             // Don't quote the line if we can't safely convert it.
           }
         }
-        appendText("\n");
+        cmts.append("\n");
 
-        appendText(c.getMessage().trim());
-        appendText("\n\n");
+        cmts.append(c.getMessage().trim());
+        cmts.append("\n\n");
       }
     } finally {
       if (repo != null) {
         repo.close();
       }
     }
+    return cmts.toString();
   }
 
   private Repository getRepository() {
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 ea57cde..18bfe976 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
@@ -40,7 +40,7 @@
   }
 
   @Override
-  protected void init() {
+  protected void init() throws EmailException {
     super.init();
 
     bccWatchers();
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 8a7ffd6..f2ab9fa 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.server.IdentifiedUser.GenericFactory;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.config.WildProjectName;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.patch.PatchListCache;
@@ -47,6 +48,7 @@
   final ChangeQueryBuilder.Factory queryBuilder;
   final Provider<ChangeQueryRewriter> queryRewriter;
   final Provider<ReviewDb> db;
+  final SitePaths site;
 
   @Inject
   EmailArguments(GitRepositoryManager server, ProjectCache projectCache,
@@ -57,7 +59,8 @@
       @CanonicalWebUrl @Nullable Provider<String> urlProvider,
       @WildProjectName Project.NameKey wildProject,
       ChangeQueryBuilder.Factory queryBuilder,
-      Provider<ChangeQueryRewriter> queryRewriter, Provider<ReviewDb> db) {
+      Provider<ChangeQueryRewriter> queryRewriter, Provider<ReviewDb> db,
+      SitePaths site) {
     this.server = server;
     this.projectCache = projectCache;
     this.accountCache = accountCache;
@@ -71,5 +74,6 @@
     this.queryBuilder = queryBuilder;
     this.queryRewriter = queryRewriter;
     this.db = db;
+    this.site = site;
   }
 }
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 00750ef..19fb48b 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
@@ -26,27 +26,18 @@
 
   @Inject
   public MergeFailSender(EmailArguments ea, @Assisted Change c) {
-    super(ea, c, "comment");
+    super(ea, c, "merge-failed");
   }
 
   @Override
-  protected void init() {
+  protected void init() throws EmailException {
     super.init();
 
     ccExistingReviewers();
   }
 
   @Override
-  protected void formatChange() {
-    appendText("Change " + change.getKey().abbreviate());
-    if (patchSetInfo != null && patchSetInfo.getAuthor() != null
-        && patchSetInfo.getAuthor().getName() != null) {
-      appendText(" by ");
-      appendText(patchSetInfo.getAuthor().getName());
-    }
-    appendText(" FAILED to submit to ");
-    appendText(change.getDest().getShortName());
-    appendText(".\n\n");
-    formatCoverLetter();
+  protected void formatChange() throws EmailException {
+    appendText(velocifyFile("MergeFail.vm"));
   }
 }
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 caf19e4..40f4790 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
@@ -51,7 +51,7 @@
   }
 
   @Override
-  protected void init() {
+  protected void init() throws EmailException {
     super.init();
 
     ccAllApprovals();
@@ -61,58 +61,47 @@
   }
 
   @Override
-  protected void formatChange() {
-    appendText("Change " + change.getKey().abbreviate());
-    if (patchSetInfo != null && patchSetInfo.getAuthor() != null
-        && patchSetInfo.getAuthor().getName() != null) {
-      appendText(" by ");
-      appendText(patchSetInfo.getAuthor().getName());
-    }
-    appendText(" submitted to ");
-    appendText(dest.getShortName());
-    appendText(":\n\n");
-    formatChangeDetail();
-    formatApprovals();
+  protected void formatChange() throws EmailException {
+    appendText(velocifyFile("Merged.vm"));
   }
 
-  private void formatApprovals() {
-    if (patchSet != null) {
-      try {
-        final Map<Account.Id, Map<ApprovalCategory.Id, PatchSetApproval>> pos =
-            new HashMap<Account.Id, Map<ApprovalCategory.Id, PatchSetApproval>>();
+  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>>();
+      final Map<Account.Id, Map<ApprovalCategory.Id, PatchSetApproval>> neg =
+          new HashMap<Account.Id, Map<ApprovalCategory.Id, PatchSetApproval>>();
 
-        for (PatchSetApproval ca : args.db.get().patchSetApprovals()
-            .byPatchSet(patchSet.getId())) {
-          if (ca.getValue() > 0) {
-            insert(pos, ca);
-          } else if (ca.getValue() < 0) {
-            insert(neg, ca);
-          }
+      for (PatchSetApproval ca : args.db.get().patchSetApprovals()
+          .byPatchSet(patchSet.getId())) {
+        if (ca.getValue() > 0) {
+          insert(pos, ca);
+        } else if (ca.getValue() < 0) {
+          insert(neg, ca);
         }
-
-        format("Approvals", pos);
-        format("Objections", neg);
-      } catch (OrmException err) {
-        // Don't list the approvals
       }
+
+      return format("Approvals", pos) + format("Objections", neg);
+    } catch (OrmException err) {
+      // Don't list the approvals
     }
+    return "";
   }
 
-  private void format(final String type,
+  private String format(final String type,
       final Map<Account.Id, Map<ApprovalCategory.Id, PatchSetApproval>> list) {
+    StringBuilder txt = new StringBuilder();
     if (list.isEmpty()) {
-      return;
+      return "";
     }
-    appendText(type + ":\n");
+    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();
-      appendText("  ");
-      appendText(getNameFor(ent.getKey()));
-      appendText(": ");
+      txt.append("  ");
+      txt.append(getNameFor(ent.getKey()));
+      txt.append(": ");
       boolean first = true;
       for (ApprovalType at : approvalTypes.getApprovalTypes()) {
         final PatchSetApproval ca = l.get(at.getCategory().getId());
@@ -123,24 +112,25 @@
         if (first) {
           first = false;
         } else {
-          appendText("; ");
+          txt.append("; ");
         }
 
         final ApprovalCategoryValue v = at.getValue(ca);
         if (v != null) {
-          appendText(v.getName());
+          txt.append(v.getName());
         } else {
-          appendText(at.getCategory().getName());
-          appendText("=");
+          txt.append(at.getCategory().getName());
+          txt.append("=");
           if (ca.getValue() > 0) {
-            appendText("+");
+            txt.append("+");
           }
-          appendText("" + ca.getValue());
+          txt.append("" + ca.getValue());
         }
       }
-      appendText("\n");
+      txt.append("\n");
     }
-    appendText("\n");
+    txt.append("\n");
+    return txt.toString();
   }
 
   private void insert(
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 dc8c2c2..9e78cab 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
@@ -20,6 +20,7 @@
 
 import com.jcraft.jsch.HostKey;
 
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -46,7 +47,7 @@
   }
 
   @Override
-  protected void init() {
+  protected void init() throws EmailException {
     super.init();
 
     setHeader("Message-ID", getChangeMessageThreadId());
@@ -57,74 +58,19 @@
   }
 
   @Override
-  protected void formatChange() {
-    formatSalutation();
-    formatChangeDetail();
-
-    appendText("\n");
-    appendText("  " + getPullUrl() + "\n");
+  protected void formatChange() throws EmailException {
+    appendText(velocifyFile("NewChange.vm"));
   }
 
-  private void formatSalutation() {
-    final String changeUrl = getChangeUrl();
-
+  public List<String> getReviewerNames() {
     if (reviewers.isEmpty()) {
-      formatDest();
-      if (changeUrl != null) {
-        appendText("\n");
-        appendText("    " + changeUrl + "\n");
-        appendText("\n");
-      }
-      appendText("\n");
-
-    } else {
-      appendText("Hello");
-      for (final Iterator<Account.Id> i = reviewers.iterator(); i.hasNext();) {
-        appendText(" ");
-        appendText(getNameFor(i.next()));
-        appendText(",");
-      }
-      appendText("\n");
-      appendText("\n");
-
-      appendText("I'd like you to do a code review.");
-      if (changeUrl != null) {
-        appendText("  Please visit\n");
-        appendText("\n");
-        appendText("    " + changeUrl + "\n");
-        appendText("\n");
-        appendText("to review the following change:\n");
-      }
-      appendText("\n");
-
-      formatDest();
-      appendText("\n");
+      return null;
     }
-  }
-
-  private void formatDest() {
-    appendText("Change " + change.getKey().abbreviate());
-    appendText(" for ");
-    appendText(change.getDest().getShortName());
-    appendText(" in ");
-    appendText(projectName);
-    appendText(":\n");
-  }
-
-  private String getPullUrl() {
-    final String host = getSshHost();
-    if (host == null) {
-      return "";
+    List<String> names = new ArrayList<String>();
+    for (Account.Id id : reviewers) {
+      names.add(getNameFor(id));
     }
-
-    final StringBuilder r = new StringBuilder();
-    r.append("git pull ssh://");
-    r.append(host);
-    r.append("/");
-    r.append(projectName);
-    r.append(" ");
-    r.append(patchSet.getRefName());
-    return r.toString();
+    return names;
   }
 
   public String getSshHost() {
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 2136c65..02e5000 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
@@ -19,10 +19,15 @@
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.mail.EmailHeader.AddressList;
 
+import org.apache.commons.lang.StringUtils;
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.app.Velocity;
+import org.apache.velocity.exception.ResourceNotFoundException;
 import org.eclipse.jgit.util.SystemReader;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.StringWriter;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.util.ArrayList;
@@ -47,6 +52,7 @@
   private final List<Address> smtpRcptTo = new ArrayList<Address>();
   private Address smtpFromAddress;
   private StringBuilder body;
+  protected VelocityContext velocityContext;
 
   protected final EmailArguments args;
   protected Account.Id fromId;
@@ -113,10 +119,12 @@
   }
 
   /** Format the message body by calling {@link #appendText(String)}. */
-  protected abstract void format();
+  protected abstract void format() throws EmailException;
 
   /** Setup the message headers and envelope (TO, CC, BCC). */
-  protected void init() {
+  protected void init() throws EmailException {
+    setupVelocityContext();
+
     smtpFromAddress = args.fromAddressGenerator.from(fromId);
     setHeader("Date", new Date());
     headers.put("From", new EmailHeader.AddressList(smtpFromAddress));
@@ -140,25 +148,31 @@
     body = new StringBuilder();
 
     if (fromId != null && args.fromAddressGenerator.isGenericAddress(fromId)) {
-      final Account account = args.accountCache.get(fromId).getAccount();
-      final String name = account.getFullName();
-      final String email = account.getPreferredEmail();
-
-      if ((name != null && !name.isEmpty())
-          || (email != null && !email.isEmpty())) {
-        body.append("From");
-        if (name != null && !name.isEmpty()) {
-          body.append(" ").append(name);
-        }
-        if (email != null && !email.isEmpty()) {
-          body.append(" <").append(email).append(">");
-        }
-        body.append(":\n\n");
-      }
+      appendText(getFromLine());
     }
   }
 
-  protected String getGerritHost() {
+  protected String getFromLine() {
+    final Account account = args.accountCache.get(fromId).getAccount();
+    final String name = account.getFullName();
+    final String email = account.getPreferredEmail();
+    StringBuilder f = new StringBuilder();
+
+    if ((name != null && !name.isEmpty())
+        || (email != null && !email.isEmpty())) {
+      f.append("From");
+      if (name != null && !name.isEmpty()) {
+        f.append(" ").append(name);
+      }
+      if (email != null && !email.isEmpty()) {
+        f.append(" <").append(email).append(">");
+      }
+      f.append(":\n\n");
+    }
+    return f.toString();
+  }
+
+  public String getGerritHost() {
     if (getGerritUrl() != null) {
       try {
         return new URL(getGerritUrl()).getHost();
@@ -184,10 +198,16 @@
     return null;
   }
 
-  protected String getGerritUrl() {
+  public String getGerritUrl() {
     return args.urlProvider.get();
   }
 
+  /** Set a header in the outgoing message using a template. */
+  protected void setVHeader(final String name, final String value) throws
+      EmailException {
+    setHeader(name, velocify(value));
+  }
+
   /** Set a header in the outgoing message. */
   protected void setHeader(final String name, final String value) {
     headers.put(name, new EmailHeader.String(value));
@@ -221,7 +241,7 @@
     return name;
   }
 
-  protected String getNameEmailFor(Account.Id accountId) {
+  public String getNameEmailFor(Account.Id accountId) {
     AccountState who = args.accountCache.get(accountId);
     String name = who.getAccount().getFullName();
     String email = who.getAccount().getPreferredEmail();
@@ -310,9 +330,40 @@
   private Address toAddress(final Account.Id id) {
     final Account a = args.accountCache.get(id).getAccount();
     final String e = a.getPreferredEmail();
-    if (e == null) {
+    if (!a.isActive() || e == null) {
       return null;
     }
     return new Address(a.getFullName(), e);
   }
+
+  protected void setupVelocityContext() {
+    velocityContext = new VelocityContext();
+
+    velocityContext.put("email", this);
+    velocityContext.put("messageClass", messageClass);
+    velocityContext.put("StringUtils", StringUtils.class);
+  }
+
+  protected String velocify(String template) throws EmailException {
+    try {
+      StringWriter w = new StringWriter();
+      Velocity.evaluate(velocityContext, w, "OutgoingEmail", template);
+      return w.toString();
+    } catch(Exception e) {
+      throw new EmailException("Velocity template " + template, e);
+    }
+  }
+
+  protected String velocifyFile(String name) throws EmailException {
+    if (!Velocity.resourceExists(name)) {
+      name = "com/google/gerrit/server/mail/" + name;
+    }
+    try {
+      StringWriter w = new StringWriter();
+      Velocity.mergeTemplate(name, "UTF-8", velocityContext, w);
+      return w.toString();
+    } catch(Exception e) {
+      throw new EmailException("Velocity template " + name + ".\n", e);
+    }
+  }
 }
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 9b201fd..2c77999 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
@@ -40,7 +40,7 @@
   }
 
   @Override
-  protected void init() {
+  protected void init() throws EmailException {
     super.init();
     setHeader("Subject", "[Gerrit Code Review] Email Verification");
     add(RecipientType.TO, new Address(addr));
@@ -52,39 +52,8 @@
   }
 
   @Override
-  protected void format() {
-    final StringBuilder url = new StringBuilder();
-    url.append(getGerritUrl());
-    url.append("#VE,");
-    url.append(getEmailRegistrationToken());
-
-    appendText("Welcome to Gerrit Code Review at ");
-    appendText(getGerritHost());
-    appendText(".\n");
-
-    appendText("\n");
-    appendText("To add a verified email address to your user account, please\n");
-    appendText("click on the following link:\n");
-    appendText("\n");
-    appendText(url.toString());
-    appendText("\n");
-
-    appendText("\n");
-    appendText("If you have received this mail in error,"
-        + " you do not need to take any\n");
-    appendText("action to cancel the account."
-        + " The account will not be activated, and\n");
-    appendText("you will not receive any further emails.\n");
-
-    appendText("\n");
-    appendText("If clicking the link above does not work,"
-        + " copy and paste the URL in a\n");
-    appendText("new browser window instead.\n");
-
-    appendText("\n");
-    appendText("This is a send-only email address."
-        + "  Replies to this message will not\n");
-    appendText("be read or answered.\n");
+  protected void format() throws EmailException {
+    appendText(velocifyFile("RegisterNewEmail.vm"));
   }
 
   public String getEmailRegistrationToken() {
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 841aa35..0c8ca9a 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
@@ -22,6 +22,7 @@
 
 import com.jcraft.jsch.HostKey;
 
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -53,7 +54,7 @@
   }
 
   @Override
-  protected void init() {
+  protected void init() throws EmailException {
     super.init();
 
     if (fromId != null) {
@@ -67,77 +68,19 @@
   }
 
   @Override
-  protected void formatChange() {
-    formatSalutation();
-    formatChangeDetail();
-
-    appendText("\n");
-    appendText("  " + getPullUrl() + "\n");
+  protected void formatChange() throws EmailException {
+    appendText(velocifyFile("ReplacePatchSet.vm"));
   }
 
-  private void formatSalutation() {
-    final String changeUrl = getChangeUrl();
-
+  public List<String> getReviewerNames() {
     if (reviewers.isEmpty()) {
-      formatDest();
-      if (changeUrl != null) {
-        appendText("\n");
-        appendText("    " + changeUrl + "\n");
-        appendText("\n");
-      }
-      appendText("\n");
-
-    } else {
-      appendText("Hello");
-      for (final Iterator<Account.Id> i = reviewers.iterator(); i.hasNext();) {
-        appendText(" ");
-        appendText(getNameFor(i.next()));
-        appendText(",");
-      }
-      appendText("\n");
-      appendText("\n");
-
-      appendText("I'd like you to reexamine change "
-          + change.getKey().abbreviate() + ".");
-      if (changeUrl != null) {
-        appendText("  Please visit\n");
-        appendText("\n");
-        appendText("    " + changeUrl + "\n");
-        appendText("\n");
-        appendText("to look at patch set " + patchSet.getPatchSetId());
-        appendText(":\n");
-      }
-      appendText("\n");
-
-      formatDest();
-      appendText("\n");
+      return null;
     }
-  }
-
-  private void formatDest() {
-    appendText("Change " + change.getKey().abbreviate());
-    appendText(" (patch set " + patchSet.getPatchSetId() + ")");
-    appendText(" for ");
-    appendText(change.getDest().getShortName());
-    appendText(" in ");
-    appendText(projectName);
-    appendText(":\n");
-  }
-
-  private String getPullUrl() {
-    final String host = getSshHost();
-    if (host == null) {
-      return "";
+    List<String> names = new ArrayList<String>();
+    for (Account.Id id : reviewers) {
+      names.add(getNameFor(id));
     }
-
-    final StringBuilder r = new StringBuilder();
-    r.append("git pull ssh://");
-    r.append(host);
-    r.append("/");
-    r.append(projectName);
-    r.append(" ");
-    r.append(patchSet.getRefName());
-    return r.toString();
+    return names;
   }
 
   public String getSshHost() {
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 05d2753..4c3ed76 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
@@ -23,7 +23,7 @@
   }
 
   @Override
-  protected void init() {
+  protected void init() throws EmailException {
     super.init();
 
     final String threadId = getChangeMessageThreadId();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/CharText.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/CharText.java
index fd62086..2a0cd2f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/CharText.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/CharText.java
@@ -16,11 +16,11 @@
 
 import org.eclipse.jgit.diff.Sequence;
 
-class CharText implements Sequence {
+class CharText extends Sequence {
   private final String content;
 
   CharText(Text text, int s, int e) {
-    content = text.getLines(s, e, false /* keep LF */);
+    content = text.getString(s, e, false /* keep LF */);
   }
 
   char charAt(int idx) {
@@ -41,11 +41,6 @@
   }
 
   @Override
-  public boolean equals(int a, Sequence other, int b) {
-    return content.charAt(a) == ((CharText) other).content.charAt(b);
-  }
-
-  @Override
   public int size() {
     return content.length();
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/CharTextComparator.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/CharTextComparator.java
new file mode 100644
index 0000000..8119313
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/CharTextComparator.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.patch;
+
+import org.eclipse.jgit.diff.SequenceComparator;
+
+class CharTextComparator extends SequenceComparator<CharText> {
+  @Override
+  public boolean equals(CharText a, int ai, CharText b, int bi) {
+    return a.charAt(ai) == b.charAt(bi);
+  }
+
+  @Override
+  public int hash(CharText seq, int ptr) {
+    return seq.charAt(ptr);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java
index 22be063..4feccd0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java
@@ -23,7 +23,7 @@
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevTree;
@@ -49,29 +49,34 @@
     this.repo = repo;
     this.entry = patchList.get(fileName);
 
-    final RevWalk rw = new RevWalk(repo);
-    final RevCommit bCommit = rw.parseCommit(patchList.getNewId());
+    final ObjectReader reader = repo.newObjectReader();
+    try {
+      final RevWalk rw = new RevWalk(reader);
+      final RevCommit bCommit = rw.parseCommit(patchList.getNewId());
 
-    if (Patch.COMMIT_MSG.equals(fileName)) {
-      if (patchList.isAgainstParent()) {
-        a = Text.EMPTY;
+      if (Patch.COMMIT_MSG.equals(fileName)) {
+        if (patchList.isAgainstParent()) {
+          a = Text.EMPTY;
+        } else {
+          a = Text.forCommit(repo, reader, patchList.getOldId());
+        }
+        b = Text.forCommit(repo, reader, bCommit);
+
+        aTree = null;
+        bTree = null;
+
       } else {
-        a = Text.forCommit(repo, patchList.getOldId());
+        if (patchList.getOldId() != null) {
+          aTree = rw.parseTree(patchList.getOldId());
+        } else {
+          final RevCommit p = bCommit.getParent(0);
+          rw.parseHeaders(p);
+          aTree = p.getTree();
+        }
+        bTree = bCommit.getTree();
       }
-      b = Text.forCommit(repo, bCommit);
-
-      aTree = null;
-      bTree = null;
-
-    } else {
-      if (patchList.getOldId() != null) {
-        aTree = rw.parseTree(patchList.getOldId());
-      } else {
-        final RevCommit p = bCommit.getParent(0);
-        rw.parseHeaders(p);
-        aTree = p.getTree();
-      }
-      bTree = bCommit.getTree();
+    } finally {
+      reader.release();
     }
   }
 
@@ -93,13 +98,13 @@
         if (a == null) {
           a = load(aTree, entry.getOldName());
         }
-        return a.getLine(line - 1);
+        return a.getString(line - 1);
 
       case 1:
         if (b == null) {
           b = load(bTree, entry.getNewName());
         }
-        return b.getLine(line - 1);
+        return b.getString(line - 1);
 
       default:
         throw new NoSuchEntityException();
@@ -119,11 +124,6 @@
     if (tw.getFileMode(0).getObjectType() != Constants.OBJ_BLOB) {
       return Text.EMPTY;
     }
-    final ObjectId id = tw.getObjectId(0);
-    final ObjectLoader ldr = repo.openObject(id);
-    if (ldr == null) {
-      throw new MissingObjectException(id, Constants.TYPE_BLOB);
-    }
-    return new Text(ldr.getCachedBytes());
+    return new Text(repo.open(tw.getObjectId(0), Constants.OBJ_BLOB));
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
index bb96237..e5c3b72 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
@@ -84,17 +84,16 @@
 import org.eclipse.jgit.diff.EditList;
 import org.eclipse.jgit.diff.MyersDiff;
 import org.eclipse.jgit.diff.RawText;
-import org.eclipse.jgit.diff.RawTextIgnoreAllWhitespace;
-import org.eclipse.jgit.diff.RawTextIgnoreTrailingWhitespace;
-import org.eclipse.jgit.diff.RawTextIgnoreWhitespaceChange;
-import org.eclipse.jgit.diff.RenameDetector;
+import org.eclipse.jgit.diff.RawTextComparator;
 import org.eclipse.jgit.diff.ReplaceEdit;
+import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.ObjectLoader;
-import org.eclipse.jgit.lib.ObjectWriter;
+import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.patch.FileHeader;
 import org.eclipse.jgit.patch.FileHeader.PatchType;
@@ -187,81 +186,85 @@
         final Repository repo) throws IOException {
       // TODO(jeffschu) correctly handle merge commits
 
-      RawText.Factory rawTextFactory;
+      RawTextComparator cmp;
       switch (key.getWhitespace()) {
         case IGNORE_ALL_SPACE:
-          rawTextFactory = RawTextIgnoreAllWhitespace.FACTORY;
+          cmp = RawTextComparator.WS_IGNORE_ALL;
           break;
         case IGNORE_SPACE_AT_EOL:
-          rawTextFactory = RawTextIgnoreTrailingWhitespace.FACTORY;
+          cmp = RawTextComparator.WS_IGNORE_TRAILING;
           break;
         case IGNORE_SPACE_CHANGE:
-          rawTextFactory = RawTextIgnoreWhitespaceChange.FACTORY;
+          cmp = RawTextComparator.WS_IGNORE_CHANGE;
           break;
         case IGNORE_NONE:
         default:
-          rawTextFactory = RawText.FACTORY;
+          cmp = RawTextComparator.DEFAULT;
           break;
       }
 
-      final RevWalk rw = new RevWalk(repo);
-      final RevCommit b = rw.parseCommit(key.getNewId());
-      final RevObject a = aFor(key, repo, rw, b);
+      final ObjectReader reader = repo.newObjectReader();
+      try {
+        final RevWalk rw = new RevWalk(reader);
+        final RevCommit b = rw.parseCommit(key.getNewId());
+        final RevObject a = aFor(key, repo, rw, b);
 
-      if (a == null) {
-        // This is a merge commit, compared to its ancestor.
-        //
-        final PatchListEntry[] entries = new PatchListEntry[1];
-        entries[0] = newCommitMessage(rawTextFactory, repo, null, b);
-        return new PatchList(a, b, computeIntraline, true, entries);
+        if (a == null) {
+          // This is a merge commit, compared to its ancestor.
+          //
+          final PatchListEntry[] entries = new PatchListEntry[1];
+          entries[0] = newCommitMessage(cmp, repo, reader, null, b);
+          return new PatchList(a, b, computeIntraline, true, entries);
+        }
+
+        final boolean againstParent =
+            b.getParentCount() > 0 && b.getParent(0) == a;
+
+        RevCommit aCommit;
+        RevTree aTree;
+        if (a instanceof RevCommit) {
+          aCommit = (RevCommit) a;
+          aTree = aCommit.getTree();
+        } else if (a instanceof RevTree) {
+          aCommit = null;
+          aTree = (RevTree) a;
+        } else {
+          throw new IOException("Unexpected type: " + a.getClass());
+        }
+
+        RevTree bTree = b.getTree();
+
+        final TreeWalk walk = new TreeWalk(reader);
+        walk.reset();
+        walk.setRecursive(true);
+        walk.addTree(aTree);
+        walk.addTree(bTree);
+        walk.setFilter(TreeFilter.ANY_DIFF);
+
+        DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE);
+        df.setRepository(repo);
+        df.setDiffComparator(cmp);
+        df.setDetectRenames(true);
+        List<DiffEntry> diffEntries = df.scan(aTree, bTree);
+
+        final int cnt = diffEntries.size();
+        final PatchListEntry[] entries = new PatchListEntry[1 + cnt];
+        entries[0] = newCommitMessage(cmp, repo, reader, //
+            againstParent ? null : aCommit, b);
+        for (int i = 0; i < cnt; i++) {
+          FileHeader fh = df.toFileHeader(diffEntries.get(i));
+          entries[1 + i] = newEntry(reader, aTree, bTree, fh);
+        }
+        return new PatchList(a, b, computeIntraline, againstParent, entries);
+      } finally {
+        reader.release();
       }
-
-      final boolean againstParent =
-          b.getParentCount() > 0 && b.getParent(0) == a;
-
-      RevCommit aCommit;
-      RevTree aTree;
-      if (a instanceof RevCommit) {
-        aCommit = (RevCommit) a;
-        aTree = aCommit.getTree();
-      } else if (a instanceof RevTree) {
-        aCommit = null;
-        aTree = (RevTree) a;
-      } else {
-        throw new IOException("Unexpected type: " + a.getClass());
-      }
-
-      RevTree bTree = b.getTree();
-
-      final TreeWalk walk = new TreeWalk(repo);
-      walk.reset();
-      walk.setRecursive(true);
-      walk.addTree(aTree);
-      walk.addTree(bTree);
-      walk.setFilter(TreeFilter.ANY_DIFF);
-
-      DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE);
-      df.setRepository(repo);
-      df.setRawTextFactory(rawTextFactory);
-
-      RenameDetector rd = new RenameDetector(repo);
-      rd.addAll(DiffEntry.scan(walk));
-      List<DiffEntry> diffEntries = rd.compute();
-
-      final int cnt = diffEntries.size();
-      final PatchListEntry[] entries = new PatchListEntry[1 + cnt];
-      entries[0] = newCommitMessage(rawTextFactory, repo, //
-          againstParent ? null : aCommit, b);
-      for (int i = 0; i < cnt; i++) {
-        FileHeader fh = df.createFileHeader(diffEntries.get(i));
-        entries[1 + i] = newEntry(repo, aTree, bTree, fh);
-      }
-      return new PatchList(a, b, computeIntraline, againstParent, entries);
     }
 
     private PatchListEntry newCommitMessage(
-        final RawText.Factory rawTextFactory, final Repository repo,
-        final RevCommit aCommit, final RevCommit bCommit) throws IOException {
+        final RawTextComparator cmp, final Repository db,
+        final ObjectReader reader, final RevCommit aCommit,
+        final RevCommit bCommit) throws IOException {
       StringBuilder hdr = new StringBuilder();
 
       hdr.append("diff --git");
@@ -280,18 +283,18 @@
       }
       hdr.append("+++ b/" + Patch.COMMIT_MSG + "\n");
 
-      Text aText = aCommit != null ? Text.forCommit(repo, aCommit) : Text.EMPTY;
-      Text bText = Text.forCommit(repo, bCommit);
+      Text aText = aCommit != null ? Text.forCommit(db, reader, aCommit) : Text.EMPTY;
+      Text bText = Text.forCommit(db, reader, bCommit);
 
       byte[] rawHdr = hdr.toString().getBytes("UTF-8");
-      RawText aRawText = rawTextFactory.create(aText.getContent());
-      RawText bRawText = rawTextFactory.create(bText.getContent());
-      EditList edits = new MyersDiff(aRawText, bRawText).getEdits();
+      RawText aRawText = new RawText(aText.getContent());
+      RawText bRawText = new RawText(bText.getContent());
+      EditList edits = MyersDiff.INSTANCE.diff(cmp, aRawText, bRawText);
       FileHeader fh = new FileHeader(rawHdr, edits, PatchType.UNIFIED);
-      return newEntry(repo, aText, bText, edits, null, null, fh);
+      return newEntry(reader, aText, bText, edits, null, null, fh);
     }
 
-    private PatchListEntry newEntry(Repository repo, RevTree aTree,
+    private PatchListEntry newEntry(ObjectReader reader, RevTree aTree,
         RevTree bTree, FileHeader fileHeader) throws IOException {
       final FileMode oldMode = fileHeader.getOldMode();
       final FileMode newMode = fileHeader.getNewMode();
@@ -320,10 +323,10 @@
           return new PatchListEntry(fileHeader, edits);
       }
 
-      return newEntry(repo, null, null, edits, aTree, bTree, fileHeader);
+      return newEntry(reader, null, null, edits, aTree, bTree, fileHeader);
     }
 
-    private PatchListEntry newEntry(Repository repo, Text aContent,
+    private PatchListEntry newEntry(ObjectReader reader, Text aContent,
         Text bContent, List<Edit> edits, RevTree aTree, RevTree bTree,
         FileHeader fileHeader) throws IOException {
       for (int i = 0; i < edits.size(); i++) {
@@ -332,8 +335,8 @@
         if (e.getType() == Edit.Type.REPLACE) {
           if (aContent == null) {
             edits = new ArrayList<Edit>(edits);
-            aContent = read(repo, fileHeader.getOldName(), aTree);
-            bContent = read(repo, fileHeader.getNewName(), bTree);
+            aContent = read(reader, fileHeader.getOldPath(), aTree);
+            bContent = read(reader, fileHeader.getNewPath(), bTree);
             combineLineEdits(edits, aContent, bContent);
             i = -1; // restart the entire scan after combining lines.
             continue;
@@ -341,8 +344,9 @@
 
           CharText a = new CharText(aContent, e.getBeginA(), e.getEndA());
           CharText b = new CharText(bContent, e.getBeginB(), e.getEndB());
+          CharTextComparator cmp = new CharTextComparator();
 
-          List<Edit> wordEdits = new MyersDiff(a, b).getEdits();
+          List<Edit> wordEdits = MyersDiff.INSTANCE.diff(cmp, a, b);
 
           // Combine edits that are really close together. If they are
           // just a few characters apart we tend to get better results
@@ -407,11 +411,11 @@
             // such that the edges of each text is identical. Fix by
             // by dropping out that incorrectly replaced region.
             //
-            while (ab < ae && bb < be && a.equals(ab, b, bb)) {
+            while (ab < ae && bb < be && cmp.equals(a, ab, b, bb)) {
               ab++;
               bb++;
             }
-            while (ab < ae && bb < be && a.equals(ae - 1, b, be - 1)) {
+            while (ab < ae && bb < be && cmp.equals(a, ae - 1, b, be - 1)) {
               ae--;
               be--;
             }
@@ -429,7 +433,7 @@
                 int nb = lf + 1;
                 int p = 0;
                 while (p < ae - ab) {
-                  if (a.equals(ab + p, a, ab + p))
+                  if (cmp.equals(a, ab + p, a, ab + p))
                     p++;
                   else
                     break;
@@ -443,12 +447,12 @@
             }
             if (aShift) {
               while (0 < ab && ab < ae && a.charAt(ab - 1) != '\n'
-                  && a.equals(ab - 1, a, ae - 1)) {
+                  && cmp.equals(a, ab - 1, a, ae - 1)) {
                 ab--;
                 ae--;
               }
               if (!a.isLineStart(ab) || !a.contains(ab, ae, '\n')) {
-                while (ab < ae && ae < a.size() && a.equals(ab, a, ae)) {
+                while (ab < ae && ae < a.size() && cmp.equals(a, ab, a, ae)) {
                   ab++;
                   ae++;
                   if (a.charAt(ae - 1) == '\n') {
@@ -465,7 +469,7 @@
                 int nb = lf + 1;
                 int p = 0;
                 while (p < be - bb) {
-                  if (b.equals(bb + p, b, bb + p))
+                  if (cmp.equals(b, bb + p, b, bb + p))
                     p++;
                   else
                     break;
@@ -479,12 +483,12 @@
             }
             if (bShift) {
               while (0 < bb && bb < be && b.charAt(bb - 1) != '\n'
-                  && b.equals(bb - 1, b, be - 1)) {
+                  && cmp.equals(b, bb - 1, b, be - 1)) {
                 bb--;
                 be--;
               }
               if (!b.isLineStart(bb) || !b.contains(bb, be, '\n')) {
-                while (bb < be && be < b.size() && b.equals(bb, b, be)) {
+                while (bb < be && be < b.size() && cmp.equals(b, bb, b, be)) {
                   bb++;
                   be++;
                   if (b.charAt(be - 1) == '\n') {
@@ -551,7 +555,7 @@
 
     private static boolean isBlankLineGap(Text a, int b, int e) {
       for (; b < e; b++) {
-        if (!BLANK_LINE_RE.matcher(a.getLine(b)).matches()) {
+        if (!BLANK_LINE_RE.matcher(a.getString(b)).matches()) {
           return false;
         }
       }
@@ -559,7 +563,7 @@
     }
 
     private static boolean isControlBlockStart(Text a, int idx) {
-      final String l = a.getLine(idx);
+      final String l = a.getString(idx);
       return CONTROL_BLOCK_START_RE.matcher(l).find();
     }
 
@@ -590,17 +594,19 @@
       return b < e;
     }
 
-    private static Text read(Repository repo, String path, RevTree tree)
+    private static Text read(ObjectReader reader, String path, RevTree tree)
         throws IOException {
-      TreeWalk tw = TreeWalk.forPath(repo, path, tree);
+      TreeWalk tw = TreeWalk.forPath(reader, path, tree);
       if (tw == null || tw.getFileMode(0).getObjectType() != Constants.OBJ_BLOB) {
         return Text.EMPTY;
       }
-      ObjectLoader ldr = repo.openObject(tw.getObjectId(0));
-      if (ldr == null) {
+      ObjectLoader ldr;
+      try {
+        ldr = reader.open(tw.getObjectId(0), Constants.OBJ_BLOB);
+      } catch (MissingObjectException notFound) {
         return Text.EMPTY;
       }
-      return new Text(ldr.getCachedBytes());
+      return new Text(ldr);
     }
 
     private static RevObject aFor(final PatchListKey key,
@@ -625,7 +631,14 @@
     }
 
     private static ObjectId emptyTree(final Repository repo) throws IOException {
-      return new ObjectWriter(repo).writeCanonicalTree(new byte[0]);
+      ObjectInserter oi = repo.newObjectInserter();
+      try {
+        ObjectId id = oi.insert(Constants.OBJ_TREE, new byte[] {});
+        oi.flush();
+        return id;
+      } finally {
+        oi.release();
+      }
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java
index fd7da08..fac8ec4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java
@@ -69,19 +69,19 @@
     switch (changeType) {
       case DELETED:
         oldName = null;
-        newName = hdr.getOldName();
+        newName = hdr.getOldPath();
         break;
 
       case ADDED:
       case MODIFIED:
         oldName = null;
-        newName = hdr.getNewName();
+        newName = hdr.getNewPath();
         break;
 
       case COPIED:
       case RENAMED:
-        oldName = hdr.getOldName();
-        newName = hdr.getNewName();
+        oldName = hdr.getOldPath();
+        newName = hdr.getNewPath();
         break;
 
       default:
@@ -286,21 +286,17 @@
   private static PatchType toPatchType(final FileHeader hdr) {
     PatchType pt;
 
-    if (hdr instanceof CombinedFileHeader) {
-      pt = Patch.PatchType.N_WAY;
-    } else {
-      switch (hdr.getPatchType()) {
-        case UNIFIED:
-          pt = Patch.PatchType.UNIFIED;
-          break;
-        case GIT_BINARY:
-        case BINARY:
-          pt = Patch.PatchType.BINARY;
-          break;
-        default:
-          throw new IllegalArgumentException("Unsupported type "
-              + hdr.getPatchType());
-      }
+    switch (hdr.getPatchType()) {
+      case UNIFIED:
+        pt = Patch.PatchType.UNIFIED;
+        break;
+      case GIT_BINARY:
+      case BINARY:
+        pt = Patch.PatchType.BINARY;
+        break;
+      default:
+        throw new IllegalArgumentException("Unsupported type "
+            + hdr.getPatchType());
     }
 
     if (pt != PatchType.BINARY) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
index 9617172..840e76f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
@@ -79,9 +79,13 @@
       final String projectName = projectKey.get();
       repo = repoManager.openRepository(projectName);
       final RevWalk rw = new RevWalk(repo);
-      final RevCommit src =
-          rw.parseCommit(ObjectId.fromString(patchSet.getRevision().get()));
-      return get(src, patchSetId);
+      try {
+        final RevCommit src =
+            rw.parseCommit(ObjectId.fromString(patchSet.getRevision().get()));
+        return get(src, patchSetId);
+      } finally {
+        rw.release();
+      }
     } catch (OrmException e) {
       throw new PatchSetInfoNotAvailableException(e);
     } catch (IOException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/Text.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/Text.java
index 860e4b9..97f53fc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/Text.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/Text.java
@@ -15,11 +15,16 @@
 package com.google.gerrit.server.patch;
 
 import org.eclipse.jgit.diff.RawText;
+import org.eclipse.jgit.errors.LargeObjectException;
+import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.storage.pack.PackConfig;
 import org.eclipse.jgit.util.RawParseUtils;
 import org.mozilla.universalchardet.UniversalDetector;
 import org.slf4j.Logger;
@@ -34,13 +39,14 @@
 public class Text extends RawText {
   private static final Logger log = LoggerFactory.getLogger(Text.class);
   private static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");
+  private static final int bigFileThreshold = PackConfig.DEFAULT_BIG_FILE_THRESHOLD;
 
   public static final byte[] NO_BYTES = {};
   public static final Text EMPTY = new Text(NO_BYTES);
 
-  public static Text forCommit(Repository db, AnyObjectId commitId)
-      throws IOException {
-    RevWalk rw = new RevWalk(db);
+  public static Text forCommit(Repository db, ObjectReader reader,
+      AnyObjectId commitId) throws IOException {
+    RevWalk rw = new RevWalk(reader);
     RevCommit c;
     if (commitId instanceof RevCommit) {
       c = (RevCommit) commitId;
@@ -56,7 +62,7 @@
         RevCommit p = c.getParent(0);
         rw.parseBody(p);
         b.append("Parent:     ");
-        b.append(p.abbreviate(db, 8).name());
+        b.append(reader.abbreviate(p, 8).name());
         b.append(" (");
         b.append(p.getShortMessage());
         b.append(")\n");
@@ -67,7 +73,7 @@
           RevCommit p = c.getParent(i);
           rw.parseBody(p);
           b.append(i == 0 ? "Merge Of:   " : "            ");
-          b.append(p.abbreviate(db, 8).name());
+          b.append(reader.abbreviate(p, 8).name());
           b.append(" (");
           b.append(p.getShortMessage());
           b.append(")\n");
@@ -103,8 +109,9 @@
     }
   }
 
-  public static String asString(byte[] content, String encoding) {
-    return new String(content, charset(content, encoding));
+  public static byte[] asByteArray(ObjectLoader ldr)
+      throws MissingObjectException, LargeObjectException, IOException {
+    return ldr.getCachedBytes(bigFileThreshold);
   }
 
   private static Charset charset(byte[] content, String encoding) {
@@ -136,39 +143,20 @@
     super(r);
   }
 
+  public Text(ObjectLoader ldr) throws MissingObjectException,
+      LargeObjectException, IOException {
+    this(asByteArray(ldr));
+  }
+
   public byte[] getContent() {
     return content;
   }
 
-  public String getLine(final int i) {
-    return getLines(i, i + 1, true);
-  }
-
-  public String getLines(final int begin, final int end, boolean dropLF) {
-    if (begin == end) {
-      return "";
-    }
-
-    final int s = getLineStart(begin);
-    int e = getLineEnd(end - 1);
-    if (dropLF && content[e - 1] == '\n') {
-      e--;
-    }
-    return decode(s, e);
-  }
-
-  private String decode(final int s, int e) {
+  @Override
+  protected String decode(final int s, int e) {
     if (charset == null) {
       charset = charset(content, null);
     }
     return RawParseUtils.decode(charset, content, s, e);
   }
-
-  private int getLineStart(final int i) {
-    return lines.get(i + 1);
-  }
-
-  private int getLineEnd(final int i) {
-    return lines.get(i + 2);
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/AccessControlModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/AccessControlModule.java
new file mode 100644
index 0000000..1e2e7f4
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/AccessControlModule.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import static com.google.inject.Scopes.SINGLETON;
+
+import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.server.config.FactoryModule;
+import com.google.gerrit.server.config.GitReceivePackGroups;
+import com.google.gerrit.server.config.GitReceivePackGroupsProvider;
+import com.google.gerrit.server.config.GitUploadPackGroups;
+import com.google.gerrit.server.config.GitUploadPackGroupsProvider;
+import com.google.gerrit.server.config.ProjectCreatorGroups;
+import com.google.gerrit.server.config.ProjectCreatorGroupsProvider;
+import com.google.gerrit.server.config.ProjectOwnerGroups;
+import com.google.gerrit.server.config.ProjectOwnerGroupsProvider;
+import com.google.inject.TypeLiteral;
+
+import java.util.Set;
+
+public class AccessControlModule extends FactoryModule {
+  @Override
+  protected void configure() {
+    bind(new TypeLiteral<Set<AccountGroup.Id>>() {}) //
+        .annotatedWith(ProjectCreatorGroups.class) //
+        .toProvider(ProjectCreatorGroupsProvider.class).in(SINGLETON);
+
+    bind(new TypeLiteral<Set<AccountGroup.Id>>() {}) //
+        .annotatedWith(ProjectOwnerGroups.class) //
+        .toProvider(ProjectOwnerGroupsProvider.class).in(SINGLETON);
+
+    bind(new TypeLiteral<Set<AccountGroup.Id>>() {}) //
+        .annotatedWith(GitUploadPackGroups.class) //
+        .toProvider(GitUploadPackGroupsProvider.class).in(SINGLETON);
+
+    bind(new TypeLiteral<Set<AccountGroup.Id>>() {}) //
+        .annotatedWith(GitReceivePackGroups.class) //
+        .toProvider(GitReceivePackGroupsProvider.class).in(SINGLETON);
+
+    factory(ProjectControl.AssistedFactory.class);
+  }
+}
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 ff788dd..25778a6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.project;
 
+import static com.google.gerrit.common.CollectionsUtil.*;
 import com.google.gerrit.reviewdb.AccountGroup;
 import com.google.gerrit.reviewdb.ApprovalCategory;
 import com.google.gerrit.reviewdb.Branch;
@@ -22,8 +23,11 @@
 import com.google.gerrit.reviewdb.RefRight;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.ReplicationUser;
+import com.google.gerrit.server.config.GitReceivePackGroups;
+import com.google.gerrit.server.config.GitUploadPackGroups;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.assistedinject.Assisted;
 
 import java.util.HashSet;
 import java.util.Set;
@@ -93,10 +97,25 @@
     }
   }
 
+  interface AssistedFactory {
+    ProjectControl create(CurrentUser who, ProjectState ps);
+  }
+
+  private final Set<AccountGroup.Id> uploadGroups;
+  private final Set<AccountGroup.Id> receiveGroups;
+
+  private final RefControl.Factory refControlFactory;
   private final CurrentUser user;
   private final ProjectState state;
 
-  ProjectControl(final CurrentUser who, final ProjectState ps) {
+  @Inject
+  ProjectControl(@GitUploadPackGroups Set<AccountGroup.Id> uploadGroups,
+      @GitReceivePackGroups Set<AccountGroup.Id> receiveGroups,
+      final RefControl.Factory refControlFactory,
+      @Assisted CurrentUser who, @Assisted ProjectState ps) {
+    this.uploadGroups = uploadGroups;
+    this.receiveGroups = receiveGroups;
+    this.refControlFactory = refControlFactory;
     user = who;
     state = ps;
   }
@@ -118,7 +137,7 @@
   }
 
   public RefControl controlForRef(String refName) {
-    return new RefControl(this, refName);
+    return refControlFactory.create(this, refName);
   }
 
   public CurrentUser getCurrentUser() {
@@ -169,33 +188,25 @@
   }
 
   /** @return true if the user can upload to at least one reference */
-  public boolean canUploadToAtLeastOneRef() {
-    return canPerformOnAnyRef(ApprovalCategory.READ, (short) 2);
+  public boolean canPushToAtLeastOneRef() {
+    return canPerformOnAnyRef(ApprovalCategory.READ, (short) 2)
+        || canPerformOnAnyRef(ApprovalCategory.PUSH_HEAD, (short) 1)
+        || canPerformOnAnyRef(ApprovalCategory.PUSH_TAG, (short) 1);
   }
 
+  // TODO (anatol.pomazau): Try to merge this method with similar RefRightsForPattern#canPerform
   private boolean canPerformOnAnyRef(ApprovalCategory.Id actionId,
       short requireValue) {
     final Set<AccountGroup.Id> groups = user.getEffectiveGroups();
-    int val = Integer.MIN_VALUE;
 
-    for (final RefRight pr : state.getLocalRights(actionId)) {
-      if (groups.contains(pr.getAccountGroupId())) {
-        val = Math.max(pr.getMaxValue(), val);
-      }
-    }
-    if (val >= requireValue) {
-      return true;
-    }
-
-    if (actionId.canInheritFromWildProject()) {
-      for (final RefRight pr : state.getInheritedRights(actionId)) {
-        if (groups.contains(pr.getAccountGroupId())) {
-          val = Math.max(pr.getMaxValue(), val);
-        }
+    for (final RefRight pr : state.getAllRights(actionId, true)) {
+      if (groups.contains(pr.getAccountGroupId())
+          && pr.getMaxValue() >= requireValue) {
+        return true;
       }
     }
 
-    return val >= requireValue;
+    return false;
   }
 
   private boolean canPerformOnAllRefs(ApprovalCategory.Id actionId,
@@ -220,14 +231,17 @@
 
   private Set<String> allRefPatterns(ApprovalCategory.Id actionId) {
     final Set<String> all = new HashSet<String>();
-    for (final RefRight pr : state.getLocalRights(actionId)) {
+    for (final RefRight pr : state.getAllRights(actionId, true)) {
       all.add(pr.getRefPattern());
     }
-    if (actionId.canInheritFromWildProject()) {
-      for (final RefRight pr : state.getInheritedRights(actionId)) {
-        all.add(pr.getRefPattern());
-      }
-    }
     return all;
   }
+
+  public boolean canRunUploadPack() {
+    return isAnyIncludedIn(uploadGroups, user.getEffectiveGroups());
+  }
+
+  public boolean canRunReceivePack() {
+    return isAnyIncludedIn(receiveGroups, user.getEffectiveGroups());
+  }
 }
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 330fc6e..567ecfe 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
@@ -28,6 +28,8 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Set;
 
@@ -40,6 +42,7 @@
   private final AnonymousUser anonymousUser;
   private final Project.NameKey wildProject;
   private final ProjectCache projectCache;
+  private final ProjectControl.AssistedFactory projectControlFactory;
 
   private final Project project;
   private final Collection<RefRight> localRights;
@@ -51,11 +54,13 @@
   protected ProjectState(final AnonymousUser anonymousUser,
       final ProjectCache projectCache,
       @WildProjectName final Project.NameKey wildProject,
+      final ProjectControl.AssistedFactory projectControlFactory,
       @Assisted final Project project,
       @Assisted final Collection<RefRight> rights) {
     this.anonymousUser = anonymousUser;
     this.projectCache = projectCache;
     this.wildProject = wildProject;
+    this.projectControlFactory = projectControlFactory;
 
     this.project = project;
     this.localRights = rights;
@@ -134,16 +139,59 @@
   }
 
   /**
-   * Get the rights this project inherits from the wild project.
+   * Utility class that is needed to filter overridden refrights
+   */
+  private static class Grant {
+    final AccountGroup.Id group;
+    final String pattern;
+
+    private Grant(AccountGroup.Id group, String pattern) {
+      this.group = group;
+      this.pattern = pattern;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      Grant grant = (Grant) o;
+      return group.equals(grant.group) && pattern.equals(grant.pattern);
+    }
+
+    @Override
+    public int hashCode() {
+      int result = group.hashCode();
+      result = 31 * result + pattern.hashCode();
+      return result;
+    }
+  }
+
+  /**
+   * Get the rights this project has and inherits from the wild project.
    *
    * @param action the category requested.
+   * @param dropOverridden whether to remove inherited permissions in case if we have a
+   *     local one that matches (action,group,ref)
    * @return immutable collection of rights for the requested category.
    */
-  public Collection<RefRight> getInheritedRights(ApprovalCategory.Id action) {
+  public Collection<RefRight> getAllRights(ApprovalCategory.Id action, boolean dropOverridden) {
+    Collection<RefRight> rights = new LinkedList<RefRight>(getLocalRights(action));
     if (action.canInheritFromWildProject()) {
-      return filter(getInheritedRights(), action);
+      rights.addAll(filter(getInheritedRights(), action));
     }
-    return Collections.emptyList();
+    if (dropOverridden) {
+      Set<Grant> grants = new HashSet<Grant>();
+      Iterator<RefRight> iter = rights.iterator();
+      while (iter.hasNext()) {
+        RefRight right = iter.next();
+
+        Grant grant = new Grant(right.getAccountGroupId(), right.getRefPattern());
+        if (grants.contains(grant)) {
+          iter.remove();
+        } else {
+          grants.add(grant);
+        }
+      }
+    }
+    return Collections.unmodifiableCollection(rights);
   }
 
   /** Is this the special wild project which manages inherited rights? */
@@ -160,7 +208,7 @@
   }
 
   public ProjectControl controlFor(final CurrentUser user) {
-    return new ProjectControl(user, this);
+    return projectControlFactory.create(user, this);
   }
 
   private static Collection<RefRight> filter(Collection<RefRight> all,
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 46fd90e..7a27835 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
@@ -32,8 +32,11 @@
 import com.google.gerrit.reviewdb.AccountGroup;
 import com.google.gerrit.reviewdb.ApprovalCategory;
 import com.google.gerrit.reviewdb.RefRight;
+import com.google.gerrit.reviewdb.SystemConfig;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
 
 import dk.brics.automaton.RegExp;
 
@@ -50,6 +53,7 @@
 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;
@@ -60,13 +64,22 @@
 
 /** Manages access control for Git references (aka branches, tags). */
 public class RefControl {
+  public interface Factory {
+    RefControl create(ProjectControl projectControl, String ref);
+  }
+
+  private final SystemConfig systemConfig;
   private final ProjectControl projectControl;
   private final String refName;
 
   private Boolean canForgeAuthor;
   private Boolean canForgeCommitter;
 
-  RefControl(final ProjectControl projectControl, String ref) {
+  @Inject
+  protected RefControl(final SystemConfig systemConfig,
+      @Assisted final ProjectControl projectControl,
+      @Assisted String ref) {
+    this.systemConfig = systemConfig;
     if (isRE(ref)) {
       ref = shortestExample(ref);
 
@@ -302,38 +315,35 @@
      * @param groups The groups of the user
      * @return The allowed value for this ref for all the specified groups
      */
-    public int allowedValueForRef(Set<AccountGroup.Id> groups) {
-      int val = Integer.MIN_VALUE;
+    private boolean allowedValueForRef(Set<AccountGroup.Id> groups, short level) {
       for (RefRight right : rights) {
-        if (groups.contains(right.getAccountGroupId())) {
-          val = Math.max(right.getMaxValue(), val);
+        if (groups.contains(right.getAccountGroupId())
+            && right.getMaxValue() >= level) {
+          return true;
         }
       }
-      return val;
+      return false;
     }
   }
 
   boolean canPerform(ApprovalCategory.Id actionId, short level) {
     final Set<AccountGroup.Id> groups = getCurrentUser().getEffectiveGroups();
-    int val = Integer.MIN_VALUE;
 
     List<RefRight> allRights = new ArrayList<RefRight>();
-    allRights.addAll(getLocalRights(actionId));
-
-    if (actionId.canInheritFromWildProject()) {
-      allRights.addAll(getInheritedRights(actionId));
-    }
+    allRights.addAll(getAllRights(actionId));
 
     SortedMap<String, RefRightsForPattern> perPatternRights =
       sortedRightsByPattern(allRights);
 
     for (RefRightsForPattern right : perPatternRights.values()) {
-      val = Math.max(val, right.allowedValueForRef(groups));
-      if (val >= level || right.containsExclusive()) {
-        return val >= level;
+      if (right.allowedValueForRef(groups, level)) {
+        return true;
+      }
+      if (right.containsExclusive() && !actionId.equals(OWN)) {
+        break;
       }
     }
-    return val >= level;
+    return false;
   }
 
   /**
@@ -464,12 +474,9 @@
     return rights;
   }
 
-  private List<RefRight> getLocalRights(ApprovalCategory.Id actionId) {
-    return filter(getProjectState().getLocalRights(actionId));
-  }
-
-  private List<RefRight> getInheritedRights(ApprovalCategory.Id actionId) {
-    return filter(getProjectState().getInheritedRights(actionId));
+  private List<RefRight> getAllRights(ApprovalCategory.Id actionId) {
+    final List<RefRight> allRefRights = filter(getProjectState().getAllRights(actionId, true));
+    return resolveOwnerGroups(allRefRights);
   }
 
   /**
@@ -484,8 +491,7 @@
    */
   public List<RefRight> getApplicableRights(final ApprovalCategory.Id id) {
     List<RefRight> l = new ArrayList<RefRight>();
-    l.addAll(getLocalRights(id));
-    l.addAll(getInheritedRights(id));
+    l.addAll(getAllRights(id));
     SortedMap<String, RefRightsForPattern> perPatternRights =
       sortedRightsByPattern(l);
     List<RefRight> applicable = new ArrayList<RefRight>();
@@ -498,6 +504,47 @@
     return Collections.unmodifiableList(applicable);
   }
 
+  /**
+   * Resolves all refRights which assign privileges to the 'Project Owners'
+   * group. All other refRights stay unchanged.
+   *
+   * @param refRights refRights to be resolved
+   * @return the resolved refRights
+   */
+  private List<RefRight> resolveOwnerGroups(final List<RefRight> refRights) {
+    final List<RefRight> resolvedRefRights =
+        new ArrayList<RefRight>(refRights.size());
+    for (final RefRight refRight : refRights) {
+      resolvedRefRights.addAll(resolveOwnerGroups(refRight));
+    }
+    return resolvedRefRights;
+  }
+
+  /**
+   * Checks if the given refRight assigns privileges to the 'Project Owners'
+   * group.
+   * If yes, resolves the 'Project Owners' group to the concrete groups that
+   * own the project and creates new refRights for the concrete owner groups
+   * which are returned.
+   * If no, the given refRight is returned unchanged.
+   *
+   * @param refRight refRight
+   * @return the resolved refRights
+   */
+  private Set<RefRight> resolveOwnerGroups(final RefRight refRight) {
+    final Set<RefRight> resolvedRefRights = new HashSet<RefRight>();
+    if (refRight.getAccountGroupId().equals(systemConfig.ownerGroupId)) {
+      for (final AccountGroup.Id ownerGroup : getProjectState().getOwners()) {
+        if (!ownerGroup.equals(systemConfig.ownerGroupId)) {
+          resolvedRefRights.add(new RefRight(refRight, ownerGroup));
+        }
+      }
+    } else {
+      resolvedRefRights.add(refRight);
+    }
+    return resolvedRefRights;
+  }
+
   private List<RefRight> filter(Collection<RefRight> all) {
     List<RefRight> mine = new ArrayList<RefRight>(all.size());
     for (RefRight right : all) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BranchPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BranchPredicate.java
index ae48fdc..1083d6a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BranchPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BranchPredicate.java
@@ -28,7 +28,7 @@
   BranchPredicate(Provider<ReviewDb> dbProvider, String branch) {
     super(ChangeQueryBuilder.FIELD_BRANCH, branch);
     this.dbProvider = dbProvider;
-    this.shortName = new Branch.NameKey(null, branch).getShortName();
+    this.shortName = branch;
   }
 
   @Override
@@ -37,7 +37,8 @@
     if (change == null) {
       return false;
     }
-    return shortName.equals(change.getDest().getShortName());
+    return change.getDest().get().startsWith(Branch.R_HEADS)
+        && shortName.equals(change.getDest().getShortName());
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
index 930b2d6..b4965f0 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
@@ -32,13 +32,16 @@
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 public class ChangeData {
   private final Change.Id legacyId;
   private Change change;
   private Collection<PatchSet> patches;
   private Collection<PatchSetApproval> approvals;
+  private Map<PatchSet.Id,Collection<PatchSetApproval>> approvalsMap;
   private Collection<PatchSetApproval> currentApprovals;
   private String[] currentFiles;
   private Collection<PatchLineComment> comments;
@@ -175,6 +178,23 @@
     return approvals;
   }
 
+  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);
+        }
+        c.add(psa);
+      }
+    }
+    return approvalsMap;
+  }
+
   public Collection<PatchLineComment> comments(Provider<ReviewDb> db)
       throws OrmException {
     if (comments == null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index eccb2e8..5cd05a8 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
@@ -27,6 +27,7 @@
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.WildProjectName;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.query.IntPredicate;
@@ -75,6 +76,7 @@
   public static final String FIELD_HAS = "has";
   public static final String FIELD_LABEL = "label";
   public static final String FIELD_LIMIT = "limit";
+  public static final String FIELD_MESSAGE = "message";
   public static final String FIELD_OWNER = "owner";
   public static final String FIELD_PROJECT = "project";
   public static final String FIELD_REF = "ref";
@@ -102,6 +104,7 @@
     final ApprovalTypes approvalTypes;
     final Project.NameKey wildProjectName;
     final PatchListCache patchListCache;
+    final GitRepositoryManager repoManager;
 
     @Inject
     Arguments(Provider<ReviewDb> dbProvider,
@@ -112,7 +115,8 @@
         AccountResolver accountResolver, GroupCache groupCache,
         AuthConfig authConfig, ApprovalTypes approvalTypes,
         @WildProjectName Project.NameKey wildProjectName,
-        PatchListCache patchListCache) {
+        PatchListCache patchListCache,
+        GitRepositoryManager repoManager) {
       this.dbProvider = dbProvider;
       this.rewriter = rewriter;
       this.userFactory = userFactory;
@@ -124,6 +128,7 @@
       this.approvalTypes = approvalTypes;
       this.wildProjectName = wildProjectName;
       this.patchListCache = patchListCache;
+      this.repoManager = repoManager;
     }
   }
 
@@ -238,21 +243,29 @@
 
   @Operator
   public Predicate<ChangeData> project(String name) {
+    if (name.startsWith("^"))
+      return new RegexProjectPredicate(args.dbProvider, name);
     return new ProjectPredicate(args.dbProvider, name);
   }
 
   @Operator
   public Predicate<ChangeData> branch(String name) {
+    if (name.startsWith("^"))
+      return new RegexBranchPredicate(args.dbProvider, name);
     return new BranchPredicate(args.dbProvider, name);
   }
 
   @Operator
   public Predicate<ChangeData> topic(String name) {
+    if (name.startsWith("^"))
+      return new RegexTopicPredicate(args.dbProvider, name);
     return new TopicPredicate(args.dbProvider, name);
   }
 
   @Operator
   public Predicate<ChangeData> ref(String ref) {
+    if (ref.startsWith("^"))
+      return new RegexRefPredicate(args.dbProvider, ref);
     return new RefPredicate(args.dbProvider, ref);
   }
 
@@ -276,6 +289,11 @@
   }
 
   @Operator
+  public Predicate<ChangeData> message(String text) {
+    return new MessagePredicate(args.dbProvider, args.repoManager, text);
+  }
+
+  @Operator
   public Predicate<ChangeData> starredby(String who)
       throws QueryParseException, OrmException {
     Account account = args.accountResolver.find(who);
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 904bc3c..6fbd060 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
@@ -38,7 +38,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, null, null, null), null));
 
   private final Provider<ReviewDb> dbProvider;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java
new file mode 100644
index 0000000..0490127
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java
@@ -0,0 +1,124 @@
+// 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.query.change;
+
+import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.RevId;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gwtorm.client.OrmException;
+import com.google.inject.Provider;
+
+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.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.revwalk.filter.MessageRevFilter;
+import org.eclipse.jgit.revwalk.filter.RevFilter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+/**
+ * Predicate to match changes that contains specified text in commit messages
+ * body.
+ */
+public class MessagePredicate extends OperatorPredicate<ChangeData> {
+
+  private static final Logger log =
+      LoggerFactory.getLogger(MessagePredicate.class);
+
+  private final Provider<ReviewDb> db;
+  private final GitRepositoryManager repoManager;
+  private final RevFilter rFilter;
+
+  public MessagePredicate(Provider<ReviewDb> db,
+      GitRepositoryManager repoManager, String text) {
+    super(ChangeQueryBuilder.FIELD_MESSAGE, text);
+    this.db = db;
+    this.repoManager = repoManager;
+    this.rFilter = MessageRevFilter.create(text);
+  }
+
+  @Override
+  public boolean match(ChangeData object) throws OrmException {
+    final PatchSet patchSet = object.currentPatchSet(db);
+
+    if (patchSet == null) {
+      return false;
+    }
+
+    final RevId revision = patchSet.getRevision();
+
+    if (revision == null) {
+      return false;
+    }
+
+    final AnyObjectId objectId = ObjectId.fromString(revision.get());
+
+    if (objectId == null) {
+      return false;
+    }
+
+    final Change change = object.change(db);
+
+    if (change == null) {
+      return false;
+    }
+
+    final Project.NameKey projectName = change.getProject();
+
+    if (projectName == null) {
+      return false;
+    }
+
+    try {
+      final Repository repo = repoManager.openRepository(projectName.get());
+      try {
+        final RevWalk rw = new RevWalk(repo);
+        try {
+          return rFilter.include(rw, rw.parseCommit(objectId));
+        } finally {
+          rw.release();
+        }
+      } finally {
+        repo.close();
+      }
+    } catch (RepositoryNotFoundException e) {
+      log.error("Repository \"" + projectName.get() + "\" unknown.", e);
+    } catch (MissingObjectException e) {
+      log.error(projectName.get() + "\" commit does not exist.", e);
+    } catch (IncorrectObjectTypeException e) {
+      log.error(projectName.get() + "\" revision is not a commit.", e);
+    } catch (IOException e) {
+      log.error("Could not search for commit message in \"" + projectName.get()
+          + "\" repository.", e);
+    }
+
+    return false;
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
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 3433fa9..6c9fbc0 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
@@ -20,6 +20,7 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.events.ChangeAttribute;
 import com.google.gerrit.server.events.EventFactory;
+import com.google.gerrit.server.events.PatchSetAttribute;
 import com.google.gerrit.server.events.QueryStats;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryParseException;
@@ -69,6 +70,7 @@
   private OutputFormat outputFormat = OutputFormat.TEXT;
   private boolean includePatchSets;
   private boolean includeCurrentPatchSet;
+  private boolean includeApprovals;
 
   private OutputStream outputStream = DisabledOutputStream.INSTANCE;
   private PrintWriter out;
@@ -91,6 +93,10 @@
     includeCurrentPatchSet = on;
   }
 
+  public void setIncludeApprovals(boolean on) {
+    includeApprovals = on;
+  }
+
   public void setOutput(OutputStream out, OutputFormat fmt) {
     this.outputStream = out;
     this.outputFormat = fmt;
@@ -151,7 +157,8 @@
           eventFactory.addTrackingIds(c, d.trackingIds(db));
 
           if (includePatchSets) {
-            eventFactory.addPatchSets(c, d.patches(db));
+            eventFactory.addPatchSets(c, d.patches(db),
+              includeApprovals ? d.approvalsMap(db) : null);
           }
 
           if (includeCurrentPatchSet) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexBranchPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexBranchPredicate.java
new file mode 100644
index 0000000..a18d43a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexBranchPredicate.java
@@ -0,0 +1,60 @@
+// 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.query.change;
+
+import com.google.gerrit.reviewdb.Branch;
+import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gwtorm.client.OrmException;
+import com.google.inject.Provider;
+
+import dk.brics.automaton.RegExp;
+import dk.brics.automaton.RunAutomaton;
+
+class RegexBranchPredicate extends OperatorPredicate<ChangeData> {
+  private final Provider<ReviewDb> dbProvider;
+  private final RunAutomaton pattern;
+
+  RegexBranchPredicate(Provider<ReviewDb> dbProvider, String re) {
+    super(ChangeQueryBuilder.FIELD_BRANCH, re);
+
+    if (re.startsWith("^")) {
+      re = re.substring(1);
+    }
+
+    if (re.endsWith("$") && !re.endsWith("\\$")) {
+      re = re.substring(0, re.length() - 1);
+    }
+
+    this.dbProvider = dbProvider;
+    this.pattern = new RunAutomaton(new RegExp(re).toAutomaton());
+  }
+
+  @Override
+  public boolean match(final ChangeData object) throws OrmException {
+    Change change = object.change(dbProvider);
+    if (change == null) {
+      return false;
+    }
+    return change.getDest().get().startsWith(Branch.R_HEADS)
+        && pattern.run(change.getDest().getShortName());
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexProjectPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexProjectPredicate.java
new file mode 100644
index 0000000..c35b66e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexProjectPredicate.java
@@ -0,0 +1,61 @@
+// 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.query.change;
+
+import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gwtorm.client.OrmException;
+import com.google.inject.Provider;
+
+import dk.brics.automaton.RegExp;
+import dk.brics.automaton.RunAutomaton;
+
+class RegexProjectPredicate extends OperatorPredicate<ChangeData> {
+  private final Provider<ReviewDb> dbProvider;
+  private final RunAutomaton pattern;
+
+  RegexProjectPredicate(Provider<ReviewDb> dbProvider, String re) {
+    super(ChangeQueryBuilder.FIELD_PROJECT, re);
+
+    if (re.startsWith("^")) {
+      re = re.substring(1);
+    }
+
+    if (re.endsWith("$") && !re.endsWith("\\$")) {
+      re = re.substring(0, re.length() - 1);
+    }
+
+    this.dbProvider = dbProvider;
+    this.pattern = new RunAutomaton(new RegExp(re).toAutomaton());
+  }
+
+  @Override
+  public boolean match(final ChangeData object) throws OrmException {
+    Change change = object.change(dbProvider);
+    if (change == null) {
+      return false;
+    }
+
+    Project.NameKey p = change.getDest().getParentKey();
+    return pattern.run(p.get());
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexRefPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexRefPredicate.java
new file mode 100644
index 0000000..e9e9958
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexRefPredicate.java
@@ -0,0 +1,58 @@
+// 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.query.change;
+
+import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gwtorm.client.OrmException;
+import com.google.inject.Provider;
+
+import dk.brics.automaton.RegExp;
+import dk.brics.automaton.RunAutomaton;
+
+class RegexRefPredicate extends OperatorPredicate<ChangeData> {
+  private final Provider<ReviewDb> dbProvider;
+  private final RunAutomaton pattern;
+
+  RegexRefPredicate(Provider<ReviewDb> dbProvider, String re) {
+    super(ChangeQueryBuilder.FIELD_REF, re);
+
+    if (re.startsWith("^")) {
+      re = re.substring(1);
+    }
+
+    if (re.endsWith("$") && !re.endsWith("\\$")) {
+      re = re.substring(0, re.length() - 1);
+    }
+
+    this.dbProvider = dbProvider;
+    this.pattern = new RunAutomaton(new RegExp(re).toAutomaton());
+  }
+
+  @Override
+  public boolean match(final ChangeData object) throws OrmException {
+    Change change = object.change(dbProvider);
+    if (change == null) {
+      return false;
+    }
+    return pattern.run(change.getDest().get());
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java
new file mode 100644
index 0000000..e16088c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java
@@ -0,0 +1,58 @@
+// 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.query.change;
+
+import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gwtorm.client.OrmException;
+import com.google.inject.Provider;
+
+import dk.brics.automaton.RegExp;
+import dk.brics.automaton.RunAutomaton;
+
+class RegexTopicPredicate extends OperatorPredicate<ChangeData> {
+  private final Provider<ReviewDb> dbProvider;
+  private final RunAutomaton pattern;
+
+  RegexTopicPredicate(Provider<ReviewDb> dbProvider, String re) {
+    super(ChangeQueryBuilder.FIELD_TOPIC, re);
+
+    if (re.startsWith("^")) {
+      re = re.substring(1);
+    }
+
+    if (re.endsWith("$") && !re.endsWith("\\$")) {
+      re = re.substring(0, re.length() - 1);
+    }
+
+    this.dbProvider = dbProvider;
+    this.pattern = new RunAutomaton(new RegExp(re).toAutomaton());
+  }
+
+  @Override
+  public boolean match(final ChangeData object) throws OrmException {
+    Change change = object.change(dbProvider);
+    if (change == null || change.getTopic() == null) {
+      return false;
+    }
+    return pattern.run(change.getTopic());
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
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 302e22b..9a266ab 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
@@ -150,12 +150,23 @@
     c.accountGroupNames().insert(
         Collections.singleton(new AccountGroupName(batchUsers)));
 
+    final AccountGroup owners =
+        new AccountGroup(new AccountGroup.NameKey("Project Owners"),
+            new AccountGroup.Id(c.nextAccountGroupId()));
+    owners.setDescription("Any owner of the project");
+    owners.setOwnerGroupId(admin.getId());
+    owners.setType(AccountGroup.Type.SYSTEM);
+    c.accountGroups().insert(Collections.singleton(owners));
+    c.accountGroupNames().insert(
+        Collections.singleton(new AccountGroupName(owners)));
+
     final SystemConfig s = SystemConfig.create();
     s.registerEmailPrivateKey = SignedToken.generateRandomKey();
     s.adminGroupId = admin.getId();
     s.anonymousGroupId = anonymous.getId();
     s.registeredGroupId = registered.getId();
     s.batchUsersGroupId = batchUsers.getId();
+    s.ownerGroupId = owners.getId();
     s.wildProjectName = DEFAULT_WILD_NAME;
     try {
       s.sitePath = site_path.getCanonicalPath();
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 2f8d4ca..b7328b5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
@@ -32,7 +32,7 @@
 /** A version of the database schema. */
 public abstract class SchemaVersion {
   /** The current schema version. */
-  private static final Class<? extends SchemaVersion> C = Schema_40.class;
+  private static final Class<? extends SchemaVersion> C = Schema_47.class;
 
   public static class Module extends AbstractModule {
     @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_41.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_41.java
new file mode 100644
index 0000000..508db43
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_41.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class Schema_41 extends SchemaVersion {
+  @Inject
+  Schema_41(Provider<Schema_40> prior) {
+    super(prior);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_42.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_42.java
new file mode 100644
index 0000000..83bca7b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_42.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class Schema_42 extends SchemaVersion {
+  @Inject
+  Schema_42(Provider<Schema_41> prior) {
+    super(prior);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_43.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_43.java
new file mode 100644
index 0000000..0edb7e5
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_43.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class Schema_43 extends SchemaVersion {
+  @Inject
+  Schema_43(Provider<Schema_42> prior) {
+    super(prior);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_44.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_44.java
new file mode 100644
index 0000000..4ab1986
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_44.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class Schema_44 extends SchemaVersion {
+  @Inject
+  Schema_44(Provider<Schema_43> prior) {
+    super(prior);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_45.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_45.java
new file mode 100644
index 0000000..e37e87d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_45.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class Schema_45 extends SchemaVersion {
+  @Inject
+  Schema_45(Provider<Schema_44> prior) {
+    super(prior);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_46.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_46.java
new file mode 100644
index 0000000..8730b4e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_46.java
@@ -0,0 +1,62 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.AccountGroupName;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Collections;
+
+public class Schema_46 extends SchemaVersion {
+
+  @Inject
+  Schema_46(final Provider<Schema_45> prior) {
+    super(prior);
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui) throws SQLException,
+      OrmException {
+    AccountGroup.Id groupId = new AccountGroup.Id(db.nextAccountGroupId());
+
+    // update system_config
+    final Connection connection = ((JdbcSchema) db).getConnection();
+    final Statement stmt = connection.createStatement();
+    stmt.execute("UPDATE system_config SET OWNER_GROUP_ID = " + groupId.get());
+    final ResultSet resultSet =
+      stmt.executeQuery("SELECT ADMIN_GROUP_ID FROM system_config");
+    resultSet.next();
+    final int adminGroupId = resultSet.getInt(1);
+
+    // create 'Project Owners' group
+    AccountGroup.NameKey nameKey = new AccountGroup.NameKey("Project Owners");
+    AccountGroup group = new AccountGroup(nameKey, groupId);
+    group.setType(AccountGroup.Type.SYSTEM);
+    group.setOwnerGroupId(new AccountGroup.Id(adminGroupId));
+    group.setDescription("Any owner of the project");
+    AccountGroupName gn = new AccountGroupName(group);
+    db.accountGroupNames().insert(Collections.singleton(gn));
+    db.accountGroups().insert(Collections.singleton(group));
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_47.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_47.java
new file mode 100644
index 0000000..124cc02
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_47.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class Schema_47 extends SchemaVersion {
+  @Inject
+  Schema_47(Provider<Schema_46> prior) {
+    super(prior);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/CategoryFunction.java b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/CategoryFunction.java
index dd29f04..6700393 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/CategoryFunction.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/CategoryFunction.java
@@ -33,6 +33,7 @@
     all.put(MaxWithBlock.NAME, new MaxWithBlock());
     all.put(MaxNoBlock.NAME, new MaxNoBlock());
     all.put(NoOpFunction.NAME, new NoOpFunction());
+    all.put(NoBlock.NAME, new NoBlock());
   }
 
   /**
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
new file mode 100644
index 0000000..a089817
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/NoBlock.java
@@ -0,0 +1,34 @@
+// 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.server.CurrentUser;
+
+/** 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);
+  }
+
+  @Override
+  public boolean isValid(final CurrentUser user, final ApprovalType at,
+      final FunctionState state) {
+    return true;
+  }
+}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Abandoned.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Abandoned.vm
new file mode 100644
index 0000000..20529b2
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Abandoned.vm
@@ -0,0 +1,44 @@
+## 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.
+##
+##
+## 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.exmaple file to a .vm
+## file and edit it appropriately.
+##
+## This Template:
+## --------------
+## The Abandoned.vm template will determine the contents of the email related
+## to a change being abandoned.   It is a ChangeEmail: see ChangeSubject.vm and
+## ChangeFooter.vm.
+##
+$fromName has abandoned this change.
+
+Change subject: $change.subject
+......................................................................
+
+
+#if ($coverLetter)
+$coverLetter
+
+#end
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.vm
new file mode 100644
index 0000000..5b74453
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.vm
@@ -0,0 +1,51 @@
+## 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.
+##
+##
+## 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.exmaple file to a .vm
+## file and edit it appropriately.
+##
+## This Template:
+## --------------
+## The ChangeFooter.vm template will determine the contents of the footer
+## text that will be appended to ALL emails related to changes.
+##
+--
+#if ($email.changeUrl)
+To view, visit $email.changeUrl
+#set ($notblank = 1)
+#end
+#if ($email.settingsUrl)
+To unsubscribe, visit $email.settingsUrl
+#set ($notblank = 1)
+#end
+#if ($notblank == 1)
+
+#end
+Gerrit-MessageType: $messageClass
+Gerrit-Change-Id: $changeId
+Gerrit-PatchSet: $patchSet.patchSetId
+Gerrit-Project: $projectName
+Gerrit-Branch: $branch.shortName
+Gerrit-Owner: $email.getNameEmailFor($change.owner)
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
new file mode 100644
index 0000000..24cc23c
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeSubject.vm
@@ -0,0 +1,37 @@
+## 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.
+##
+##
+## 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.exmaple file to a .vm
+## file and edit it appropriately.
+##
+## This Template:
+## --------------
+## 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
+#end
+Change in $projectName.replaceAll('/.*/', '...')[$branch.shortName]: #elipses(60, $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
new file mode 100644
index 0000000..4ad355b8
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Comment.vm
@@ -0,0 +1,47 @@
+## Copyright (C) 2010 The Android Open Source Project
+##
+## Licensed under the Apache License, Version 2.0 (the "License");
+## you may not use this file except in compliance with the License.
+## You may obtain a copy of the License at
+##
+## http://www.apache.org/licenses/LICENSE-2.0
+##
+## Unless required by applicable law or agreed to in writing, software
+## distributed under the License is distributed on an "AS IS" BASIS,
+## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+## See the License for the specific language governing permissions and
+## limitations under the License.
+##
+##
+## 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.exmaple file to a .vm
+## file and edit it appropriately.
+##
+## This Template:
+## --------------
+## 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.
+##
+#if ($email.coverLetter || $email.inlineComments)
+$fromName has posted comments on this change.
+
+Change subject: $change.subject
+......................................................................
+
+
+#if ($email.coverLetter)
+$email.coverLetter
+
+#end
+#if($email.inlineComments)$email.inlineComments#end
+#end
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/MergeFail.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/MergeFail.vm
new file mode 100644
index 0000000..dfe3d92f
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/MergeFail.vm
@@ -0,0 +1,44 @@
+## 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.
+##
+##
+## 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.exmaple file to a .vm
+## file and edit it appropriately.
+##
+## This Template:
+## --------------
+## The MergeFail.vm template will determine the contents of the email related
+## to a failure upon attempting to merge a change to the head.  It is a
+## ChangeEmail: see ChangeSubject.vm and ChangeFooter.vm.
+##
+$fromName has submitted this change and it FAILED to merge.
+
+Change subject: $change.subject
+......................................................................
+
+
+#if ($email.coverLetter)
+$email.coverLetter
+
+#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
new file mode 100644
index 0000000..296a37a
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Merged.vm
@@ -0,0 +1,44 @@
+## 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.
+##
+##
+## 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.exmaple file to a .vm
+## file and edit it appropriately.
+##
+## This Template:
+## --------------
+## The Merged.vm template will determine the contents of the email related to
+## 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
+......................................................................
+
+
+$email.changeDetail$email.approvals
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.vm
new file mode 100644
index 0000000..a9a5fed
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.vm
@@ -0,0 +1,52 @@
+## 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.
+##
+##
+## 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.exmaple file to a .vm
+## file and edit it appropriately.
+##
+## This Template:
+## --------------
+## The NewChange.vm template will determine the contents of the email related
+## to a user submitting a new change for review. It is a ChangeEmail: see
+## ChangeSubject.vm and ChangeFooter.vm.
+##
+#if($email.reviewerNames)
+Hello $StringUtils.join($email.reviewerNames, ' ,'),
+
+I'd like you to do a code review.#if($email.changeUrl)  Please visit
+
+    $email.changeUrl
+
+to review the following change.
+#end
+#else
+$fromName has uploaded a new change for review.
+#end
+
+Change subject: $change.subject
+......................................................................
+
+$email.changeDetail
+  git pull ssh://$email.sshHost/$projectName $patchSet.refName
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RegisterNewEmail.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RegisterNewEmail.vm
new file mode 100644
index 0000000..c1de87e
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RegisterNewEmail.vm
@@ -0,0 +1,49 @@
+## 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.
+##
+##
+## 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.exmaple file to a .vm
+## file and edit it appropriately.
+##
+## This Template:
+## --------------
+## The RegisterNewEmail.vm template will determine the contents of the email
+## related to registering new email accounts.
+##
+Welcome to Gerrit Code Review at ${email.gerritHost}.
+
+To add a verified email address to your user account, please
+click on the following link:
+
+$email.gerritUrl#VE,$email.emailRegistrationToken
+
+If you have received this mail in error, you do not need to take any
+action to cancel the account.  The account will not be activated, and
+you will not receive any further emails.
+
+If clicking the link above does not work, copy and paste the URL in a
+new browser window instead.
+
+This is a send-only email address.  Replies to this message will not
+be read or answered.
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ReplacePatchSet.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ReplacePatchSet.vm
new file mode 100644
index 0000000..356f02e
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ReplacePatchSet.vm
@@ -0,0 +1,52 @@
+## 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.
+##
+##
+## 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.exmaple file to a .vm
+## file and edit it appropriately.
+##
+## This Template:
+## --------------
+## The ReplacePatchSet.vm template will determine the contents of the email
+## related to a user submitting a new patchset for a change.  It is a
+## ChangeEmail: see ChangeSubject.vm and ChangeFooter.vm.
+##
+#if($email.reviewerNames)
+Hello $StringUtils.join($email.reviewerNames, ' ,'),
+
+I'd like you to reexamine a change.#if($email.changeUrl)  Please visit
+
+    $email.changeUrl
+
+to look at the new patch set (#$patchSet.patchSetId).
+#end
+#else
+$fromName has uploaded a new patch set (#$patchSet.patchSetId).
+#end
+
+Change subject: $change.subject
+......................................................................
+
+$email.changeDetail
+  git pull ssh://$email.sshHost/$projectName $patchSet.refName
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/config/ConfigUtilTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/config/ConfigUtilTest.java
new file mode 100644
index 0000000..386a6d1
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/config/ConfigUtilTest.java
@@ -0,0 +1,76 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import static java.util.concurrent.TimeUnit.DAYS;
+import static java.util.concurrent.TimeUnit.*;
+
+import junit.framework.TestCase;
+
+import java.util.concurrent.TimeUnit;
+
+public class ConfigUtilTest extends TestCase {
+  public void testTimeUnit() {
+    assertEquals(ms(2, MILLISECONDS), parse("2ms"));
+    assertEquals(ms(200, MILLISECONDS), parse("200 milliseconds"));
+
+    assertEquals(ms(2, SECONDS), parse("2s"));
+    assertEquals(ms(231, SECONDS), parse("231sec"));
+    assertEquals(ms(1, SECONDS), parse("1second"));
+    assertEquals(ms(300, SECONDS), parse("300 seconds"));
+
+    assertEquals(ms(2, MINUTES), parse("2m"));
+    assertEquals(ms(2, MINUTES), parse("2min"));
+    assertEquals(ms(1, MINUTES), parse("1 minute"));
+    assertEquals(ms(10, MINUTES), parse("10 minutes"));
+
+    assertEquals(ms(5, HOURS), parse("5h"));
+    assertEquals(ms(5, HOURS), parse("5hr"));
+    assertEquals(ms(1, HOURS), parse("1hour"));
+    assertEquals(ms(48, HOURS), parse("48hours"));
+
+    assertEquals(ms(5, HOURS), parse("5 h"));
+    assertEquals(ms(5, HOURS), parse("5 hr"));
+    assertEquals(ms(1, HOURS), parse("1 hour"));
+    assertEquals(ms(48, HOURS), parse("48 hours"));
+    assertEquals(ms(48, HOURS), parse("48 \t \r hours"));
+
+    assertEquals(ms(4, DAYS), parse("4d"));
+    assertEquals(ms(1, DAYS), parse("1day"));
+    assertEquals(ms(14, DAYS), parse("14days"));
+
+    assertEquals(ms(7, DAYS), parse("1w"));
+    assertEquals(ms(7, DAYS), parse("1week"));
+    assertEquals(ms(14, DAYS), parse("2w"));
+    assertEquals(ms(14, DAYS), parse("2weeks"));
+
+    assertEquals(ms(30, DAYS), parse("1mon"));
+    assertEquals(ms(30, DAYS), parse("1month"));
+    assertEquals(ms(60, DAYS), parse("2mon"));
+    assertEquals(ms(60, DAYS), parse("2months"));
+
+    assertEquals(ms(365, DAYS), parse("1y"));
+    assertEquals(ms(365, DAYS), parse("1year"));
+    assertEquals(ms(365 * 2, DAYS), parse("2years"));
+  }
+
+  private static long ms(int cnt, TimeUnit unit) {
+    return MILLISECONDS.convert(cnt, unit);
+  }
+
+  private static long parse(String string) {
+    return ConfigUtil.getTimeUnit(string, 1, MILLISECONDS);
+  }
+}
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 8c26705..81b1762 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
@@ -16,6 +16,7 @@
 
 import static com.google.gerrit.reviewdb.ApprovalCategory.OWN;
 import static com.google.gerrit.reviewdb.ApprovalCategory.READ;
+import static com.google.gerrit.reviewdb.ApprovalCategory.SUBMIT;
 
 import com.google.gerrit.reviewdb.AccountGroup;
 import com.google.gerrit.reviewdb.AccountProjectWatch;
@@ -50,7 +51,7 @@
 
 public class RefControlTest extends TestCase {
   public void testOwnerProject() {
-    local.add(grant(OWN, admin, "refs/*", 1));
+    grant(local, OWN, admin, "refs/*", 1);
 
     ProjectControl uBlah = user(devs);
     ProjectControl uAdmin = user(devs, admin);
@@ -60,8 +61,8 @@
   }
 
   public void testBranchDelegation1() {
-    local.add(grant(OWN, admin, "refs/*", 1));
-    local.add(grant(OWN, devs, "refs/heads/x/*", 1));
+    grant(local, OWN, admin, "refs/*", 1);
+    grant(local, OWN, devs, "refs/heads/x/*", 1);
 
     ProjectControl uDev = user(devs);
     assertFalse("not owner", uDev.isOwner());
@@ -76,9 +77,9 @@
   }
 
   public void testBranchDelegation2() {
-    local.add(grant(OWN, admin, "refs/*", 1));
-    local.add(grant(OWN, devs, "refs/heads/x/*", 1));
-    local.add(grant(OWN, fixers, "-refs/heads/x/y/*", 1));
+    grant(local, OWN, admin, "refs/*", 1);
+    grant(local, OWN, devs, "refs/heads/x/*", 1);
+    grant(local, OWN, fixers, "-refs/heads/x/y/*", 1);
 
     ProjectControl uDev = user(devs);
     assertFalse("not owner", uDev.isOwner());
@@ -86,9 +87,9 @@
 
     assertOwner("refs/heads/x/*", uDev);
     assertOwner("refs/heads/x/y", uDev);
+    assertOwner("refs/heads/x/y/*", uDev);
     assertNotOwner("refs/*", uDev);
     assertNotOwner("refs/heads/master", uDev);
-    assertNotOwner("refs/heads/x/y/*", uDev);
 
     ProjectControl uFix = user(fixers);
     assertFalse("not owner", uFix.isOwner());
@@ -103,11 +104,11 @@
   }
 
   public void testInheritRead_SingleBranchDeniesUpload() {
-    inherited.add(grant(READ, registered, "refs/*", 1, 2));
-    local.add(grant(READ, registered, "-refs/heads/foobar", 1, 1));
+    grant(parent, READ, registered, "refs/*", 1, 2);
+    grant(local, READ, registered, "-refs/heads/foobar", 1);
 
     ProjectControl u = user();
-    assertTrue("can upload", u.canUploadToAtLeastOneRef());
+    assertTrue("can upload", u.canPushToAtLeastOneRef());
 
     assertTrue("can upload refs/heads/master", //
         u.controlForRef("refs/heads/master").canUpload());
@@ -117,11 +118,11 @@
   }
 
   public void testInheritRead_SingleBranchDoesNotOverrideInherited() {
-    inherited.add(grant(READ, registered, "refs/*", 1, 2));
-    local.add(grant(READ, registered, "refs/heads/foobar", 1, 1));
+    grant(parent, READ, registered, "refs/*", 1, 2);
+    grant(local, READ, registered, "refs/heads/foobar", 1);
 
     ProjectControl u = user();
-    assertTrue("can upload", u.canUploadToAtLeastOneRef());
+    assertTrue("can upload", u.canPushToAtLeastOneRef());
 
     assertTrue("can upload refs/heads/master", //
         u.controlForRef("refs/heads/master").canUpload());
@@ -130,12 +131,54 @@
         u.controlForRef("refs/heads/foobar").canUpload());
   }
 
-  public void testCannotUploadToAnyRef() {
-    inherited.add(grant(READ, registered, "refs/*", 1, 1));
-    local.add(grant(READ, devs, "refs/heads/*",1,2));
+  public void testInheritRead_OverrideWithDeny() {
+    grant(parent, READ, registered, "refs/*", 1);
+    grant(local, READ, registered, "refs/*", 0);
 
     ProjectControl u = user();
-    assertFalse("cannot upload", u.canUploadToAtLeastOneRef());
+    assertFalse("can't read", u.isVisible());
+  }
+
+  public void testInheritRead_AppendWithDenyOfRef() {
+    grant(parent, READ, registered, "refs/*", 1);
+    grant(local, READ, registered, "refs/heads/*", 0);
+
+    ProjectControl u = user();
+    assertTrue("can read", u.isVisible());
+    assertTrue("can read", u.controlForRef("refs/master").isVisible());
+    assertTrue("can read", u.controlForRef("refs/tags/foobar").isVisible());
+    assertTrue("no master", u.controlForRef("refs/heads/master").isVisible());
+  }
+
+  public void testInheritRead_OverridesAndDeniesOfRef() {
+    grant(parent, READ, registered, "refs/*", 1);
+    grant(local, READ, registered, "refs/*", 0);
+    grant(local, READ, registered, "refs/heads/*", -1, 1);
+
+    ProjectControl u = user();
+    assertTrue("can read", u.isVisible());
+    assertFalse("can't read", u.controlForRef("refs/foobar").isVisible());
+    assertFalse("can't read", u.controlForRef("refs/tags/foobar").isVisible());
+    assertTrue("can read", u.controlForRef("refs/heads/foobar").isVisible());
+  }
+
+  public void testInheritSubmit_OverridesAndDeniesOfRef() {
+    grant(parent, SUBMIT, registered, "refs/*", 1);
+    grant(local, SUBMIT, registered, "refs/*", 0);
+    grant(local, SUBMIT, registered, "refs/heads/*", -1, 1);
+
+    ProjectControl u = user();
+    assertFalse("can't submit", u.controlForRef("refs/foobar").canSubmit());
+    assertFalse("can't submit", u.controlForRef("refs/tags/foobar").canSubmit());
+    assertTrue("can submit", u.controlForRef("refs/heads/foobar").canSubmit());
+  }
+
+  public void testCannotUploadToAnyRef() {
+    grant(parent, READ, registered, "refs/*", 1);
+    grant(local, READ, devs, "refs/heads/*", 1, 2);
+
+    ProjectControl u = user();
+    assertFalse("cannot upload", u.canPushToAtLeastOneRef());
     assertFalse("cannot upload refs/heads/master", //
         u.controlForRef("refs/heads/master").canUpload());
   }
@@ -143,22 +186,26 @@
 
   // -----------------------------------------------------------------------
 
-  private final Project.NameKey projectNameKey = new Project.NameKey("test");
+  private final Project.NameKey local = new Project.NameKey("test");
+  private final Project.NameKey parent = new Project.NameKey("parent");
   private final AccountGroup.Id admin = new AccountGroup.Id(1);
   private final AccountGroup.Id anonymous = new AccountGroup.Id(2);
   private final AccountGroup.Id registered = new AccountGroup.Id(3);
+  private final AccountGroup.Id owners = new AccountGroup.Id(4);
 
-  private final AccountGroup.Id devs = new AccountGroup.Id(4);
-  private final AccountGroup.Id fixers = new AccountGroup.Id(5);
+  private final AccountGroup.Id devs = new AccountGroup.Id(5);
+  private final AccountGroup.Id fixers = new AccountGroup.Id(6);
 
+  private final SystemConfig systemConfig;
   private final AuthConfig authConfig;
   private final AnonymousUser anonymousUser;
 
   public RefControlTest() {
-    final SystemConfig systemConfig = SystemConfig.create();
+    systemConfig = SystemConfig.create();
     systemConfig.adminGroupId = admin;
     systemConfig.anonymousGroupId = anonymous;
     systemConfig.registeredGroupId = registered;
+    systemConfig.ownerGroupId = owners;
     systemConfig.batchUsersGroupId = anonymous;
     try {
       byte[] bin = "abcdefghijklmnopqrstuvwxyz".getBytes("UTF-8");
@@ -183,14 +230,14 @@
     anonymousUser = injector.getInstance(AnonymousUser.class);
   }
 
-  private List<RefRight> local;
-  private List<RefRight> inherited;
+  private List<RefRight> localRights;
+  private List<RefRight> inheritedRights;
 
   @Override
   protected void setUp() throws Exception {
     super.setUp();
-    local = new ArrayList<RefRight>();
-    inherited = new ArrayList<RefRight>();
+    localRights = new ArrayList<RefRight>();
+    inheritedRights = new ArrayList<RefRight>();
   }
 
   private static void assertOwner(String ref, ProjectControl u) {
@@ -201,32 +248,48 @@
     assertFalse("NOT OWN " + ref, u.controlForRef(ref).isOwner());
   }
 
-  private RefRight grant(ApprovalCategory.Id categoryId, AccountGroup.Id group,
-      String ref, int maxValue) {
-    return grant(categoryId, group, ref, maxValue, maxValue);
+  private void grant(Project.NameKey project, ApprovalCategory.Id categoryId, 
+      AccountGroup.Id group, String ref, int maxValue) {
+    grant(project, categoryId, group, ref, maxValue, maxValue);
   }
 
-  private RefRight grant(ApprovalCategory.Id categoryId, AccountGroup.Id group,
+  private void grant(Project.NameKey project, ApprovalCategory.Id categoryId, AccountGroup.Id group,
       String ref, int minValue, int maxValue) {
     RefRight right =
-        new RefRight(new RefRight.Key(projectNameKey, new RefPattern(ref),
+        new RefRight(new RefRight.Key(project, new RefPattern(ref),
             categoryId, group));
     right.setMinValue((short) minValue);
     right.setMaxValue((short) maxValue);
-    return right;
+
+    if (project == parent) {
+      inheritedRights.add(right);
+    } else if (project == local) {
+      localRights.add(right);
+    } else {
+      fail("Unknown project key: " + project);
+    }
   }
 
   private ProjectControl user(AccountGroup.Id... memberOf) {
-    return new ProjectControl(new MockUser(memberOf), newProjectState());
+    RefControl.Factory refControlFactory = new RefControl.Factory() {
+      @Override
+      public RefControl create(final ProjectControl projectControl, final String ref) {
+        return new RefControl(systemConfig, projectControl, ref);
+      }
+    };
+    return new ProjectControl(Collections.<AccountGroup.Id> emptySet(),
+        Collections.<AccountGroup.Id> emptySet(), refControlFactory,
+        new MockUser(memberOf), newProjectState());
   }
 
   private ProjectState newProjectState() {
     ProjectCache projectCache = null;
     Project.NameKey wildProject = null;
+    ProjectControl.AssistedFactory projectControlFactory = null;
     ProjectState ps =
-        new ProjectState(anonymousUser, projectCache, wildProject, new Project(
-            projectNameKey), local);
-    ps.setInheritedRights(inherited);
+        new ProjectState(anonymousUser, projectCache, wildProject,
+            projectControlFactory, new Project(parent), localRights);
+    ps.setInheritedRights(inheritedRights);
     return ps;
   }
 
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 1644d22e..e05ce2e 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
@@ -163,6 +163,7 @@
       assertEquals("-- All Projects --", all.getName());
       assertFalse(all.isUseContributorAgreements());
       assertFalse(all.isUseSignedOffBy());
+      assertFalse(all.isRequireChangeID());
     } finally {
       c.close();
     }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/CommitMsgHookTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/CommitMsgHookTest.java
index 4738802..0d1b6f9 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/CommitMsgHookTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/CommitMsgHookTest.java
@@ -19,11 +19,11 @@
 import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.dircache.DirCacheBuilder;
 import org.eclipse.jgit.dircache.DirCacheEntry;
-import org.eclipse.jgit.lib.Commit;
+import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectWriter;
+import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.RefUpdate;
 
@@ -144,7 +144,7 @@
         "Change-Id: I7fc3876fee63c766a2063df97fbe04a2dddd8d7c\n",//
         call("a\n"));
 
-    final DirCacheBuilder builder = DirCache.lock(repository).builder();
+    final DirCacheBuilder builder = repository.lockDirCache().builder();
     builder.add(file("A"));
     assertTrue(builder.commit());
 
@@ -386,35 +386,41 @@
   }
 
   private DirCacheEntry file(final String name) throws IOException {
-    final DirCacheEntry e = new DirCacheEntry(name);
-    e.setFileMode(FileMode.REGULAR_FILE);
-    e.setObjectId(writer().writeBlob(Constants.encode(name)));
-    return e;
-  }
-
-  private void setHEAD() throws Exception {
-    final ObjectWriter ow = writer();
-    final Commit commit = new Commit(repository);
-    commit.setTreeId(DirCache.newInCore().writeTree(ow));
-    commit.setAuthor(author);
-    commit.setCommitter(committer);
-    commit.setMessage("test\n");
-    final ObjectId commitId = ow.writeCommit(commit);
-
-    final RefUpdate ref = repository.updateRef(Constants.HEAD);
-    ref.setNewObjectId(commitId);
-    switch (ref.forceUpdate()) {
-      case NEW:
-      case FAST_FORWARD:
-      case FORCED:
-      case NO_CHANGE:
-        break;
-      default:
-        fail(Constants.HEAD + " did not change: " + ref.getResult());
+    final ObjectInserter oi = repository.newObjectInserter();
+    try {
+      final DirCacheEntry e = new DirCacheEntry(name);
+      e.setFileMode(FileMode.REGULAR_FILE);
+      e.setObjectId(oi.insert(Constants.OBJ_BLOB, Constants.encode(name)));
+      oi.flush();
+      return e;
+    } finally {
+      oi.release();
     }
   }
 
-  private ObjectWriter writer() {
-    return new ObjectWriter(repository);
+  private void setHEAD() throws Exception {
+    final ObjectInserter oi = repository.newObjectInserter();
+    try {
+      final CommitBuilder commit = new CommitBuilder();
+      commit.setTreeId(oi.insert(Constants.OBJ_TREE, new byte[] {}));
+      commit.setAuthor(author);
+      commit.setCommitter(committer);
+      commit.setMessage("test\n");
+      ObjectId commitId = oi.insert(commit);
+
+      final RefUpdate ref = repository.updateRef(Constants.HEAD);
+      ref.setNewObjectId(commitId);
+      switch (ref.forceUpdate()) {
+        case NEW:
+        case FAST_FORWARD:
+        case FORCED:
+        case NO_CHANGE:
+          break;
+        default:
+          fail(Constants.HEAD + " did not change: " + ref.getResult());
+      }
+    } finally {
+      oi.release();
+    }
   }
 }
diff --git a/gerrit-sshd/pom.xml b/gerrit-sshd/pom.xml
index 2bb4713..32bcf57 100644
--- a/gerrit-sshd/pom.xml
+++ b/gerrit-sshd/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.1.4-SNAPSHOT</version>
+    <version>2.1-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-sshd</artifactId>
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 6c96b70..977d209 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
@@ -135,6 +135,11 @@
       }
     }
 
+    if (!createUser(sd, key).getAccount().isActive()) {
+      sd.authenticationError(username, "inactive-account");
+      return false;
+    }
+
     return success(username, session, sd, createUser(sd, key));
   }
 
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 95c3416..431b420 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/NoShell.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/NoShell.java
@@ -14,15 +14,28 @@
 
 package com.google.gerrit.sshd;
 
+import com.google.gerrit.reviewdb.Account;
+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.inject.Inject;
+import com.google.inject.Provider;
+
 import org.apache.sshd.common.Factory;
 import org.apache.sshd.server.Command;
 import org.apache.sshd.server.Environment;
 import org.apache.sshd.server.ExitCallback;
+import org.apache.sshd.server.SessionAware;
+import org.apache.sshd.server.session.ServerSession;
 import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.util.SystemReader;
 
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
 
 /**
  * Dummy shell which prints a message and terminates.
@@ -32,41 +45,135 @@
  * cannot continue further.
  */
 class NoShell implements Factory<Command> {
+  private final Provider<SendMessage> shell;
+
+  @Inject
+  NoShell(Provider<SendMessage> shell) {
+    this.shell = shell;
+  }
+
   public Command create() {
-    return new Command() {
-      private InputStream in;
-      private OutputStream out;
-      private OutputStream err;
-      private ExitCallback exit;
+    return shell.get();
+  }
 
-      public void setInputStream(final InputStream in) {
-        this.in = in;
+  static class SendMessage implements Command, SessionAware {
+    private final Provider<MessageFactory> messageFactory;
+
+    private InputStream in;
+    private OutputStream out;
+    private OutputStream err;
+    private ExitCallback exit;
+    private Context context;
+
+    @Inject
+    SendMessage(Provider<MessageFactory> messageFactory) {
+      this.messageFactory = messageFactory;
+    }
+
+    public void setInputStream(final InputStream in) {
+      this.in = in;
+    }
+
+    public void setOutputStream(final OutputStream out) {
+      this.out = out;
+    }
+
+    public void setErrorStream(final OutputStream err) {
+      this.err = err;
+    }
+
+    public void setExitCallback(final ExitCallback callback) {
+      this.exit = callback;
+    }
+
+    public void setSession(final ServerSession session) {
+      this.context = new Context(session.getAttribute(SshSession.KEY), "");
+    }
+
+    public void start(final Environment env) throws IOException {
+      Context old = SshScope.set(context);
+      String message;
+      try {
+        message = messageFactory.get().getMessage();
+      } finally {
+        SshScope.set(old);
+      }
+      err.write(Constants.encodeASCII(message.toString()));
+      err.flush();
+
+      in.close();
+      out.close();
+      err.close();
+      exit.onExit(127);
+    }
+
+    public void destroy() {
+    }
+  }
+
+  static class MessageFactory {
+    private final IdentifiedUser user;
+    private final SshInfo sshInfo;
+    private final Provider<String> urlProvider;
+
+    @Inject
+    MessageFactory(IdentifiedUser user, SshInfo sshInfo,
+        @CanonicalWebUrl Provider<String> urlProvider) {
+      this.user = user;
+      this.sshInfo = sshInfo;
+      this.urlProvider = urlProvider;
+    }
+
+    String getMessage() {
+      StringBuilder msg = new StringBuilder();
+
+      msg.append("\r\n");
+      msg.append("  ****    Welcome to Gerrit Code Review    ****\r\n");
+      msg.append("\r\n");
+
+      Account account = user.getAccount();
+      String name = account.getFullName();
+      if (name == null || name.isEmpty()) {
+        name = user.getUserName();
+      }
+      msg.append("  Hi ");
+      msg.append(name);
+      msg.append(", you have successfully connected over SSH.");
+      msg.append("\r\n");
+      msg.append("\r\n");
+
+      msg.append("  Unfortunately, interactive shells are disabled.\r\n");
+      msg.append("  To clone a hosted Git repository, use:\r\n");
+      msg.append("\r\n");
+
+      if (!sshInfo.getHostKeys().isEmpty()) {
+        String host = sshInfo.getHostKeys().get(0).getHost();
+        if (host.startsWith("*:")) {
+          host = getGerritHost() + host.substring(1);
+        }
+
+        msg.append("  git clone ssh://");
+        msg.append(user.getUserName());
+        msg.append("@");
+        msg.append(host);
+        msg.append("/");
+        msg.append("REPOSITORY_NAME.git");
+        msg.append("\r\n");
       }
 
-      public void setOutputStream(final OutputStream out) {
-        this.out = out;
-      }
+      msg.append("\r\n");
+      return msg.toString();
+    }
 
-      public void setErrorStream(final OutputStream err) {
-        this.err = err;
+    private String getGerritHost() {
+      String url = urlProvider.get();
+      if (url != null) {
+        try {
+          return new URL(url).getHost();
+        } catch (MalformedURLException e) {
+        }
       }
-
-      public void setExitCallback(final ExitCallback callback) {
-        this.exit = callback;
-      }
-
-      public void start(final Environment env) throws IOException {
-        err.write(Constants.encodeASCII("gerrit: no shell available\r\n"));
-        err.flush();
-
-        in.close();
-        out.close();
-        err.close();
-        exit.onExit(127);
-      }
-
-      public void destroy() {
-      }
-    };
+      return SystemReader.getInstance().getHostname();
+    }
   }
 }
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 2636ff2..dee55a5 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,7 +14,12 @@
 
 package com.google.gerrit.sshd;
 
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import com.google.gerrit.common.Version;
 import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gerrit.server.util.IdGenerator;
@@ -58,8 +63,11 @@
 import org.apache.sshd.common.util.SecurityUtils;
 import org.apache.sshd.server.Command;
 import org.apache.sshd.server.CommandFactory;
+import org.apache.sshd.server.FileSystemFactory;
+import org.apache.sshd.server.FileSystemView;
 import org.apache.sshd.server.ForwardingFilter;
 import org.apache.sshd.server.PublickeyAuthenticator;
+import org.apache.sshd.server.SshFile;
 import org.apache.sshd.server.UserAuth;
 import org.apache.sshd.server.auth.UserAuthPublicKey;
 import org.apache.sshd.server.channel.ChannelDirectTcpip;
@@ -116,7 +124,7 @@
   private volatile IoAcceptor acceptor;
 
   @Inject
-  SshDaemon(final CommandFactory commandFactory,
+  SshDaemon(final CommandFactory commandFactory, final NoShell noShell,
       final PublickeyAuthenticator userAuth,
       final KeyPairProvider hostKeyProvider, final IdGenerator idGenerator,
       @GerritServerConfig final Config cfg, final SshLog sshLog) {
@@ -126,6 +134,25 @@
     reuseAddress = cfg.getBoolean("sshd", "reuseaddress", true);
     keepAlive = cfg.getBoolean("sshd", "tcpkeepalive", true);
 
+    getProperties().put(SERVER_IDENTIFICATION,
+        "GerritCodeReview_" + Version.getVersion() //
+            + " (" + super.getVersion() + ")");
+
+    getProperties().put(MAX_AUTH_REQUESTS,
+        String.valueOf(cfg.getInt("sshd", "maxAuthTries", 6)));
+
+    getProperties().put(
+        AUTH_TIMEOUT,
+        String.valueOf(MILLISECONDS.convert(ConfigUtil.getTimeUnit(cfg, "sshd",
+            null, "loginGraceTime", 120, SECONDS), SECONDS)));
+
+    final int maxConnectionsPerUser =
+        cfg.getInt("sshd", "maxConnectionsPerUser", 64);
+    if (0 < maxConnectionsPerUser) {
+      getProperties().put(MAX_CONCURRENT_SESSIONS,
+          String.valueOf(maxConnectionsPerUser));
+    }
+
     if (SecurityUtils.isBouncyCastleRegistered()) {
       initProviderBouncyCastle();
     } else {
@@ -136,12 +163,13 @@
     initSignatures();
     initChannels();
     initForwardingFilter();
+    initFileSystemFactory();
     initSubsystems();
     initCompression();
     initUserAuth(userAuth);
     setKeyPairProvider(hostKeyProvider);
     setCommandFactory(commandFactory);
-    setShellFactory(new NoShell());
+    setShellFactory(noShell);
     setSessionFactory(new SessionFactory() {
       @Override
       protected ServerSession createSession(final IoSession io)
@@ -485,4 +513,17 @@
       }
     });
   }
+
+  private void initFileSystemFactory() {
+    setFileSystemFactory(new FileSystemFactory() {
+      @Override
+      public FileSystemView createFileSystemView(String userName) {
+        return new FileSystemView() {
+          @Override
+          public SshFile getFile(String file) {
+            return null;
+          }};
+      }
+    });
+  }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
index 40d271b..c9e9b72 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
@@ -26,6 +26,7 @@
 import com.google.gerrit.server.RemotePeer;
 import com.google.gerrit.server.config.FactoryModule;
 import com.google.gerrit.server.config.GerritRequestModule;
+import com.google.gerrit.server.git.TransferConfig;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.ssh.SshInfo;
@@ -77,7 +78,6 @@
 
     bind(PublickeyAuthenticator.class).to(DatabasePubKeyAuth.class);
     bind(KeyPairProvider.class).toProvider(HostKeyProvider.class).in(SINGLETON);
-    bind(TransferConfig.class);
 
     install(new DefaultCommandModule());
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminShowCaches.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminShowCaches.java
index a78ced5..c27ad0d 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminShowCaches.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminShowCaches.java
@@ -21,7 +21,7 @@
 import net.sf.ehcache.config.CacheConfiguration;
 
 import org.apache.sshd.server.Environment;
-import org.eclipse.jgit.lib.WindowCacheStatAccessor;
+import org.eclipse.jgit.storage.file.WindowCacheStatAccessor;
 
 import java.io.PrintWriter;
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProject.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProject.java
index 6d027d7..5269e43 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProject.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProject.java
@@ -14,12 +14,14 @@
 
 package com.google.gerrit.sshd.commands;
 
+import com.google.gerrit.common.CollectionsUtil;
 import com.google.gerrit.reviewdb.AccountGroup;
 import com.google.gerrit.reviewdb.ApprovalCategory;
 import com.google.gerrit.reviewdb.Project;
 import com.google.gerrit.reviewdb.RefRight;
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.reviewdb.Project.SubmitType;
+import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.ProjectCreatorGroups;
 import com.google.gerrit.server.config.ProjectOwnerGroups;
@@ -31,14 +33,21 @@
 import com.google.inject.Inject;
 
 import org.apache.sshd.server.Environment;
+import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.RefUpdate.Result;
 import org.kohsuke.args4j.Option;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
+import java.io.IOException;
 import java.io.PrintWriter;
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
@@ -46,6 +55,8 @@
 
 /** Create a new project. **/
 final class CreateProject extends BaseCommand {
+  private static final Logger log = LoggerFactory.getLogger(CreateProject.class);
+
   @Option(name = "--name", required = true, aliases = {"-n"}, metaVar = "NAME", usage = "name of project to be created")
   private String projectName;
 
@@ -55,6 +66,9 @@
   @Option(name = "--parent", aliases = {"-p"}, metaVar = "NAME", usage = "parent project")
   private ProjectControl newParent;
 
+  @Option(name = "--permissions-only", usage = "create project for use only as parent")
+  private boolean permissionsOnly;
+
   @Option(name = "--description", aliases = {"-d"}, metaVar = "DESC", usage = "description of project")
   private String projectDescription = "";
 
@@ -68,10 +82,19 @@
   @Option(name = "--use-signed-off-by", aliases = {"--so"}, usage = "if signed-off-by is required")
   private boolean signedOffBy;
 
+  @Option(name = "--use-content-merge", usage = "allow automatic conflict resolving within files")
+  private boolean contentMerge;
+
+  @Option(name = "--require-change-id", aliases = {"--id"}, usage = "if change-id is required")
+  private boolean requireChangeID;
+
   @Option(name = "--branch", aliases = {"-b"}, metaVar = "BRANCH", usage = "initial branch name\n"
       + "(default: master)")
   private String branch = Constants.MASTER;
 
+  @Option(name = "--empty-commit", usage = "to create initial empty commit")
+  private boolean createEmptyCommit;
+
   @Inject
   private ReviewDb db;
 
@@ -92,6 +115,10 @@
   @Inject
   private ReplicationQueue rq;
 
+  @Inject
+  @GerritPersonIdent
+  private PersonIdent serverIdent;
+
   @Override
   public void start(final Environment env) {
     startThread(new CommandRunnable() {
@@ -104,18 +131,30 @@
         try {
           validateParameters();
 
-          Repository repo = repoManager.createRepository(projectName);
-          repo.create(true);
+          if (!permissionsOnly) {
+            final Repository repo = repoManager.createRepository(projectName);
+            try {
+              repo.create(true);
 
-          RefUpdate u = repo.updateRef(Constants.HEAD);
-          u.disableRefLog();
-          u.link(branch);
+              RefUpdate u = repo.updateRef(Constants.HEAD);
+              u.disableRefLog();
+              u.link(branch);
 
-          repoManager.setProjectDescription(projectName, projectDescription);
+              repoManager
+                  .setProjectDescription(projectName, projectDescription);
+
+              final Project.NameKey project = new Project.NameKey(projectName);
+              rq.replicateNewProject(project, branch);
+
+              if (createEmptyCommit) {
+                createEmptyCommit(repo, project, branch);
+              }
+            } finally {
+              repo.close();
+            }
+          }
 
           createProject();
-
-          rq.replicateNewProject(new Project.NameKey(projectName), branch);
         } catch (Exception e) {
           p.print("Error when trying to create project: " + e.getMessage()
               + "\n");
@@ -126,24 +165,36 @@
     });
   }
 
-  /**
-   * Checks if any of the elements in the first collection can be found in the
-   * second collection.
-   *
-   * @param findAnyOfThese which elements to look for.
-   * @param inThisCollection where to look for them.
-   * @param <E> type of the elements in question.
-   * @return {@code true} if any of the elements in {@code findAnyOfThese} can
-   *         be found in {@code inThisCollection}, {@code false} otherwise.
-   */
-  private static <E> boolean isAnyIncludedIn(Collection<E> findAnyOfThese,
-      Collection<E> inThisCollection) {
-    for (E findThisItem : findAnyOfThese) {
-      if (inThisCollection.contains(findThisItem)) {
-        return true;
+  private void createEmptyCommit(final Repository repo,
+      final Project.NameKey project, final String ref) throws IOException {
+    ObjectInserter oi = repo.newObjectInserter();
+    try {
+      CommitBuilder cb = new CommitBuilder();
+      cb.setTreeId(oi.insert(Constants.OBJ_TREE, new byte[] {}));
+      cb.setCommitter(serverIdent);
+      cb.setAuthor(cb.getCommitter());
+      cb.setMessage("Initial empty repository");
+
+      ObjectId id = oi.insert(cb);
+      oi.flush();
+
+      RefUpdate ru = repo.updateRef(Constants.HEAD);
+      ru.setNewObjectId(id);
+      final Result result = ru.update();
+      switch (result) {
+        case NEW:
+          rq.scheduleUpdate(project, ref);
+          break;
+        default: {
+          throw new IOException(result.name());
+        }
       }
+    } catch (IOException e) {
+      log.error("Cannot create empty commit for " + projectName, e);
+      throw e;
+    } finally {
+      oi.release();
     }
-    return false;
   }
 
   private void createProject() throws OrmException {
@@ -166,6 +217,8 @@
     newProject.setSubmitType(submitType);
     newProject.setUseContributorAgreements(contributorAgreements);
     newProject.setUseSignedOffBy(signedOffBy);
+    newProject.setUseContentMerge(contentMerge);
+    newProject.setRequireChangeID(requireChangeID);
     if (newParent != null) {
       newProject.setParent(newParent.getProject().getNameKey());
     }
@@ -179,7 +232,7 @@
           projectName.substring(0, projectName.length() - ".git".length());
     }
 
-    if (!isAnyIncludedIn(currentUser.getEffectiveGroups(), projectCreatorGroups)) {
+    if (!CollectionsUtil.isAnyIncludedIn(currentUser.getEffectiveGroups(), projectCreatorGroups)) {
       throw new Failure(1, "fatal: Not permitted to create " + projectName);
     }
 
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 d2bf26c..fecdc59 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
@@ -43,6 +43,14 @@
     processor.setIncludePatchSets(on);
   }
 
+  @Option(name = "--all-approvals", usage = "Include information about all patch sets and approvals")
+  void setApprovals(boolean on) {
+    if (on) {
+      processor.setIncludePatchSets(on);
+    }
+    processor.setIncludeApprovals(on);
+  }
+
   @Argument(index = 0, required = true, multiValued = true, metaVar = "QUERY", usage = "Query to execute")
   private List<String> query;
 
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 eb5d1da..1996ce4 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
@@ -17,16 +17,23 @@
 import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.ReceiveCommits;
+import com.google.gerrit.server.git.TransferConfig;
+import com.google.gerrit.server.git.VisibleRefFilter;
 import com.google.gerrit.sshd.AbstractGitCommand;
-import com.google.gerrit.sshd.TransferConfig;
 import com.google.inject.Inject;
 
+import org.eclipse.jgit.errors.UnpackException;
+import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.transport.ReceivePack;
+import org.eclipse.jgit.transport.RefFilter;
 import org.kohsuke.args4j.Option;
 
 import java.io.IOException;
 import java.io.InterruptedIOException;
+import java.util.ArrayList;
 import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 /** Receives change upload over SSH using the Git receive-pack protocol. */
@@ -58,6 +65,10 @@
 
   @Override
   protected void runImpl() throws IOException, Failure {
+    if (!projectControl.canRunReceivePack()) {
+      throw new Failure(1, "fatal: receive-pack not permitted on this server");
+    }
+
     final ReceiveCommits receive = factory.create(projectControl, repo);
 
     ReceiveCommits.Capable r = receive.canUpload();
@@ -78,6 +89,49 @@
       rp.receive(in, out, err);
     } catch (InterruptedIOException err) {
       throw new Failure(128, "fatal: client IO read/write timeout", err);
+
+    } catch (UnpackException badStream) {
+      // This may have been triggered by branch level access controls.
+      // Log what the heck is going on, as detailed as we can.
+      //
+      StringBuilder msg = new StringBuilder();
+      msg.append("Unpack error on project \""
+          + projectControl.getProject().getName() + "\":\n");
+
+      msg.append("  RefFilter: " + rp.getRefFilter());
+      if (rp.getRefFilter() == RefFilter.DEFAULT) {
+        msg.append("DEFAULT");
+      } else if (rp.getRefFilter() instanceof VisibleRefFilter) {
+        msg.append("VisibleRefFilter");
+      } else {
+        msg.append(rp.getRefFilter().getClass());
+      }
+      msg.append("\n");
+
+      if (rp.getRefFilter() instanceof VisibleRefFilter) {
+        Map<String, Ref> adv = rp.getAdvertisedRefs();
+        msg.append("  Visible references (" + adv.size() + "):\n");
+        for (Ref ref : adv.values()) {
+          msg.append("  - " + ref.getObjectId().abbreviate(8).name() + " "
+              + ref.getName() + "\n");
+        }
+
+        List<Ref> hidden = new ArrayList<Ref>();
+        for (Ref ref : rp.getRepository().getAllRefs().values()) {
+          if (!adv.containsKey(ref.getName())) {
+            hidden.add(ref);
+          }
+        }
+
+        msg.append("  Hidden references (" + hidden.size() + "):\n");
+        for (Ref ref : hidden) {
+          msg.append("  - " + ref.getObjectId().abbreviate(8).name() + " "
+              + ref.getName() + "\n");
+        }
+      }
+
+      IOException detail = new IOException(msg.toString(), badStream);
+      throw new Failure(128, "fatal: Unpack error, check server log", detail);
     }
   }
 
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 eca797d..bcc9d19 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,9 +15,9 @@
 package com.google.gerrit.sshd.commands;
 
 import com.google.gerrit.reviewdb.ReviewDb;
+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.TransferConfig;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
@@ -36,10 +36,15 @@
 
   @Override
   protected void runImpl() throws IOException, Failure {
+    if (!projectControl.canRunUploadPack()) {
+        throw new Failure(1, "fatal: upload-pack not permitted on this server");
+    }
+
     final UploadPack up = new UploadPack(repo);
     if (!projectControl.allRefsAreVisible()) {
       up.setRefFilter(new VisibleRefFilter(repo, projectControl, db.get()));
     }
+    up.setPackConfig(config.getPackConfig());
     up.setTimeout(config.getTimeout());
     try {
       up.upload(in, out, err);
diff --git a/gerrit-util-cli/pom.xml b/gerrit-util-cli/pom.xml
index 20f28e1..6db5241 100644
--- a/gerrit-util-cli/pom.xml
+++ b/gerrit-util-cli/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.1.4-SNAPSHOT</version>
+    <version>2.1-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-util-cli</artifactId>
diff --git a/gerrit-util-ssl/pom.xml b/gerrit-util-ssl/pom.xml
index 220ae47..39d3ce0 100644
--- a/gerrit-util-ssl/pom.xml
+++ b/gerrit-util-ssl/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.1.4-SNAPSHOT</version>
+    <version>2.1-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-util-ssl</artifactId>
diff --git a/gerrit-war/pom.xml b/gerrit-war/pom.xml
index 4bfa17e..87cfaa5 100644
--- a/gerrit-war/pom.xml
+++ b/gerrit-war/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.1.4-SNAPSHOT</version>
+    <version>2.1-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-war</artifactId>
diff --git a/gerrit-war/src/main/resources/log4j.properties b/gerrit-war/src/main/resources/log4j.properties
index f7a5cdc..5993790 100644
--- a/gerrit-war/src/main/resources/log4j.properties
+++ b/gerrit-war/src/main/resources/log4j.properties
@@ -57,3 +57,7 @@
 log4j.logger.com.mchange.v2.c3p0=WARN
 log4j.logger.com.mchange.v2.resourcepool=WARN
 log4j.logger.com.mchange.v2.sql=WARN
+
+# Silence non-critical messages from Velocity
+#
+log4j.logger.velocity=WARN
diff --git a/pom.xml b/pom.xml
index 633cecb..0f59058 100644
--- a/pom.xml
+++ b/pom.xml
@@ -22,7 +22,7 @@
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-parent</artifactId>
   <packaging>pom</packaging>
-  <version>2.1.4-SNAPSHOT</version>
+  <version>2.1-SNAPSHOT</version>
 
   <name>Gerrit Code Review - Parent</name>
   <url>http://code.google.com/p/gerrit/</url>
@@ -46,7 +46,7 @@
   </issueManagement>
 
   <properties>
-    <jgitVersion>0.8.4.89-ge2f5716</jgitVersion>
+    <jgitVersion>0.9.3.133-gaa09599</jgitVersion>
     <gwtormVersion>1.1.4</gwtormVersion>
     <gwtjsonrpcVersion>1.2.2</gwtjsonrpcVersion>
     <gwtexpuiVersion>1.2.2</gwtexpuiVersion>
@@ -478,7 +478,13 @@
       <dependency>
         <groupId>org.apache.sshd</groupId>
         <artifactId>sshd-core</artifactId>
-        <version>0.4.0-r897374</version>
+        <version>0.5.1-r1031886</version>
+      </dependency>
+
+      <dependency>
+        <groupId>org.apache.velocity</groupId>
+        <artifactId>velocity</artifactId>
+        <version>1.6.4</version>
       </dependency>
 
       <dependency>