Merge branch 'commons-net-20'

* commons-net-20:
  Drop Commons Net back to 2.0
diff --git a/Documentation/Makefile b/Documentation/Makefile
index 18a92b4..72e3f86 100644
--- a/Documentation/Makefile
+++ b/Documentation/Makefile
@@ -32,6 +32,7 @@
 SCRIPTSDIR    := $(shell pwd)/javascript
 COMMIT        := $(shell git describe HEAD | sed s/^v//)
 PUB_DIR       := $(PUB_ROOT)/$(VERSION)
+PRIOR          = PRIOR
 
 ifeq ($(VERSION),)
   REVISION = $(COMMIT)
@@ -52,9 +53,8 @@
 	@-rm -rf $(LOCAL_ROOT)
 	@echo "Checking out current $(VERSION)"
 	@if ! $(SVN) checkout $(PUB_DIR) $(LOCAL_ROOT) 2>/dev/null ; then \
-		p=$$(git describe HEAD^ | perl -lne 'print $$1 if /^v(\d+\.\d+(?:\.\d+)?)/') && \
-		echo "Copying $$p to $(VERSION) ..." && \
-		$(SVN) cp -m "Create $(VERSION) documentation" $(PUB_ROOT)/$$p $(PUB_DIR) && \
+		echo "Copying $(PRIOR) to $(VERSION) ..." && \
+		$(SVN) cp -m "Create $(VERSION) documentation" $(PUB_ROOT)/$(PRIOR) $(PUB_DIR) && \
 		$(SVN) checkout $(PUB_DIR) $(LOCAL_ROOT) ; \
 	fi
 	@rm -f $(LOCAL_ROOT)/*.html
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index 66b4082..b45853d 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -135,10 +135,22 @@
 
 Permissions can be set on a single reference name to match one
 branch (e.g. `refs/heads/master`), or on a reference namespace
-(e.g. `refs/heads/*`) to match any branch starting with that
-prefix. So a permission with `refs/heads/*` will match
+(e.g. `refs/heads/\*`) to match any branch starting with that
+prefix. So a permission with `refs/heads/\*` will match
 `refs/heads/master` and `refs/heads/experimental`, etc.
 
+Reference names can also be described with a regular expression
+by prefixing the reference name with `\^`.  For example
+`\^refs/heads/[a-z]\{1,8\}` matches all lower case branch names
+between 1 and 8 characters long.  Within a regular expression `.`
+is a wildcard matching any character, but may be escaped as `\.`.
+
+References can have the current user name automatically included,
+creating dynamic access controls that change to match the currently
+logged in user.  For example to provide a personal sandbox space
+to all developers, `refs/heads/sandbox/$\{username\}/*` allowing
+the user 'joe' to use 'refs/heads/sandbox/joe/foo'.
+
 When evaluating a reference-level access right, Gerrit will use
 the full set of access rights to determine if the user
 is allowed to perform a given action. For example, if a user is a
diff --git a/Documentation/cmd-index.txt b/Documentation/cmd-index.txt
index 7fbf31b..e213222 100644
--- a/Documentation/cmd-index.txt
+++ b/Documentation/cmd-index.txt
@@ -66,6 +66,9 @@
 link:cmd-ls-projects.html[gerrit ls-projects]::
 	List projects visible to the caller.
 
+link:cmd-query.html[gerrit query]::
+	Query the change database.
+
 link:cmd-review.html[gerrit review]::
 	Verify, approve and/or submit a patch set from the command line.
 
diff --git a/Documentation/cmd-query.txt b/Documentation/cmd-query.txt
new file mode 100644
index 0000000..76a9b7d
--- /dev/null
+++ b/Documentation/cmd-query.txt
@@ -0,0 +1,110 @@
+gerrit query
+============
+
+NAME
+----
+gerrit query - Query the change database
+
+SYNOPSIS
+--------
+[verse]
+'ssh' -p <port> <host> 'gerrit query' \
+[\--format {TEXT | JSON}] \
+[\--current-patch-set] \
+[\--patch-sets] \
+[\--] \
+<query> \
+[limit:<n>] \
+[resume\_sortkey:<sortKey>]
+
+DESCRIPTION
+-----------
+
+Queries the change database and returns results describing changes
+that match the input query.  More recently updated changes appear
+before older changes, which is the same order presented in the
+web interface.
+
+A query may be limited on the number of results it returns with the
+'limit:' operator.  If no limit is supplied an internal default
+limit is used to prevent explosion of the result set.  To obtain
+results beyond the limit, the 'resume_sortkey:' operator can be used
+to resume the query at the change that follows the last change of
+the prior result set.
+
+Non-option arguments to this command are joined with spaces and then
+parsed as a query.  This simplifies calling conventions over SSH
+by permitting operators to appear in different arguments without
+multiple levels of quoting required.
+
+OPTIONS
+-------
+\--current-patch-set::
+	Include information about the current patch set in the results.
+
+\--patch-sets::
+	Include information about all patch sets.  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
+	than one limit: operator is provided, the smallest limit
+	will be used to cut the result set.
+
+resume\_sortkey:<sortKey>::
+	Resume results from this sort key.  Callers should pass
+	the sortKey of the last change of the prior result set to
+	resume a prior query.  This is actually a query operator,
+	and not a command line option.
+
+ACCESS
+------
+Any user who has configured an SSH key.
+
+SCRIPTING
+---------
+This command is intended to be used in scripts.
+
+EXAMPLES
+--------
+
+Find the 2 most recent open changes in the tools/gerrit project:
+-----
+  $ ssh -p 29418 review.example.com gerrit query --format=JSON status:open project:tools/gerrit limit:2
+  {"project":"tools/gerrit", ...}
+  {"project":"tools/gerrit", ..., sortKey:"000e6aee00003e26", ...}
+  {"type":"stats","rowCount":2,"runningTimeMilliseconds:15}
+-----
+
+Resume the same query and obtain the final results:
+-----
+  $ ssh -p 29418 review.example.com gerrit query --format=JSON status:open project:tools/gerrit limit:2 resume_sortkey:000e6aee00003e26
+  {"project":"tools/gerrit", ...}
+  {"project":"tools/gerrit", ...}
+  {"type":"stats","rowCount":1,"runningTimeMilliseconds:15}
+-----
+
+
+SCHEMA
+------
+The JSON messages consist of nested objects referencing the
+link:json.html#change[change],
+link:json.html#patchset[patchset],
+link:json.html#[account]
+involved, and other attributes as appropriate.
+
+Note that any field may be missing in the JSON messages, so consumers
+of this JSON stream should deal with that appropriately.
+
+SEE ALSO
+--------
+
+* link:user-search.html[Query Operators]
+* link:json.html[JSON Data Formats]
+* link:access-control.html[Access Controls]
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-receive-pack.txt b/Documentation/cmd-receive-pack.txt
index 5a5ed6d..ca96550 100644
--- a/Documentation/cmd-receive-pack.txt
+++ b/Documentation/cmd-receive-pack.txt
@@ -64,6 +64,11 @@
 	git push --receive-pack='git receive-pack --reviewer charlie@example.com' ssh://review.example.com:29418/project HEAD:refs/for/master
 =====
 
+Send reviews, but tagging them with the topic name 'bug42':
+=====
+	git push --receive-pack='git receive-pack --reviewer charlie@example.com' ssh://review.example.com:29418/project HEAD:refs/for/master/bug42
+=====
+
 Also CC two other parties:
 =====
 	git push --receive-pack='git receive-pack --reviewer charlie@example.com --cc alice@example.com --cc bob@example.com' ssh://review.example.com:29418/project HEAD:refs/for/master
diff --git a/Documentation/cmd-stream-events.txt b/Documentation/cmd-stream-events.txt
index 2d68d72..0b536fc 100644
--- a/Documentation/cmd-stream-events.txt
+++ b/Documentation/cmd-stream-events.txt
@@ -54,86 +54,48 @@
 ^^^^^^^^^^^^^^
 type:: "patchset-added"
 
-change:: <<change,change attribute>>
+change:: link:json.html#change[change attribute]
 
-patchset:: <<patchset,patchset attribute>>
+patchset:: link:json.html#patchset[patchset attribute]
 
-uploader:: <<account,account attribute>>
+uploader:: link:json.html#account[account attribute]
 
 Change Abandoned
 ^^^^^^^^^^^^^^^^
 type:: "change-abandoned"
 
-change:: <<change,change attribute>>
+change:: link:json.html#change[change attribute]
 
-patchset:: <<patchset,patchset attribute>>
+patchset:: link:json.html#patchset[patchset attribute]
 
-abandoner:: <<account,account attribute>>
+abandoner:: link:json.html#account[account attribute]
 
 Change Merged
 ^^^^^^^^^^^^^
 type:: "change-merged"
 
-change:: <<change,change attribute>>
+change:: link:json.html#change[change attribute]
 
-patchset:: <<patchset,patchset attribute>>
+patchset:: link:json.html#patchset[patchset attribute]
 
-submitter:: <<account,account attribute>>
+submitter:: link:json.html#account[account attribute]
 
 Comment Added
 ^^^^^^^^^^^^^
 type:: "comment-added"
 
-change:: <<change,change attribute>>
+change:: link:json.html#change[change attribute]
 
-patchset:: <<patchset,patchset attribute>>
+patchset:: link:json.html#patchset[patchset attribute]
 
-author:: <<account,account attribute>>
+author:: link:json.html#account[account attribute]
 
 comment:: Comment text author had written
 
-Attributes
-~~~~~~~~~~
-Attributes are part events to give context related to the event.
-
-[[change]]
-change:: The Gerrit change the event is related to
-
-  project;; Project path in Gerrit
-
-  branch;; Branch name within project
-
-  id;; Change identifier
-
-  number;; Change number (deprecated)
-
-  subject;; Description of change
-
-  owner;; Owner in account attribute
-
-  url;; Canonical URL to reach this change
-
-[[account]]
-account:: An account that is related to an event or attribute
-
-  name;; Account user's full name
-
-  email;; Account user's preferred email
-
-[[patchset]]
-patchset:: Refers to a specific patchset within a change
-
-  number;; The patchset number
-
-  revision;; Git commit-ish for this patchset
-
-  ref;; Git reference pointing at revision
-
-  uploader;; Uploader of patch set in account attribute
-
 SEE ALSO
 --------
 
+* link:json.html[JSON Data Formats]
 * link:access-control.html[Access Controls]
 
 GERRIT
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 814d32e..7384a87 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -250,7 +250,6 @@
 Default is `90 days` for most caches, except:
 +
 * `"ldap_groups"`: default is `1 hour`
-* `"openid"`: default is `5 minutes`
 * `"web_sessions"`: default is `12 hours`
 
 [[cache.name.memoryLimit]]cache.<name>.memoryLimit::
@@ -261,7 +260,6 @@
 Default is 1024 for most caches, except:
 +
 * `"diff"`: default is `128`
-* `"openid"`: default is `64`
 
 [[cache.name.diskLimit]]cache.<name>.diskLimit::
 +
@@ -345,13 +343,6 @@
 cache automatically updates when a user first creates their account
 within Gerrit, so the cache expire time is largely irrelevant.
 
-cache `"openid"`::
-+
-If OpenID authentication is enabled, caches the OpenID discovery
-response by URL, for up to 5 minutes.  This can reduce the time
-required for OpenID authentication through very common providers,
-such as Google Accounts.
-
 cache `"projects"`::
 +
 Caches the project description records, from the `projects` table
diff --git a/Documentation/index.txt b/Documentation/index.txt
index 856c974..1e13e8d 100644
--- a/Documentation/index.txt
+++ b/Documentation/index.txt
@@ -5,6 +5,7 @@
 ----------
 
 * link:http://source.android.com/submit-patches/workflow[Default Workflow]
+* link:user-search.html[Searching Changes]
 * link:cmd-index.html[Command Line Tools]
 * link:pgm-index.html[Server Programs]
 * link:user-upload.html[Uploading Changes]
diff --git a/Documentation/json.txt b/Documentation/json.txt
new file mode 100644
index 0000000..1c9a808
--- /dev/null
+++ b/Documentation/json.txt
@@ -0,0 +1,118 @@
+Gerrit Code Review - JSON Data
+==============================
+
+Some commands produce JSON data streams intended for other
+applications to consume.  The structures are documented below.
+Note that any field may be missing in the JSON messages, so consumers
+of this JSON stream should deal with that appropriately.
+
+[[change]]
+change
+------
+The Gerrit change being reviewed, or that was already reviewed.
+
+project:: Project path in Gerrit
+
+branch:: Branch name within project
+
+topic:: Topic name specified by the uploader for this change series
+
+id:: Change identifier, as scraped out of the Change-Id field in
+the commit message, or as assigned by the server if it was missing.
+
+number:: Change number (deprecated)
+
+subject:: Description of change
+
+owner:: Owner in <<account,account attribute>>
+
+url:: Canonical URL to reach this change
+
+lastUpdated:: Time in seconds since the UNIX epoch when this change
+was last updated.
+
+sortKey:: Internal key used to sort changes, based on lastUpdated.
+
+open:: Boolean indicating if the change is still open for review.
+
+status:: Current state of this change.
+
+  NEW;; Change is still being reviewed.
+
+  SUBMITTED;; Change has been submitted and is in the merge queue.
+  It may be waiting for one or more dependencies.
+
+  MERGED;; Change has been merged to its branch.
+
+  ABANDONED;; Change was abandoned by its owner or administrator.
+
+trackingIds:: Issue tracking system links in
+<<trackingid,trackingid attribute>>, scraped out of the commit
+message based on the server's
+link:config-gerrit.html#trackingid[trackingid] sections.
+
+currentPatchSet:: Current <<patchset,patchset attribute>>.
+
+patchSets:: All <<patchset,patchset attribute>> for this change.
+
+[[trackingid]]
+trackingid
+----------
+A link to an issue tracking system.
+
+system:: Name of the system.  This comes straight from the
+gerrit.config file.
+
+id:: Id number as scraped out of the commit message.
+
+[[account]]
+account
+-------
+A user account.
+
+name:: User's full name, if configured.
+
+email:: User's preferred email address.
+
+[[patchset]]
+patchset
+--------
+Refers to a specific patchset within a <<change,change>>.
+
+number:: The patchset number.
+
+revision:: Git commit for this patchset.
+
+ref:: Git reference pointing at the revision.  This reference is
+available through the Gerrit Code Review server's Git interface
+for the containing change.
+
+uploader:: Uploader of the patch set in <<account,account attribute>>.
+
+approvals:: The <<approval,approval attribute>> granted.
+
+[[approval]]
+approval
+--------
+Records the code review approval granted to a patch set.
+
+type:: Internal name of the approval given.
+
+description::  Human readable category of the approval.
+
+value:: Value assigned by the approval, usually a numerical score.
+
+grantedOn:: Time in seconds since the UNIX epoch when this approval
+was added or last updated.
+
+by:: Reviewer of the patch set in <<account,account attribute>>.
+
+SEE ALSO
+--------
+
+* link:cmd-stream-events.html[gerrit stream-events]
+* link:cmd-query.html[gerrit query]
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
index 4427f87..2c123f8 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -22,6 +22,7 @@
 Apache Commons Codec        <<apache2,Apache License 2.0>>
 Apache Commons DBCP         <<apache2,Apache License 2.0>>
 Apache Commons Http Client  <<apache2,Apache License 2.0>>
+Apache Commons Lang         <<apache2,Apache License 2.0>>
 Apache Commons Logging      <<apache2,Apache License 2.0>>
 Apache Commons Net          <<apache2,Apache License 2.0>>
 Apache Commons Pool         <<apache2,Apache License 2.0>>
@@ -48,6 +49,7 @@
 juniversalchardet           <<mpl1_1,MPL 1.1>>
 AOP Alliance                Public Domain
 JSR 305                     <<jsr305,New-Style BSD>>
+dk.brics.automaton          <<automaton,New-Style BSD>>
 -----------------------------------------------------------
 
 Cryptography Notice
@@ -560,6 +562,43 @@
 POSSIBILITY OF SUCH DAMAGE.
 ----
 
+[[automaton]]
+dk.brics.automaton - New Style BSD
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+* link:http://www.brics.dk/automaton/index.html
+
+----
+Copyright (c) 2007-2009, dk.brics.automaton
+All rights reserved.
+
+http://www.opensource.org/licenses/bsd-license.php
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice,
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice,
+      this list of conditions and the following disclaimer in the documentation
+      and/or other materials provided with the distribution.
+    * Neither the name of the JSR305 expert group nor the names of its
+      contributors may be used to endorse or promote products derived from
+      this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+----
+
 [[args4j]]
 args4j - MIT License
 ~~~~~~~~~~~~~~~~~~~~
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
new file mode 100644
index 0000000..c62e894
--- /dev/null
+++ b/Documentation/user-search.txt
@@ -0,0 +1,362 @@
+Gerrit Code Review - Searching Changes
+======================================
+
+Default Searches 
+----------------
+
+Most basic searches can be viewed by clicking on a link along the top
+menu bar.  The link will prefill the search box with a common search
+query, execute it, and present the results.  If exactly one change
+matches the search, the change will be presented instead of a list.
+
+
+[grid="all"]
+`---------------------------`------------------------------
+Description                 Default Query
+-----------------------------------------------------------
+All > Open                  status:open '(or is:open)'
+All > Merged                status:merged
+All > Abandoned             status:abandoned
+My > Dafts                  has:draft
+My > Watched Changes        status:open is:watched
+My > Starred Changes        is:starred
+Open changes in Foo         status:open project:Foo
+-----------------------------------------------------------
+
+Basic Change Search
+-------------------
+
+Similar to many popular search engines on the web, just enter some
+text and let Gerrit figure out the meaning:
+
+[grid="all"]
+`---------------------------------`------------------------------
+Description                       Examples
+-----------------------------------------------------------------
+Legacy numerical id               15183
+Full or abbreviated Change-Id     Ic0ff33
+Full or abbreviated commit SHA-1  d81b32ef
+Email address                     user@example.com
+Approval requirement              CodeReview>=+2, Verified=1
+-----------------------------------------------------------------
+
+
+Search Operators
+----------------
+
+Operators act as restrictions on the search.  As more operators
+are added to the same query string, they further restrict the
+returned results.
+
+[[age]]
+age:'AGE'::
++
+Amount of time that has expired since the change was last updated
+with a review comment or new patch set.  The age must be specified
+to include a unit suffix, for example `age:2d`:
++
+* s, sec, second, seconds
+* m, min, minute, minutes
+* h, hr, hour, hours
+* d, day, days
+* w, week, weeks (`1 week` is treated as `7 days`)
+* mon, month, months (`1 month` is treated as `30 days`)
+* y, year, years (`1 year` is treated as `365 days`)
+
+[[change]]
+change:'ID'::
++
+Either a legacy numerical 'ID' such as 15183, or a newer style
+Change-Id that was scraped out of the commit message.
+
+[[owner]]
+owner:'USER'::
++
+Changes originally submitted by 'USER'.
+
+[[reviewer]]
+reviewer:'USER'::
++
+Changes that have been, or need to be, reviewed by 'USER'.
+
+[[commit]]
+commit:'SHA1'::
++
+Changes where 'SHA1' is one of the patch sets of the change.
+
+[[project]]
+project:'PROJECT'::
++
+Changes occuring in 'PROJECT'.
+
+[[branch]]
+branch:'BRANCH'::
++
+Changes for 'BRANCH'.  The branch name is the short name shown
+in the web interface, without the traditional 'refs/heads/'
+prefix.  This operator is a shorthand for 'refs:'.  Searching for
+'branch:master' really means 'ref:refs/heads/master', and searching
+for 'branch:refs/heads/master' is the same as searching for
+'ref:refs/heads/refs/heads/master'.
+
+[[topic]]
+topic:'TOPIC'::
++
+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.
+
+[[ref]]
+ref:'REF'::
++
+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/'.
+
+[[tr]][[bug]]
+tr:'ID', bug:'ID'::
++
+Search for changes whose commit message contains 'ID' and matched
+one or more of the
+link:config-gerrit.html#trackingid[trackingid sections]
+in the server's configuration file.  This is typically used to
+search for changes that fix a bug or defect by the issue tracking
+system's issue identifier.
+
+[[label]]
+label:'VALUE'::
++
+Matches changes where the approval score 'VALUE' has been set during
+a review.  See <<labels,labels>> below for more detail on the format
+of the argument.
+
+[[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$`.
+Currently this operator is only available on a watched project
+and may not be used in the search bar.
+
+[[has]]
+has:draft::
++
+True if there is a draft comment saved by the current user.
+
+has:star::
++
+Same as 'is:starred', true if the change has been starred by the
+current user.
+
+[[is]]
+is:starred::
++
+Same as 'has:star', true if the change has been starred by the
+current user.
+
+is:watched::
++
+True if this change matches one of the current user's watch filters,
+and thus is likely to notify the user when it updates.
+
+is:reviewed::
++
+True if there is at least one non-zero score on the change, in any
+approval category, by any user.
+
+is:open::
++
+True if the change is other open or submitted, merge pending.
+
+is:closed::
++
+True if the change is either merged or abandoned.
+
+is:submitted, is:merged, is:abandoned::
++
+Same as <<status,status:'STATE'>>.
+
+[[status]]
+status:open::
++
+True if the change state is other 'review in progress' or 'submitted,
+merge pending'.
+
+status:reviewed::
++
+Same as 'is:reviewed', matches if there is at least one non-zero
+score on the change, in any approval category, by any user.
+
+status:submitted::
++
+Change has been submitted, but is waiting for a dependency.
+
+status:closed::
++
+True if the change is either 'merged' or 'abandoned'.
+
+status:merged::
++
+Change has been merged into the branch.
+
+status:abandoned::
++
+Change has been abandoned by the change owner, or administrator.
+
+
+Boolean Operators
+-----------------
+
+Unless otherwise specified, operators are joined using the `AND`
+boolean operator, thereby restricting the search results.
+
+Parentheses can be used to force a particular precendence on complex
+operator expressions, otherwise OR has higher precendence than AND.
+
+Negation
+~~~~~~~~
+Any operator can be negated by prefixing it with `-`, for example
+`-is:starred` is the exact opposite of `is:starred` and will
+therefore return changes that are *not* starred by the current user.
+
+The operator `NOT` (in all caps) is a synonym.
+
+AND
+~~~
+The boolean operator `AND` (in all caps) can be used to join two
+other operators together.  This results in a restriction of the
+results, returning only changes that match both operators.
+
+OR
+~~
+The boolean operator `OR` (in all caps) can be used to find changes
+that match either operator.  This increases the nubmer of results
+that are returned, as more changes are considered.
+
+
+[[labels]]
+Labels
+------
+Label operators can be used to match approval score given during
+a code review.  The specific set of supported labels depends on
+the server configuration, however `CodeReview` and `Verified`
+are the default labels provided out of the box.
+
+A label name is any of the following:
+
+* The category name.  If the category name contains spaces,
+  it must be wrapped in double quotes.  Example: `label:"Code Review"`.
+
+* The name, without spaces.  This avoids needing to use double quotes
+  for the common category Code Review.  Example: `label:CodeReview`.
+
+* The internal short name.  Example: `label:CRVW`, or `label:VRIF`.
+
+* The one or two character abbreviation shown in the column header
+  of change list pages.  Example: `label:R` or `label:V`.
+
+A label name must be followed by a score, or an operator and a score.
+The easiest way to explain these are by example.
+
+`label:CodeReview=2`::
+`label:CodeReview=+2`::
+`label:CodeReview+2`::
++
+Matches changes where there is at least one \+2 score for Code Review.
+The \+ prefix is optional for positive score values.  If the + is used,
+the = operator is optional.
+
+`label:CodeReview=-2`::
+`label:CodeReview-2`::
++
+Matches changes where there is at least one -2 score for Code Review.
+Because the negative sign is required, the = operator is optional.
+
+`label:CodeReview=1`::
++
+Matches changes where there is at least one +1 score for Code Review.
+Scores of +2 are not matched, even though they are higher.
+
+`label:CodeReview>=1`::
++
+Matches changes with either a +1, +2, or any higher score.
+
+`label:CodeReview<=-1`::
++
+Matches changes with either a -1, -2, or any lower score.
+
+`is:open CodeReview+2 Verified+1 -Verified-1 -CodeReview-2`::
++
+Matches changes that are ready to be submitted.
+
+`is:open (Verified-1 OR CodeReview-2)`::
++
+Changes that are blocked from submission due to a blocking score.
+
+
+Magical Operators
+-----------------
+
+Most of these operators exist to support features of Gerrit Code
+Review, and are not meant to be accessed by the average end-user.
+However, they are recognized by the query parser, and may prove
+useful in limited contexts to administrators or power-users.
+
+visibleto:'USER-or-GROUP'::
++
+Matches changes that are visible to 'USER' or to anyone who is a
+member of 'GROUP'.  Here group names may be specified as either
+an internal group name, or if LDAP is being used, an external LDAP
+group name.  The value may be wrapped in double quotes to include
+spaces or other special characters.  For example, to match an LDAP
+group: `visibleto:"CN=Developers, DC=example, DC=com"`.
++
+This operator may be useful to test access control rules, however a
+change can only be matched if both the current user and the supplied
+user or group can see it.  This is due to the implicit 'is:visible'
+clause that is always added by the server.
+
+is:visible::
++
+Magical internal flag to prove the current user has access to read
+the change.  This flag is always added to any query.
+
+starredby:'USER'::
++
+Matches changes that have been started by 'USER'.
+
+watchedby:'USER'::
++
+Matches changes that 'USER' has configured watch filters for.
+
+draftby:'USER'::
++
+Matches changes that 'USER' has left unpublished drafts on.
+Since the drafts are unpublished, it is not possible to see the
+draft text, or even how many drafts there are.
+
+limit:'CNT'::
++
+Limit the returned results to no more than 'CNT' records.  This is
+automatically set to the page size configured in the current user's
+preferences.  Including it in a web query may lead to unpredictable
+results with regards to pagination.
+
+resume\_sortkey:'KEY'::
++
+Positions the low level scan routine to start from 'KEY' and
+continue through changes from this point.  This is most often used
+for paginating result sets.  Including this in a web query may lead
+to unpredictable results.
+
+sortkey\_after:'KEY', sortkey\_before:'KEY'::
++
+Restart the low level scan routine from 'KEY'.  This is automatically
+set by the pagination system as the user navigates through results
+of a query.  Including either value in a web query may lead to
+unpredictable results.
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/user-upload.txt b/Documentation/user-upload.txt
index 8034637..10fddd3 100644
--- a/Documentation/user-upload.txt
+++ b/Documentation/user-upload.txt
@@ -116,6 +116,16 @@
 notify them of new changes will be automatically sent an email
 message when the push is completed.
 
+To include a short tag associated with all of the changes in the
+same group, such as the local topic branch name, append it after
+the destination branch name.  In this example the short topic tag
+'driver/i42' will be saved on each change this push creates or
+updates:
+
+====
+  git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/experimental/driver/i42
+====
+
 If you are frequently uploading changes to the same Gerrit server,
 consider adding an SSH host block in `~/.ssh/config` to remember
 your username, hostname and port number.  This permits the use of
diff --git a/ReleaseNotes/ReleaseNotes-2.1.4.txt b/ReleaseNotes/ReleaseNotes-2.1.4.txt
new file mode 100644
index 0000000..d882d56
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.1.4.txt
@@ -0,0 +1,209 @@
+Release notes for Gerrit 2.1.4
+==============================
+
+Gerrit 2.1.4 is now available in the usual location:
+
+link:http://code.google.com/p/gerrit/downloads/list[http://code.google.com/p/gerrit/downloads/list]
+
+Schema Change
+-------------
+
+*WARNING* This release contains multiple schema changes.  To upgrade:
+----
+  java -jar gerrit.war init -d site_path
+----
+
+New Features
+------------
+
+Change Management
+~~~~~~~~~~~~~~~~~
+
+* issue 504 Implement full query operators
++
+The search box now implments a wide range of operators and boolean
+expressions, permitting complex queries such as `is:open CodeReview>=1
+(has:draft OR is:starred)` to locate open changes that have been code
+reviewed, but still have unpublished drafts or were starred by the
+current user.  The full range of supported operators is documented
+in the user guide.
+
+* Change lists now use query operators
++
+All current change lists have been reimplemented using query
+operators, so selecting 'All open changes' actually performs the query
+'is:open'.  This is to help end-users learn the different operators
+that are supported, and simplifies the internal implementation
+considerably by removing redundant code.
+
+* issue 51 Tag changes with topic branches
++
+Changes can be tagged with a topic name during upload.  To add the tag
+'query' when pushing to branch 'master', use `git push URL
+HEAD:refs/for/master/query`.  To add a topic name with `repo upload`
+use the `-t` command line flag.  Topic names are displayed next to the
+branch name in the web UI, and can be searched for with the `topic:`
+query operator.
+
+* Filter the list of open changes by watched projects
++
+The query operator `is:watched` matches changes matching the user's
+watched project list, and a new menu item was added under the My menu
+to select open changes matching these watched projects.
+
+Web UI
+~~~~~~
+
+* issue 579 Remember diff formatting preferences
++
+Formatting options at the top of a side-by-side or unified diff page
+are now remembered by saving the current preferences into the user's
+account whenever 'Update' is clicked.
+
+* issue 680 Show commit message on the per-file review pages
+
+* issue 498 Improved keyboard navigation
++
+More keyboard bindings have been added, reducing the need to switch to
+the mouse while navigating through a change and performing a review.
+
+* issue 395 Open new window/new tab for all files in a change
++
+New buttons permit opening all modified files of a change into
+new windows or tabs.
+
+* issue 440 Add copy to clipboard button for change-id
++
+The Change-Id field in the upper left side of a change now support to
+copy "Change-Id: I...." onto the clipboard, making it easier to paste
+into a commit message.
+
+* issue 559 Allow copying user public ssh key to clipboard
+
+Email Notifications
+~~~~~~~~~~~~~~~~~~~
+
+* issue 311 No longer CC a user by default
++
+The user who causes a notification to be sent is no longer CC'd on the
+email when it is sent.  This reduces the number of messages sent to a
+user, but can be re-enabled through a checkbox in the Settings >
+Preferences panel.
+
+* issue 535 Enable watching of all projects
++
+Adding the magic `\-- All Projects \--` to the watched project list
+permits the user to be notified of any change occurring in any
+project.  Project specific entries override the notification settings
+for all projects.
+
+* issue 492 Allow watching specific branches or any other search query
++
+In addition to watching a project, users can register a query string
+to match specific changes, reducing notifications to be a smaller
+subset of the changes that occur in a project.
+
+* issue 70 Allow file:^regex to match affected files
++
+The file:^path operator can be used in a watch filter to receive
+notifications only when files matching the regular expression are
+modified by the change.
+
+* issue 623 Include Gerrit-Owner, Gerrit-Reviewer in email footers
++
+New fields in the email footer provide additional detail, enabling
+better filtering and classification of messages.
+
+Access Control
+~~~~~~~~~~~~~~
+
+* Support regular expressions for ref access rules
++
+References in an access rule can now be specified by regular
+expression by prefixing the reference name with ^.
+
+* issue 577 Support $\{username\} in access rules
++
+Adding `$\{username\}` into a reference causes the current username to
+be inserted at that position.  When combined with the Push Branch
+permission this creates a per-user branch namespace feature, giving
+each user their own "sandbox" to push changes to.
+
+Authentication
+~~~~~~~~~~~~~~
+
+* Remove password authentication over SSH
++
+Adding password authentication over SSH turned out to be a major
+mistake.  Users primarily use SSH public keys, and the password
+prompt just got in the way or confused them.  Password support has
+been removed from the SSH server.
+
+* Username cannot be changed once assigned
++
+Once a username has been selected for a user account, it
+cannot be modified by the user.
+
+* issue 555 Make LDAP sessions persistent for the session age
++
+Web sessions are now persistent for the cache.web_sessions.maxAge
+setting, rather than expiring when the browser closes.  (Previously
+sessions expired when the browser exited.)
+
+Misc.
+~~~~~
+
+* Add topic, lastUpdated, sortKey to ChangeAttribute
++
+Additional change fields are now exported as part of the
+stream-events output.
+
+* issue 504 gerrit query SSH command
++
+Queries to lookup change information can be executed over SSH through
+the `gerrit query` command, with results output in either human
+readable text or machine readable JSON.  Change queries can also be
+run over HTTP with the `/query?q=<query>&format=JSON` URL.  Both
+interfaces are intended for automated tools.
+
+* Remove git diff-tree dependency
++
+Gerrit no longer requires `git` in the PATH; differences are now
+constructed in pure Java code.  Remote repository initialization over
+SSH still requires `git` on the remote host's PATH.
+
+* Internal dependencies updated
++
+Updated JGit to 0.8.4.87-g395d236, log4j to 1.2.16, GWT to 2.0.4,
+sfl4j to 1.6.1, easymock to 3.0, JUnit to 4.8.1.
+
+Bug Fixes
+---------
+
+Web UI
+~~~~~~
+
+* issue 352 Confirm branch deletion in web UI
++
+Deleting a branch now presents a confirmation dialog to give the user
+a second chance to abort the destructive operation.
+
+* Fix some JavaScript errors under Chrome
++
+The GWT compiler started to define symbols in the same namespace as
+the prettify syntax highlighting library.  We moved the prettify
+library into its own iframe so it has a different JavaScript namespace
+in the browser.
+
+Misc.
+~~~~~
+
+* issue 614 Fix 503 error when Jetty cancels a request
++
+A bug was introduced in 2.1.3 that caused a server 503 error
+when a fetch/pull/clone or push request timed out.  Fixed.
+
+Version
+-------
+
+df89f998d5c0fa5802a70482d4582b6313a018e4
diff --git a/ReleaseNotes/index.txt b/ReleaseNotes/index.txt
index bc9feb8..bd98667 100644
--- a/ReleaseNotes/index.txt
+++ b/ReleaseNotes/index.txt
@@ -4,6 +4,7 @@
 [[2_1]]
 Version 2.1.x
 -------------
+* link:ReleaseNotes-2.1.4.html[2.1.4]
 * link:ReleaseNotes-2.1.3.html[2.1.3]
 * link:ReleaseNotes-2.1.2.5.html[2.1.2.5]
 * link:ReleaseNotes-2.1.2.4.html[2.1.2.4]
diff --git a/contrib/check-valid-commit.py b/contrib/check-valid-commit.py
new file mode 100755
index 0000000..ca1785e
--- /dev/null
+++ b/contrib/check-valid-commit.py
@@ -0,0 +1,104 @@
+#!/usr/bin/env python
+import commands
+import getopt
+import sys
+
+
+SSH_USER = 'bot'
+SSH_HOST = 'localhost'
+SSH_PORT = 29418
+SSH_COMMAND = 'ssh %s@%s -p %d gerrit approve ' % (SSH_USER, SSH_HOST, SSH_PORT)
+FAILURE_SCORE = '--code-review=-2'
+FAILURE_MESSAGE = 'This commit message does not match the standard.' \
+        + '  Please correct the commit message and upload a replacement patch.'
+PASS_SCORE = '--code-review=0'
+PASS_MESSAGE = ''
+
+def main():
+    change = None
+    project = None
+    branch = None
+    commit = None
+    patchset = None
+
+    try:
+        opts, args = getopt.getopt(sys.argv[1:], '', \
+            ['change=', 'project=', 'branch=', 'commit=', 'patchset='])
+    except getopt.GetoptError, err:
+        print 'Error: %s' % (err)
+        usage()
+        sys.exit(-1)
+
+    for arg, value in opts:
+        if arg == '--change':
+            change = value
+        elif arg == '--project':
+            project = value
+        elif arg == '--branch':
+            branch = value
+        elif arg == '--commit':
+            commit = value
+        elif arg == '--patchset':
+            patchset = value
+        else:
+            print 'Error: option %s not recognized' % (arg)
+            usage()
+            sys.exit(-1)
+
+    if change == None or project == None or branch == None \
+        or commit == None or patchset == None:
+        usage()
+        sys.exit(-1)
+
+    command = 'git cat-file commit %s' % (commit)
+    status, output = commands.getstatusoutput(command)
+
+    if status != 0:
+        print 'Error running \'%s\'. status: %s, output:\n\n%s' % \
+            (command, status, output)
+        sys.exit(-1)
+
+    commitMessage = output[(output.find('\n\n')+2):]
+    commitLines = commitMessage.split('\n')
+
+    if len(commitLines) > 1 and len(commitLines[1]) != 0:
+        fail(commit, 'Invalid commit summary.  The summary must be ' \
+            + 'one line followed by a blank line.')
+
+    i = 0
+    for line in commitLines:
+        i = i + 1
+        if len(line) > 80:
+            fail(commit, 'Line %d is over 80 characters.' % i)
+
+    passes(commit)
+
+def usage():
+    print 'Usage:\n'
+    print sys.argv[0] + ' --change <change id> --project <project name> ' \
+        + '--branch <branch> --commit <sha1> --patchset <patchset id>'
+
+def fail( commit, message ):
+    command = SSH_COMMAND + FAILURE_SCORE + ' -m \\\"' \
+        + _shell_escape( FAILURE_MESSAGE + '\n\n' + message) \
+        + '\\\" ' + commit
+    commands.getstatusoutput(command)
+    sys.exit(1)
+
+def passes( commit ):
+    command = SSH_COMMAND + PASS_SCORE + ' -m \\\"' \
+        + _shell_escape(PASS_MESSAGE) + ' \\\" ' + commit
+    commands.getstatusoutput(command)
+
+def _shell_escape(x):
+    s = ''
+    for c in x:
+        if c in '\n':
+            s = s + '\\\"$\'\\n\'\\\"'
+        else:
+            s = s + c
+    return s
+
+if __name__ == '__main__':
+    main()
+
diff --git a/gerrit-common/pom.xml b/gerrit-common/pom.xml
index b441b82..adac3a8 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.3</version>
+    <version>2.1.4-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-common</artifactId>
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java b/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
index 5a85ef5..43541b7 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
@@ -24,7 +24,9 @@
 
 public class PageLinks {
   public static final String SETTINGS = "settings";
+  public static final String SETTINGS_PREFERENCES = "settings,preferences";
   public static final String SETTINGS_SSHKEYS = "settings,ssh-keys";
+  public static final String SETTINGS_HTTP_PASSWORD = "settings,http-password";
   public static final String SETTINGS_WEBIDENT = "settings,web-identities";
   public static final String SETTINGS_MYGROUPS = "settings,group-memberships";
   public static final String SETTINGS_AGREEMENTS = "settings,agreements";
@@ -33,11 +35,13 @@
   public static final String SETTINGS_NEW_AGREEMENT = "settings,new-agreement";
   public static final String REGISTER = "register";
 
+  public static final String TOP = "n,z";
+
   public static final String MINE = "mine";
   public static final String MINE_STARRED = "mine,starred";
   public static final String MINE_DRAFTS = "mine,drafts";
+  public static final String MINE_WATCHED = "mine,watched," + TOP;
 
-  public static final String TOP = "n,z";
   public static final String ALL_ABANDONED = "all,abandoned," + TOP;
   public static final String ALL_MERGED = "all,merged," + TOP;
   public static final String ALL_OPEN = "all,open," + TOP;
@@ -65,21 +69,28 @@
     return "q," + KeyUtil.encode(query) + "," + TOP;
   }
 
-  public static String toProject(final Project.NameKey proj, Status status) {
+  public static String projectQuery(Project.NameKey proj, Status status) {
     switch (status) {
       case ABANDONED:
-        return "project,abandoned," + proj.toString() + ",n,z";
+        return "status:abandoned " + op("project", proj.get());
 
       case MERGED:
-        return "project,merged," + proj.toString() + ",n,z";
+        return "status:merged " + op("project", proj.get());
 
       case NEW:
       case SUBMITTED:
       default:
-        return "project,open," + proj.toString() + ",n,z";
+        return "status:open " + op("project", proj.get());
     }
   }
 
+  public static String op(String name, String value) {
+    if (value.indexOf(' ') >= 0) {
+      return name + ":\"" + value + "\"";
+    }
+    return name + ":" + value;
+  }
+
   protected PageLinks() {
   }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java
index 83d31a0..1117455 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java
@@ -50,6 +50,10 @@
       AsyncCallback<AccountExternalId> callback);
 
   @SignInRequired
+  void clearPassword(AccountExternalId.Key key,
+      AsyncCallback<AccountExternalId> gerritCallback);
+
+  @SignInRequired
   void myExternalIds(AsyncCallback<List<AccountExternalId>> callback);
 
   @SignInRequired
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java
index 2fad82e..e219074 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.common.auth.SignInRequired;
 import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.AccountDiffPreference;
 import com.google.gerrit.reviewdb.AccountGeneralPreferences;
 import com.google.gerrit.reviewdb.AccountProjectWatch;
 import com.google.gwt.user.client.rpc.AsyncCallback;
@@ -33,14 +34,21 @@
   void myAccount(AsyncCallback<Account> callback);
 
   @SignInRequired
+  void myDiffPreferences(AsyncCallback<AccountDiffPreference> callback);
+
+  @SignInRequired
   void changePreferences(AccountGeneralPreferences pref,
       AsyncCallback<VoidResult> gerritCallback);
 
   @SignInRequired
+  void changeDiffPreferences(AccountDiffPreference diffPref,
+      AsyncCallback<VoidResult> callback);
+
+  @SignInRequired
   void myProjectWatch(AsyncCallback<List<AccountProjectWatchInfo>> callback);
 
   @SignInRequired
-  void addProjectWatch(String projectName,
+  void addProjectWatch(String projectName, String filter,
       AsyncCallback<AccountProjectWatchInfo> callback);
 
   @SignInRequired
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeInfo.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeInfo.java
index 3de4ee0..b5271ec 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeInfo.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeInfo.java
@@ -27,6 +27,7 @@
   protected Change.Status status;
   protected ProjectInfo project;
   protected String branch;
+  protected String topic;
   protected boolean starred;
   protected Timestamp lastUpdatedOn;
   protected String sortKey;
@@ -42,6 +43,7 @@
     status = c.getStatus();
     project = new ProjectInfo(c.getProject());
     branch = c.getDest().getShortName();
+    topic = c.getTopic();
     lastUpdatedOn = c.getLastUpdatedOn();
     sortKey = c.getSortKey();
   }
@@ -74,6 +76,10 @@
     return branch;
   }
 
+  public String getTopic() {
+    return topic;
+  }
+
   public boolean isStarred() {
     return starred;
   }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeListService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeListService.java
index 3473373..5ff85e3 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeListService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeListService.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.common.auth.SignInRequired;
 import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.Project;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwtjsonrpc.client.RemoteJsonService;
 import com.google.gwtjsonrpc.client.RpcImpl;
@@ -28,44 +27,6 @@
 
 @RpcImpl(version = Version.V2_0)
 public interface ChangeListService extends RemoteJsonService {
-  /** Get all open changes more recent than pos, fetching at most limit rows. */
-  void allOpenPrev(String pos, int limit,
-      AsyncCallback<SingleListChangeInfo> callback);
-
-  /** Get all open changes older than pos, fetching at most limit rows. */
-  void allOpenNext(String pos, int limit,
-      AsyncCallback<SingleListChangeInfo> callback);
-
-  /** Get all open changes more recent than pos, fetching at most limit rows. */
-  void byProjectOpenPrev(Project.NameKey project, String pos, int limit,
-      AsyncCallback<SingleListChangeInfo> callback);
-
-  /** Get all open changes older than pos, fetching at most limit rows. */
-  void byProjectOpenNext(Project.NameKey project, String pos, int limit,
-      AsyncCallback<SingleListChangeInfo> callback);
-
-  /**
-   * Get all closed changes with same status, more recent than pos, fetching at
-   * most limit rows.
-   */
-  void byProjectClosedPrev(Project.NameKey project, Change.Status status,
-      String pos, int limit, AsyncCallback<SingleListChangeInfo> callback);
-
-  /**
-   * Get all closed changes with same status, older than pos, fetching at most
-   * limit rows.
-   */
-  void byProjectClosedNext(Project.NameKey project, Change.Status status,
-      String pos, int limit, AsyncCallback<SingleListChangeInfo> callback);
-
-  /** Get all closed changes more recent than pos, fetching at most limit rows. */
-  void allClosedPrev(Change.Status status, String pos, int limit,
-      AsyncCallback<SingleListChangeInfo> callback);
-
-  /** Get all closed changes older than pos, fetching at most limit rows. */
-  void allClosedNext(Change.Status status, String pos, int limit,
-      AsyncCallback<SingleListChangeInfo> callback);
-
   /** Get all changes which match an arbitrary query string. */
   void allQueryPrev(String query, String pos, int limit,
       AsyncCallback<SingleListChangeInfo> callback);
@@ -77,14 +38,6 @@
   /** Get the data to show AccountDashboardScreen for an account. */
   void forAccount(Account.Id id, AsyncCallback<AccountDashboardInfo> callback);
 
-  /** Get the changes starred by the caller. */
-  @SignInRequired
-  void myStarredChanges(AsyncCallback<SingleListChangeInfo> callback);
-
-  /** Get the changes with unpublished drafts by the caller. */
-  @SignInRequired
-  void myDraftChanges(AsyncCallback<SingleListChangeInfo> callback);
-
   /** Get the ids of all changes starred by the caller. */
   @SignInRequired
   void myStarredChangeIds(AsyncCallback<Set<Change.Id>> callback);
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java
index e2dc1a7..717a492 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java
@@ -15,9 +15,11 @@
 package com.google.gerrit.common.data;
 
 import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.AccountDiffPreference;
 
 /** Data sent as part of the host page, to bootstrap the UI. */
 public class HostPageData {
   public Account account;
+  public AccountDiffPreference accountDiffPref;
   public GerritConfig config;
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchDetailService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchDetailService.java
index 2876566..5aec0c7 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchDetailService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchDetailService.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.common.auth.SignInRequired;
 import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.AccountDiffPreference;
 import com.google.gerrit.reviewdb.ApprovalCategoryValue;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.Patch;
@@ -34,7 +35,7 @@
 @RpcImpl(version = Version.V2_0)
 public interface PatchDetailService extends RemoteJsonService {
   void patchScript(Patch.Key key, PatchSet.Id a, PatchSet.Id b,
-      PatchScriptSettings settings, AsyncCallback<PatchScript> callback);
+      AccountDiffPreference diffPrefs, AsyncCallback<PatchScript> callback);
 
   @SignInRequired
   void saveDraft(PatchLineComment comment,
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 2c1fd72..048d440 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
@@ -14,17 +14,15 @@
 
 package com.google.gerrit.common.data;
 
-import static com.google.gerrit.reviewdb.AccountGeneralPreferences.WHOLE_FILE_CONTEXT;
-
-import com.google.gerrit.common.data.PatchScriptSettings.Whitespace;
 import com.google.gerrit.prettify.client.ClientSideFormatter;
 import com.google.gerrit.prettify.common.EditList;
 import com.google.gerrit.prettify.common.PrettyFormatter;
-import com.google.gerrit.prettify.common.PrettySettings;
 import com.google.gerrit.prettify.common.SparseFileContent;
 import com.google.gerrit.prettify.common.SparseHtmlFile;
+import com.google.gerrit.reviewdb.AccountDiffPreference;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.Patch;
+import com.google.gerrit.reviewdb.AccountDiffPreference.Whitespace;
 import com.google.gerrit.reviewdb.Patch.ChangeType;
 
 import org.eclipse.jgit.diff.Edit;
@@ -41,7 +39,7 @@
   protected String oldName;
   protected String newName;
   protected List<String> header;
-  protected PatchScriptSettings settings;
+  protected AccountDiffPreference diffPrefs;
   protected SparseFileContent a;
   protected SparseFileContent b;
   protected List<Edit> edits;
@@ -53,7 +51,7 @@
   protected boolean intralineDifference;
 
   public PatchScript(final Change.Key ck, final ChangeType ct, final String on,
-      final String nn, final List<String> h, final PatchScriptSettings s,
+      final String nn, 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,
@@ -63,7 +61,7 @@
     oldName = on;
     newName = nn;
     header = h;
-    settings = s;
+    diffPrefs = dp;
     a = ca;
     b = cb;
     edits = e;
@@ -114,12 +112,12 @@
     return history;
   }
 
-  public PatchScriptSettings getSettings() {
-    return settings;
+  public AccountDiffPreference getDiffPrefs() {
+    return diffPrefs;
   }
 
-  public void setSettings(PatchScriptSettings s) {
-    settings = s;
+  public void setDiffPrefs(AccountDiffPreference dp) {
+    diffPrefs = dp;
   }
 
   public boolean isHugeFile() {
@@ -127,7 +125,7 @@
   }
 
   public boolean isIgnoreWhitespace() {
-    return settings.getWhitespace() != Whitespace.IGNORE_NONE;
+    return diffPrefs.getIgnoreWhitespace() != Whitespace.IGNORE_NONE;
   }
 
   public boolean hasIntralineDifference() {
@@ -143,12 +141,12 @@
   }
 
   public SparseHtmlFile getSparseHtmlFileA() {
-    PrettySettings s = new PrettySettings(settings.getPrettySettings());
-    s.setFileName(a.getPath());
-    s.setShowWhiteSpaceErrors(false);
+    AccountDiffPreference dp = new AccountDiffPreference(diffPrefs);
+    dp.setShowWhitespaceErrors(false);
 
     PrettyFormatter f = ClientSideFormatter.FACTORY.get();
-    f.setPrettySettings(s);
+    f.setDiffPrefs(dp);
+    f.setFileName(a.getPath());
     f.setEditFilter(PrettyFormatter.A);
     f.setEditList(edits);
     f.format(a);
@@ -156,15 +154,15 @@
   }
 
   public SparseHtmlFile getSparseHtmlFileB() {
-    PrettySettings s = new PrettySettings(settings.getPrettySettings());
-    s.setFileName(b.getPath());
+    AccountDiffPreference dp = new AccountDiffPreference(diffPrefs);
 
     PrettyFormatter f = ClientSideFormatter.FACTORY.get();
-    f.setPrettySettings(s);
+    f.setDiffPrefs(dp);
+    f.setFileName(b.getPath());
     f.setEditFilter(PrettyFormatter.B);
     f.setEditList(edits);
 
-    if (s.isSyntaxHighlighting() && a.isWholeFile() && !b.isWholeFile()) {
+    if (dp.isSyntaxHighlighting() && a.isWholeFile() && !b.isWholeFile()) {
       f.format(b.apply(a, edits));
     } else {
       f.format(b);
@@ -177,8 +175,8 @@
   }
 
   public Iterable<EditList.Hunk> getHunks() {
-    int ctx = settings.getContext();
-    if (ctx == WHOLE_FILE_CONTEXT) {
+    int ctx = diffPrefs.getContext();
+    if (ctx == AccountDiffPreference.WHOLE_FILE_CONTEXT) {
       ctx = Math.max(a.size(), b.size());
     }
     return new EditList(edits, ctx, a.size(), b.size()).getHunks();
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScriptSettings.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScriptSettings.java
deleted file mode 100644
index cd43ba4..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScriptSettings.java
+++ /dev/null
@@ -1,80 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.common.data;
-
-import com.google.gerrit.prettify.common.PrettySettings;
-import com.google.gerrit.reviewdb.AccountGeneralPreferences;
-import com.google.gerrit.reviewdb.CodedEnum;
-
-public class PatchScriptSettings {
-  public static enum Whitespace implements CodedEnum {
-    IGNORE_NONE('N'), //
-    IGNORE_SPACE_AT_EOL('E'), //
-    IGNORE_SPACE_CHANGE('S'), //
-    IGNORE_ALL_SPACE('A');
-
-    private final char code;
-
-    private Whitespace(final char c) {
-      code = c;
-    }
-
-    public char getCode() {
-      return code;
-    }
-  }
-
-  protected int context;
-  protected Whitespace whitespace;
-  protected PrettySettings pretty;
-
-  public PatchScriptSettings() {
-    context = AccountGeneralPreferences.DEFAULT_CONTEXT;
-    whitespace = Whitespace.IGNORE_NONE;
-    pretty = new PrettySettings();
-  }
-
-  public PatchScriptSettings(final PatchScriptSettings s) {
-    context = s.context;
-    whitespace = s.whitespace;
-    pretty = new PrettySettings(s.pretty);
-  }
-
-  public PrettySettings getPrettySettings() {
-    return pretty;
-  }
-
-  public void setPrettySettings(PrettySettings s) {
-    pretty = s;
-  }
-
-  public int getContext() {
-    return context;
-  }
-
-  public void setContext(final int ctx) {
-    assert 0 <= ctx || ctx == AccountGeneralPreferences.WHOLE_FILE_CONTEXT;
-    context = ctx;
-  }
-
-  public Whitespace getWhitespace() {
-    return whitespace;
-  }
-
-  public void setWhitespace(final Whitespace ws) {
-    assert ws != null;
-    whitespace = ws;
-  }
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/errors/InvalidQueryException.java b/gerrit-common/src/main/java/com/google/gerrit/common/errors/InvalidQueryException.java
new file mode 100644
index 0000000..4a66a416a
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/errors/InvalidQueryException.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.common.errors;
+
+/** Error indicating the query cannot be executed. */
+public class InvalidQueryException extends Exception {
+  private static final long serialVersionUID = 1L;
+
+  public InvalidQueryException(String message, String query) {
+    super("Invalid query: " + query + "\n\n" + message);
+  }
+}
diff --git a/gerrit-gwtdebug/pom.xml b/gerrit-gwtdebug/pom.xml
index 04f1e41..c3a07a4 100644
--- a/gerrit-gwtdebug/pom.xml
+++ b/gerrit-gwtdebug/pom.xml
@@ -22,10 +22,10 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.1.3</version>
+    <version>2.1.4-SNAPSHOT</version>
   </parent>
 
-  <artifactId>gerrit-gwtdbug</artifactId>
+  <artifactId>gerrit-gwtdebug</artifactId>
   <name>Gerrit Code Review - GWT UI Debugging Support</name>
 
   <description>
@@ -42,12 +42,14 @@
       <groupId>com.google.gerrit</groupId>
       <artifactId>gerrit-gwtui</artifactId>
       <version>${project.version}</version>
+      <classifier>classes</classifier>
     </dependency>
 
     <dependency>
       <groupId>com.google.gerrit</groupId>
       <artifactId>gerrit-war</artifactId>
       <version>${project.version}</version>
+      <classifier>classes</classifier>
       <exclusions>
         <exclusion>
           <groupId>com.google.gerrit</groupId>
diff --git a/gerrit-gwtui/pom.xml b/gerrit-gwtui/pom.xml
index 5b4d192..29c5b01 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.3</version>
+    <version>2.1.4-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-gwtui</artifactId>
@@ -244,6 +244,7 @@
         <artifactId>maven-war-plugin</artifactId>
         <configuration>
           <packagingExcludes>WEB-INF/classes/**,WEB-INF/lib/**</packagingExcludes>
+          <attachClasses>true</attachClasses>
           <archive>
             <addMavenDescriptor>false</addMavenDescriptor>
           </archive>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationCallback.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationCallback.java
new file mode 100644
index 0000000..e3b7525
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationCallback.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.client;
+
+/**
+ * Interface that a caller must implement to react on the result of a
+ * {@link ConfirmationDialog}.
+ */
+public interface ConfirmationCallback {
+
+  /**
+   * Called when the {@link ConfirmationDialog} is finished with OK.
+   * To be overwritten by subclasses.
+   */
+  public void onOk();
+
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationDialog.java
new file mode 100644
index 0000000..c4cb770
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationDialog.java
@@ -0,0 +1,77 @@
+// 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;
+
+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.Button;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwtexpui.globalkey.client.GlobalKey;
+import com.google.gwtexpui.user.client.AutoCenterDialogBox;
+
+public class ConfirmationDialog extends AutoCenterDialogBox {
+
+
+  private Button cancelButton;
+
+  public ConfirmationDialog(final String dialogTitle, final HTML message,
+      final ConfirmationCallback callback) {
+    super(/* auto hide */false, /* modal */true);
+    setGlassEnabled(true);
+    setText(dialogTitle);
+
+    final FlowPanel buttons = new FlowPanel();
+
+    final Button okButton = new Button();
+    okButton.setText(Gerrit.C.confirmationDialogOk());
+    okButton.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(ClickEvent event) {
+        hide();
+        callback.onOk();
+      }
+    });
+    buttons.add(okButton);
+
+    cancelButton = new Button();
+    DOM.setStyleAttribute(cancelButton.getElement(), "marginLeft", "300px");
+    cancelButton.setText(Gerrit.C.confirmationDialogCancel());
+    cancelButton.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(ClickEvent event) {
+        hide();
+      }
+    });
+    buttons.add(cancelButton);
+
+    final FlowPanel center = new FlowPanel();
+    center.add(message);
+    center.add(buttons);
+    add(center);
+
+    message.setWidth("400px");
+
+    setWidget(center);
+  }
+
+  @Override
+  public void center() {
+    super.center();
+    GlobalKey.dialog(this);
+    cancelButton.setFocus(true);
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
index 7db92ad..f93fd5e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
@@ -21,37 +21,47 @@
 import static com.google.gerrit.common.PageLinks.MINE_STARRED;
 import static com.google.gerrit.common.PageLinks.REGISTER;
 import static com.google.gerrit.common.PageLinks.SETTINGS;
+import static com.google.gerrit.common.PageLinks.SETTINGS_AGREEMENTS;
+import static com.google.gerrit.common.PageLinks.SETTINGS_CONTACT;
+import static com.google.gerrit.common.PageLinks.SETTINGS_HTTP_PASSWORD;
+import static com.google.gerrit.common.PageLinks.SETTINGS_MYGROUPS;
 import static com.google.gerrit.common.PageLinks.SETTINGS_NEW_AGREEMENT;
+import static com.google.gerrit.common.PageLinks.SETTINGS_PREFERENCES;
+import static com.google.gerrit.common.PageLinks.SETTINGS_PROJECTS;
+import static com.google.gerrit.common.PageLinks.SETTINGS_SSHKEYS;
 import static com.google.gerrit.common.PageLinks.SETTINGS_WEBIDENT;
-import static com.google.gerrit.common.PageLinks.TOP;
+import static com.google.gerrit.common.PageLinks.op;
 
-import com.google.gerrit.client.account.AccountSettings;
+import com.google.gerrit.client.account.MyAgreementsScreen;
+import com.google.gerrit.client.account.MyContactInformationScreen;
+import com.google.gerrit.client.account.MyGroupsScreen;
+import com.google.gerrit.client.account.MyIdentitiesScreen;
+import com.google.gerrit.client.account.MyPasswordScreen;
+import com.google.gerrit.client.account.MyPreferencesScreen;
+import com.google.gerrit.client.account.MyProfileScreen;
+import com.google.gerrit.client.account.MySshKeysScreen;
+import com.google.gerrit.client.account.MyWatchedProjectsScreen;
 import com.google.gerrit.client.account.NewAgreementScreen;
 import com.google.gerrit.client.account.RegisterScreen;
 import com.google.gerrit.client.account.ValidateEmailScreen;
 import com.google.gerrit.client.admin.AccountGroupScreen;
 import com.google.gerrit.client.admin.GroupListScreen;
-import com.google.gerrit.client.admin.ProjectAdminScreen;
+import com.google.gerrit.client.admin.ProjectAccessScreen;
+import com.google.gerrit.client.admin.ProjectBranchesScreen;
+import com.google.gerrit.client.admin.ProjectInfoScreen;
 import com.google.gerrit.client.admin.ProjectListScreen;
+import com.google.gerrit.client.admin.ProjectScreen;
 import com.google.gerrit.client.auth.openid.OpenIdSignInDialog;
 import com.google.gerrit.client.auth.userpass.UserPassSignInDialog;
 import com.google.gerrit.client.changes.AccountDashboardScreen;
-import com.google.gerrit.client.changes.AllAbandonedChangesScreen;
-import com.google.gerrit.client.changes.AllMergedChangesScreen;
-import com.google.gerrit.client.changes.AllOpenChangesScreen;
-import com.google.gerrit.client.changes.ByProjectAbandonedChangesScreen;
-import com.google.gerrit.client.changes.ByProjectMergedChangesScreen;
-import com.google.gerrit.client.changes.ByProjectOpenChangesScreen;
-import com.google.gerrit.client.changes.ChangeQueryResultsScreen;
 import com.google.gerrit.client.changes.ChangeScreen;
-import com.google.gerrit.client.changes.MineDraftsScreen;
-import com.google.gerrit.client.changes.MineStarredScreen;
 import com.google.gerrit.client.changes.PatchTable;
 import com.google.gerrit.client.changes.PublishCommentScreen;
+import com.google.gerrit.client.changes.QueryScreen;
 import com.google.gerrit.client.patches.PatchScreen;
 import com.google.gerrit.client.ui.Screen;
-import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.auth.SignInMode;
+import com.google.gerrit.common.data.PatchSetDetail;
 import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.reviewdb.AccountGroup;
 import com.google.gerrit.reviewdb.Change;
@@ -106,7 +116,7 @@
 
   private static void select(final String token) {
     if (token.startsWith("patch,")) {
-      patch(token, null, 0, null);
+      patch(token, null, 0, null, null);
 
     } else if (token.startsWith("change,publish,")) {
       publish(token);
@@ -148,12 +158,17 @@
       }
 
     } else if (MINE_STARRED.equals(token)) {
-      return new MineStarredScreen();
+      return QueryScreen.forQuery("is:starred");
 
     } else if (MINE_DRAFTS.equals(token)) {
-      return new MineDraftsScreen();
+      return QueryScreen.forQuery("has:draft");
 
     } else {
+      String p = "mine,watched,";
+      if (token.startsWith(p)) {
+        return QueryScreen.forQuery("is:watched status:open", skip(p, token));
+      }
+
       return new NotFoundScreen();
     }
   }
@@ -163,18 +178,19 @@
 
     p = "all,abandoned,";
     if (token.startsWith(p)) {
-      return new AllAbandonedChangesScreen(skip(p, token));
+      return QueryScreen.forQuery("status:abandoned", skip(p, token));
     }
 
     p = "all,merged,";
     if (token.startsWith(p)) {
-      return new AllMergedChangesScreen(skip(p, token));
+      return QueryScreen.forQuery("status:merged", skip(p, token));
     }
 
     p = "all,open,";
     if (token.startsWith(p)) {
-      return new AllOpenChangesScreen(skip(p, token));
+      return QueryScreen.forQuery("status:open", skip(p, token));
     }
+
     return new NotFoundScreen();
   }
 
@@ -185,25 +201,32 @@
     if (token.startsWith(p)) {
       final String s = skip(p, token);
       final int c = s.indexOf(',');
-      return new ByProjectOpenChangesScreen(Project.NameKey.parse(s.substring(
-          0, c)), s.substring(c + 1));
+      Project.NameKey proj = Project.NameKey.parse(s.substring(0, c));
+      return QueryScreen.forQuery( //
+          "status:open " + op("project", proj.get()), //
+          s.substring(c + 1));
     }
 
     p = "project,merged,";
     if (token.startsWith(p)) {
       final String s = skip(p, token);
       final int c = s.indexOf(',');
-      return new ByProjectMergedChangesScreen(Project.NameKey.parse(s
-          .substring(0, c)), s.substring(c + 1));
+      Project.NameKey proj = Project.NameKey.parse(s.substring(0, c));
+      return QueryScreen.forQuery( //
+          "status:merged " + op("project", proj.get()), //
+          s.substring(c + 1));
     }
 
     p = "project,abandoned,";
     if (token.startsWith(p)) {
       final String s = skip(p, token);
       final int c = s.indexOf(',');
-      return new ByProjectAbandonedChangesScreen(Project.NameKey.parse(s
-          .substring(0, c)), s.substring(c + 1));
+      Project.NameKey proj = Project.NameKey.parse(s.substring(0, c));
+      return QueryScreen.forQuery( //
+          "status:abandoned " + op("project", proj.get()), //
+          s.substring(c + 1));
     }
+
     return new NotFoundScreen();
   }
 
@@ -222,7 +245,7 @@
     if (token.startsWith(p)) {
       final String s = skip(p, token);
       final int c = s.indexOf(',');
-      return new ChangeQueryResultsScreen(s.substring(0, c), s.substring(c + 1));
+      return new QueryScreen(s.substring(0, c), s.substring(c + 1));
     }
 
     return new NotFoundScreen();
@@ -244,7 +267,8 @@
   }
 
   public static void patch(String token, final Patch.Key id,
-      final int patchIndex, final PatchTable patchTable) {
+      final int patchIndex, final PatchSetDetail patchSetDetail,
+      final PatchTable patchTable) {
     GWT.runAsync(new AsyncSplit(token) {
       public void onSuccess() {
         Gerrit.display(token, select());
@@ -258,6 +282,7 @@
           return new PatchScreen.SideBySide( //
               id != null ? id : Patch.Key.parse(skip(p, token)), //
               patchIndex, //
+              patchSetDetail, //
               patchTable //
           );
         }
@@ -267,6 +292,7 @@
           return new PatchScreen.Unified( //
               id != null ? id : Patch.Key.parse(skip(p, token)), //
               patchIndex, //
+              patchSetDetail, //
               patchTable //
           );
         }
@@ -285,6 +311,43 @@
       private Screen select() {
         String p;
 
+        if (token.equals(SETTINGS)) {
+          return new MyProfileScreen();
+        }
+
+        if (token.equals(SETTINGS_PREFERENCES)) {
+          return new MyPreferencesScreen();
+        }
+
+        if (token.equals(SETTINGS_PROJECTS)) {
+          return new MyWatchedProjectsScreen();
+        }
+
+        if (token.equals(SETTINGS_CONTACT)) {
+          return new MyContactInformationScreen();
+        }
+
+        if (token.equals(SETTINGS_SSHKEYS)) {
+          return new MySshKeysScreen();
+        }
+
+        if (token.equals(SETTINGS_WEBIDENT)) {
+          return new MyIdentitiesScreen();
+        }
+
+        if (token.equals(SETTINGS_HTTP_PASSWORD)) {
+          return new MyPasswordScreen();
+        }
+
+        if (token.equals(SETTINGS_MYGROUPS)) {
+          return new MyGroupsScreen();
+        }
+
+        if (token.equals(SETTINGS_AGREEMENTS)
+            && Gerrit.getConfig().isUseContributorAgreements()) {
+          return new MyAgreementsScreen();
+        }
+
         p = "register,";
         if (token.startsWith(p)) {
           return new RegisterScreen(skip(p, token));
@@ -301,7 +364,7 @@
           final String[] args = skip(p, token).split(",");
           final SignInMode mode = SignInMode.valueOf(args[0]);
           final String msg = KeyUtil.decode(args[1]);
-          final String to = PageLinks.MINE;
+          final String to = MINE;
           switch (Gerrit.getConfig().getAuthType()) {
             case OPENID:
               new OpenIdSignInDialog(mode, to, msg).center();
@@ -315,9 +378,9 @@
           }
           switch (mode) {
             case SIGN_IN:
-              return new AllOpenChangesScreen(TOP);
+              return QueryScreen.forQuery("status:open");
             case LINK_IDENTIY:
-              return new AccountSettings(SETTINGS_WEBIDENT);
+              return new MyIdentitiesScreen();
           }
         }
 
@@ -329,7 +392,7 @@
           return new NewAgreementScreen(skip(p, token));
         }
 
-        return new AccountSettings(token);
+        return new NotFoundScreen();
       }
     });
   }
@@ -351,8 +414,23 @@
         if (token.startsWith(p)) {
           p = skip(p, token);
           final int c = p.indexOf(',');
-          final String idstr = p.substring(0, c);
-          return new ProjectAdminScreen(Project.NameKey.parse(idstr), token);
+          final Project.NameKey k = Project.NameKey.parse(p.substring(0, c));
+          final boolean isWild = k.equals(Gerrit.getConfig().getWildProject());
+          p = p.substring(c + 1);
+
+          if (ProjectScreen.INFO.equals(p)) {
+            return new ProjectInfoScreen(k);
+          }
+
+          if (!isWild && ProjectScreen.BRANCH.equals(p)) {
+            return new ProjectBranchesScreen(k);
+          }
+
+          if (ProjectScreen.ACCESS.equals(p)) {
+            return new ProjectAccessScreen(k);
+          }
+
+          return new NotFoundScreen();
         }
 
         if (ADMIN_GROUPS.equals(token)) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java
index 178126d..74a2678 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java
@@ -17,6 +17,9 @@
 import com.google.gerrit.client.rpc.RpcConstants;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.Window;
 import com.google.gwt.user.client.rpc.StatusCodeException;
 import com.google.gwt.user.client.ui.Button;
@@ -30,6 +33,7 @@
 public class ErrorDialog extends PluginSafePopupPanel {
   private final Label text;
   private final FlowPanel body;
+  private final Button closey;
 
   protected ErrorDialog() {
     super(/* auto hide */false, /* modal */true);
@@ -44,7 +48,7 @@
     final FlowPanel buttons = new FlowPanel();
     buttons.setStyleName(Gerrit.RESOURCES.css().errorDialogButtons());
 
-    final Button closey = new Button();
+    closey = new Button();
     closey.setText(Gerrit.C.errorDialogContinue());
     closey.addClickHandler(new ClickHandler() {
       @Override
@@ -52,6 +56,16 @@
         hide();
       }
     });
+    closey.addKeyPressHandler(new KeyPressHandler() {
+      @Override
+      public void onKeyPress(KeyPressEvent event) {
+        // if the close button is triggered by a key we need to consume the key
+        // event, otherwise the key event would be propagated to the parent
+        // screen and eventually trigger some unwanted action there after the
+        // error dialog was closed
+        event.stopPropagation();
+      }
+    });
     buttons.add(closey);
 
     final FlowPanel center = new FlowPanel();
@@ -108,7 +122,10 @@
     final Label r = new Label(cn);
     r.setStyleName(Gerrit.RESOURCES.css().errorDialogErrorType());
     body.add(r);
-    body.add(new Label(what.getMessage()));
+
+    final Label m = new Label(what.getMessage());
+    DOM.setStyleAttribute(m.getElement(),"whiteSpace","pre");
+    body.add(m);
   }
 
   public void setText(final String t) {
@@ -118,5 +135,6 @@
   @Override
   public void center() {
     show();
+    closey.setFocus(true);
   }
 }
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 641db80..ecb4cd6 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
@@ -27,6 +27,7 @@
 import com.google.gerrit.common.data.HostPageData;
 import com.google.gerrit.common.data.SystemInfoService;
 import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.AccountDiffPreference;
 import com.google.gerrit.reviewdb.AccountGeneralPreferences;
 import com.google.gwt.core.client.EntryPoint;
 import com.google.gwt.core.client.GWT;
@@ -70,6 +71,7 @@
   private static String myHost;
   private static GerritConfig myConfig;
   private static Account myAccount;
+  private static AccountDiffPreference myAccountDiffPref;
 
   private static TabPanel menuLeft;
   private static LinkMenuBar menuRight;
@@ -172,6 +174,15 @@
     return myAccount;
   }
 
+  /** @return the currently signed in users's diff preferences; null if no diff preferences defined for the account */
+  public static AccountDiffPreference getAccountDiffPreference() {
+    return myAccountDiffPref;
+  }
+
+  public static void setAccountDiffPreference(AccountDiffPreference accountDiffPref) {
+    myAccountDiffPref = accountDiffPref;
+  }
+
   /** @return true if the user is currently authenticated */
   public static boolean isSignedIn() {
     return getUserAccount() != null;
@@ -203,6 +214,7 @@
   static void deleteSessionCookie() {
     Cookies.removeCookie(SESSION_COOKIE);
     myAccount = null;
+    myAccountDiffPref = null;
     refreshMenuBar();
   }
 
@@ -234,6 +246,9 @@
         if (result.account != null) {
           myAccount = result.account;
         }
+        if (result.accountDiffPref != null) {
+          myAccountDiffPref = result.accountDiffPref;
+        }
         onModuleLoad2();
       }
     });
@@ -409,7 +424,8 @@
     if (signedIn) {
       m = new LinkMenuBar();
       addLink(m, C.menuMyChanges(), PageLinks.MINE);
-      addLink(m, C.menyMyDrafts(), PageLinks.MINE_DRAFTS);
+      addLink(m, C.menuMyDrafts(), PageLinks.MINE_DRAFTS);
+      addLink(m, C.menuMyWatchedChanges(), PageLinks.MINE_WATCHED);
       addLink(m, C.menuMyStarredChanges(), PageLinks.MINE_STARRED);
       menuLeft.add(m, C.menuMine());
       menuLeft.selectTab(1);
@@ -427,6 +443,7 @@
     if (getConfig().isDocumentationAvailable()) {
       m = new LinkMenuBar();
       addDocLink(m, C.menuDocumentationIndex(), "index.html");
+      addDocLink(m, C.menuDocumentationSearch(), "user-search.html");
       addDocLink(m, C.menuDocumentationUpload(), "user-upload.html");
       addDocLink(m, C.menuDocumentationAccess(), "access-control.html");
       menuLeft.add(m, C.menuDocumentation());
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 b4ffe34..3f263d2 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
@@ -32,6 +32,12 @@
   String errorDialogTitle();
   String errorDialogContinue();
 
+  String confirmationDialogOk();
+  String confirmationDialogCancel();
+
+  String branchDeletionDialogTitle();
+  String branchDeletionConfirmationMessage();
+
   String notSignedInTitle();
   String notSignedInBody();
 
@@ -47,7 +53,8 @@
 
   String menuMine();
   String menuMyChanges();
-  String menyMyDrafts();
+  String menuMyDrafts();
+  String menuMyWatchedChanges();
   String menuMyStarredChanges();
 
   String menuAdmin();
@@ -57,6 +64,7 @@
 
   String menuDocumentation();
   String menuDocumentationIndex();
+  String menuDocumentationSearch();
   String menuDocumentationUpload();
   String menuDocumentationAccess();
 
@@ -74,7 +82,9 @@
   String sectionJumping();
   String jumpAllOpen();
   String jumpAllMerged();
+  String jumpAllAbandoned();
   String jumpMine();
   String jumpMineDrafts();
+  String jumpMineWatched();
   String jumpMineStarred();
 }
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 4acbe17e..83a06e4 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
@@ -13,6 +13,12 @@
 errorDialogTitle = Application Error
 errorDialogContinue = Continue
 
+confirmationDialogOk = OK
+confirmationDialogCancel = Cancel
+
+branchDeletionDialogTitle = Branch Deletion
+branchDeletionConfirmationMessage = Do you really want to delete the following branches?
+
 notSignedInTitle = Code Review - Session Expired
 notSignedInBody = <b>Session Expired</b>\
 <p>You are no longer signed in to Gerrit Code Review.</p>\
@@ -30,8 +36,9 @@
 
 menuMine = My
 menuMyChanges = Changes
-menyMyDrafts = Drafts
+menuMyDrafts = Drafts
 menuMyStarredChanges = Starred Changes
+menuMyWatchedChanges = Watched Changes
 
 menuAdmin = Admin
 menuPeople = People
@@ -40,7 +47,8 @@
 
 menuDocumentation = Documentation
 menuDocumentationIndex = Index
-menuDocumentationUpload = Uploading Changes
+menuDocumentationSearch = Searching
+menuDocumentationUpload = Uploading
 menuDocumentationAccess = Access Controls
 
 searchHint = Change #, SHA-1, tr:id, owner:email or reviewer:email
@@ -57,6 +65,8 @@
 sectionJumping = Jumping
 jumpAllOpen = Go to all open changes
 jumpAllMerged = Go to all merged changes
+jumpAllAbandoned = Go to all abandoned changes
 jumpMine = Go to my dashboard
+jumpMineWatched = Go to watched changes
 jumpMineDrafts = Go to drafts
 jumpMineStarred = Go to starred changes
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 2d631ef..c556770 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
@@ -25,11 +25,14 @@
   String accountDashboard();
   String accountInfoBlock();
   String accountName();
+  String accountUsername();
+  String accountPassword();
   String activeRow();
   String addReviewer();
   String removeReviewer();
   String removeReviewerCell();
   String addSshKeyPanel();
+  String addWatchPanel();
   String approvalCategoryList();
   String approvalTable();
   String approvalhint();
@@ -123,6 +126,7 @@
   String linkMenuItemNotLast();
   String menuBarUserName();
   String menuItem();
+  String menuScreenMenuBar();
   String missingApproval();
   String missingApprovalList();
   String needsReview();
@@ -168,8 +172,6 @@
   String sshHostKeyPanelKnownHostEntry();
   String sshKeyPanelEncodedKey();
   String sshKeyPanelInvalid();
-  String sshPanelUsername();
-  String sshPanelPassword();
   String topmenu();
   String topmenuMenuLeft();
   String topmenuMenuRight();
@@ -177,5 +179,7 @@
   String topmenuTDmenu();
   String topmost();
   String useridentity();
+  String usernameField();
   String version();
+  String watchedProjectFilter();
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/JumpKeys.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/JumpKeys.java
index 6c789bf..3ec5372 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/JumpKeys.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/JumpKeys.java
@@ -38,6 +38,12 @@
         Gerrit.display(PageLinks.ALL_MERGED);
       }
     });
+    jumps.add(new KeyCommand(0, 'a', Gerrit.C.jumpAllAbandoned()) {
+      @Override
+      public void onKeyPress(final KeyPressEvent event) {
+        Gerrit.display(PageLinks.ALL_ABANDONED);
+      }
+    });
 
     if (Gerrit.isSignedIn()) {
       jumps.add(new KeyCommand(0, 'i', Gerrit.C.jumpMine()) {
@@ -52,6 +58,12 @@
           Gerrit.display(PageLinks.MINE_DRAFTS);
         }
       });
+      jumps.add(new KeyCommand(0, 'w', Gerrit.C.jumpMineWatched()) {
+        @Override
+        public void onKeyPress(final KeyPressEvent event) {
+          Gerrit.display(PageLinks.MINE_WATCHED);
+        }
+      });
       jumps.add(new KeyCommand(0, 's', Gerrit.C.jumpMineStarred()) {
         @Override
         public void onKeyPress(final KeyPressEvent event) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/NotSignedInDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/NotSignedInDialog.java
index af86a21..f354496 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/NotSignedInDialog.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/NotSignedInDialog.java
@@ -16,26 +16,35 @@
 
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
 import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.History;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwtexpui.globalkey.client.GlobalKey;
 import com.google.gwtexpui.user.client.AutoCenterDialogBox;
 
 /** A dialog box telling the user they are not signed in. */
-public class NotSignedInDialog extends AutoCenterDialogBox {
+public class NotSignedInDialog extends AutoCenterDialogBox implements CloseHandler<PopupPanel> {
+
+  private Button signin;
+  private boolean buttonClicked = false;
+
   public NotSignedInDialog() {
     super(/* auto hide */false, /* modal */true);
     setGlassEnabled(true);
     setText(Gerrit.C.notSignedInTitle());
 
     final FlowPanel buttons = new FlowPanel();
-    final Button signin = new Button();
+    signin = new Button();
     signin.setText(Gerrit.C.menuSignIn());
     signin.addClickHandler(new ClickHandler() {
       @Override
       public void onClick(ClickEvent event) {
+        buttonClicked = true;
         hide();
         Gerrit.doSignIn(History.getToken());
       }
@@ -48,6 +57,7 @@
     close.addClickHandler(new ClickHandler() {
       @Override
       public void onClick(ClickEvent event) {
+        buttonClicked = true;
         Gerrit.deleteSessionCookie();
         hide();
       }
@@ -60,5 +70,23 @@
     add(center);
 
     center.setWidth("400px");
+
+    addCloseHandler(this);
+  }
+
+  @Override
+  public void onClose(CloseEvent<PopupPanel> event) {
+    if (!buttonClicked) {
+      // the dialog was closed without one of the buttons being pressed
+      // e.g. the user pressed ESC to close the dialog
+      Gerrit.deleteSessionCookie();
+    }
+  }
+
+  @Override
+  public void center() {
+    super.center();
+    GlobalKey.dialog(this);
+    signin.setFocus(true);
   }
 }
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 db9ff59..2d3d2e8 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
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.client;
 
+import com.google.gerrit.client.changes.QueryScreen;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gwt.event.dom.client.BlurEvent;
@@ -43,7 +44,7 @@
     setStyleName(Gerrit.RESOURCES.css().searchPanel());
 
     searchBox = new NpTextBox();
-    searchBox.setVisibleLength(46);
+    searchBox.setVisibleLength(70);
     searchBox.setText(Gerrit.C.searchHint());
     searchBox.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
     searchBox.addFocusHandler(new FocusHandler() {
@@ -137,7 +138,7 @@
     if (query.matches("^[1-9][0-9]*$")) {
       Gerrit.display(PageLinks.toChange(Change.Id.parse(query)));
     } else {
-      Gerrit.display(PageLinks.toChangeQuery(query));
+      Gerrit.display(PageLinks.toChangeQuery(query), QueryScreen.forQuery(query));
     }
   }
 }
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 24439c7..3756bed 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
@@ -17,23 +17,26 @@
 import com.google.gwt.i18n.client.Constants;
 
 public interface AccountConstants extends Constants {
-  String accountSettingsHeading();
+  String settingsHeading();
 
   String fullName();
   String preferredEmail();
   String registeredOn();
   String accountId();
 
-  String defaultContextFieldLabel();
   String maximumPageSizeFieldLabel();
   String contextWholeFile();
   String showSiteHeader();
   String useFlashClipboard();
+  String copySelfOnEmails();
   String buttonSaveChanges();
 
+  String tabAccountSummary();
   String tabPreferences();
+  String tabWatchedProjects();
   String tabContactInformation();
   String tabSshKeys();
+  String tabHttpAccess();
   String tabWebIdentities();
   String tabMyGroups();
   String tabAgreements();
@@ -47,7 +50,9 @@
 
   String userName();
   String password();
+  String buttonSetUserName();
   String buttonChangeUserName();
+  String buttonClearPassword();
   String buttonGeneratePassword();
   String invalidUserName();
 
@@ -73,9 +78,11 @@
   String buttonDeleteIdentity();
   String buttonLinkIdentity();
 
-  String watchedProjects();
   String buttonWatchProject();
   String defaultProjectName();
+  String defaultFilter();
+  String watchedProjectName();
+  String watchedProjectFilter();
   String watchedProjectColumnEmailNotifications();
   String watchedProjectColumnNewChanges();
   String watchedProjectColumnAllComments();
@@ -116,6 +123,7 @@
   String welcomeToGerritCodeReview();
   String welcomeReviewContact();
   String welcomeContactFrom();
+  String welcomeUsernameHeading();
   String welcomeSshKeyHeading();
   String welcomeSshKeyText();
   String welcomeAgreementHeading();
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 09ae95a..29ee14a 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
@@ -1,4 +1,4 @@
-accountSettingsHeading = Account Settings
+settingsHeading = Settings
 
 fullName = Full Name
 preferredEmail = Email Address
@@ -6,14 +6,18 @@
 accountId = Account ID
 showSiteHeader = Show Site Header
 useFlashClipboard = Use Flash Clipboard Widget
+copySelfOnEmails = CC Me On Comments I Write
 defaultContextFieldLabel = Default Context:
 maximumPageSizeFieldLabel = Maximum Page Size:
 contextWholeFile = Whole File
 buttonSaveChanges = Save Changes
 
+tabAccountSummary = Profile
 tabPreferences = Preferences
+tabWatchedProjects = Watched Projects
 tabContactInformation = Contact Information
-tabSshKeys = SSH Keys
+tabSshKeys = SSH Public Keys
+tabHttpAccess = HTTP Password
 tabWebIdentities = Identities
 tabMyGroups = Groups
 tabAgreements = Agreements
@@ -27,8 +31,10 @@
 
 userName = Username
 password = Password
+buttonSetUserName = Select Username
 buttonChangeUserName = Change Username
-buttonGeneratePassword = Regenerate
+buttonClearPassword = Clear Password
+buttonGeneratePassword = Generate Password
 invalidUserName = Username must contain only letters, numbers, _, - or .
 
 sshKeyInvalid = Invalid Key
@@ -53,9 +59,11 @@
 invalidSshKeyError = Invalid SSH Key
 sshJavaAppletNotAvailable = Open Key Unavailable: Java not enabled
 
-watchedProjects = Watched Projects
 buttonWatchProject = Watch
 defaultProjectName = Project Name
+defaultFilter = branch:name, or other search expression
+watchedProjectName = Project Name
+watchedProjectFilter = Only If
 watchedProjectColumnEmailNotifications = Email Notifications
 watchedProjectColumnNewChanges = New Changes
 watchedProjectColumnAllComments = All Comments
@@ -111,6 +119,8 @@
   you are to others, and to send updates to code reviews you have either \
   started or subscribed to.</p>
 
+welcomeUsernameHeading = Select a unique username:
+
 welcomeSshKeyHeading = Register an SSH public key:
 welcomeSshKeyText = \
   <p>Gerrit Code Review uses \
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountSettings.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountSettings.java
deleted file mode 100644
index 59a7547..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountSettings.java
+++ /dev/null
@@ -1,181 +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.account;
-
-import static com.google.gerrit.client.FormatUtil.mediumFormat;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.ui.AccountScreen;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gwt.event.logical.shared.SelectionEvent;
-import com.google.gwt.event.logical.shared.SelectionHandler;
-import com.google.gwt.i18n.client.LocaleInfo;
-import com.google.gwt.user.client.ui.Grid;
-import com.google.gwt.user.client.ui.LazyPanel;
-import com.google.gwt.user.client.ui.TabPanel;
-import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class AccountSettings extends AccountScreen {
-  private final String initialTabToken;
-  private int labelIdx, fieldIdx;
-  private Grid info;
-
-  private List<String> tabTokens;
-  private TabPanel tabs;
-
-  public AccountSettings(final String tabToken) {
-    initialTabToken = tabToken;
-  }
-
-  @Override
-  public boolean displayToken(String token) {
-    final int tabIdx = tabTokens.indexOf(token);
-    if (0 <= tabIdx) {
-      tabs.selectTab(tabIdx);
-      setToken(token);
-      return true;
-    } else {
-      return false;
-    }
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-
-    final int idx = tabTokens.indexOf(initialTabToken);
-    tabs.selectTab(0 <= idx ? idx : 0);
-    display(Gerrit.getUserAccount());
-    display();
-  }
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    setPageTitle(Util.C.accountSettingsHeading());
-
-    if (LocaleInfo.getCurrentLocale().isRTL()) {
-      labelIdx = 1;
-      fieldIdx = 0;
-    } else {
-      labelIdx = 0;
-      fieldIdx = 1;
-    }
-
-    info = new Grid(4, 2);
-    info.setStyleName(Gerrit.RESOURCES.css().infoBlock());
-    info.addStyleName(Gerrit.RESOURCES.css().accountInfoBlock());
-    add(info);
-
-    infoRow(0, Util.C.fullName());
-    infoRow(1, Util.C.preferredEmail());
-    infoRow(2, Util.C.registeredOn());
-    infoRow(3, Util.C.accountId());
-
-    final CellFormatter fmt = info.getCellFormatter();
-    fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().topmost());
-    fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().topmost());
-    fmt.addStyleName(3, 0, Gerrit.RESOURCES.css().bottomheader());
-
-    tabTokens = new ArrayList<String>();
-    tabs = new TabPanel();
-    tabs.setWidth("98%");
-    add(tabs);
-
-    tabs.add(new LazyPanel() {
-      @Override
-      protected PreferencePanel createWidget() {
-        return new PreferencePanel();
-      }
-    }, Util.C.tabPreferences());
-    tabTokens.add(PageLinks.SETTINGS);
-
-    tabs.add(new LazyPanel() {
-      @Override
-      protected ProjectWatchPanel createWidget() {
-        return new ProjectWatchPanel();
-      }
-    }, Util.C.watchedProjects());
-    tabTokens.add(PageLinks.SETTINGS_PROJECTS);
-
-    tabs.add(new LazyPanel() {
-      @Override
-      protected ContactPanelFull createWidget() {
-        final ContactPanelFull p = new ContactPanelFull();
-        p.accountSettings = AccountSettings.this;
-        return p;
-      }
-    }, Util.C.tabContactInformation());
-    tabTokens.add(PageLinks.SETTINGS_CONTACT);
-
-    tabs.add(new LazyPanel() {
-      @Override
-      protected SshPanel createWidget() {
-        return new SshPanel();
-      }
-    }, Util.C.tabSshKeys());
-    tabTokens.add(PageLinks.SETTINGS_SSHKEYS);
-
-    tabs.add(new LazyPanel() {
-      @Override
-      protected ExternalIdPanel createWidget() {
-        return new ExternalIdPanel();
-      }
-    }, Util.C.tabWebIdentities());
-    tabTokens.add(PageLinks.SETTINGS_WEBIDENT);
-
-    tabs.add(new LazyPanel() {
-      @Override
-      protected MyGroupsPanel createWidget() {
-        return new MyGroupsPanel();
-      }
-    }, Util.C.tabMyGroups());
-    tabTokens.add(PageLinks.SETTINGS_MYGROUPS);
-
-    if (Gerrit.getConfig().isUseContributorAgreements()) {
-      tabs.add(new LazyPanel() {
-        @Override
-        protected AgreementPanel createWidget() {
-          return new AgreementPanel();
-        }
-      }, Util.C.tabAgreements());
-      tabTokens.add(PageLinks.SETTINGS_AGREEMENTS);
-    }
-
-    tabs.addSelectionHandler(new SelectionHandler<Integer>() {
-      @Override
-      public void onSelection(final SelectionEvent<Integer> event) {
-        setToken(tabTokens.get(event.getSelectedItem()));
-      }
-    });
-  }
-
-  private void infoRow(final int row, final String name) {
-    info.setText(row, labelIdx, name);
-    info.getCellFormatter().addStyleName(row, 0,
-        Gerrit.RESOURCES.css().header());
-  }
-
-  void display(final Account account) {
-    info.setText(0, fieldIdx, account.getFullName());
-    info.setText(1, fieldIdx, account.getPreferredEmail());
-    info.setText(2, fieldIdx, mediumFormat(account.getRegisteredOn()));
-    info.setText(3, fieldIdx, account.getId().toString());
-  }
-}
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 923a86a..21c9163 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
@@ -20,6 +20,7 @@
 import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.reviewdb.AccountExternalId;
 import com.google.gerrit.reviewdb.ContactInformation;
+import com.google.gerrit.reviewdb.Account.FieldName;
 import com.google.gwt.event.dom.client.ChangeEvent;
 import com.google.gwt.event.dom.client.ChangeHandler;
 import com.google.gwt.event.dom.client.ClickEvent;
@@ -46,8 +47,6 @@
 import java.util.Set;
 
 class ContactPanelShort extends Composite {
-  AccountSettings accountSettings;
-
   protected final FlowPanel body;
   protected int labelIdx, fieldIdx;
   protected Button save;
@@ -100,12 +99,18 @@
       emailLine.add(registerNewEmail);
     }
 
-    row(infoPlainText, 0, Util.C.contactFieldFullName(), nameTxt);
-    row(infoPlainText, 1, Util.C.contactFieldEmail(), emailLine);
+    int row = 0;
+    if (!Gerrit.getConfig().canEdit(FieldName.USER_NAME)) {
+      infoPlainText.resizeRows(infoPlainText.getRowCount() + 1);
+      row(infoPlainText, row++, Util.C.userName(), new UsernameField());
+    }
+
+    row(infoPlainText, row++, Util.C.contactFieldFullName(), nameTxt);
+    row(infoPlainText, row++, Util.C.contactFieldEmail(), emailLine);
 
     infoPlainText.getCellFormatter().addStyleName(0, 0, Gerrit.RESOURCES.css().topmost());
     infoPlainText.getCellFormatter().addStyleName(0, 1, Gerrit.RESOURCES.css().topmost());
-    infoPlainText.getCellFormatter().addStyleName(1, 0, Gerrit.RESOURCES.css().bottomheader());
+    infoPlainText.getCellFormatter().addStyleName(row - 1, 0, Gerrit.RESOURCES.css().bottomheader());
 
     save = new Button(Util.C.buttonSaveChanges());
     save.setEnabled(false);
@@ -227,6 +232,10 @@
       }
       registerNewEmail.setEnabled(true);
     }
+    display();
+  }
+
+  void display() {
   }
 
   protected void row(final Grid info, final int row, final String name,
@@ -354,9 +363,6 @@
     me.setFullName(result.getFullName());
     me.setPreferredEmail(result.getPreferredEmail());
     Gerrit.refreshMenuBar();
-    if (accountSettings != null) {
-      accountSettings.display(me);
-    }
   }
 
   ContactInformation toContactInformation() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AgreementPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java
similarity index 88%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AgreementPanel.java
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java
index 326a1df..54de472 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AgreementPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java
@@ -16,7 +16,7 @@
 
 import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.client.ui.FancyFlexTable;
 import com.google.gerrit.client.ui.Hyperlink;
 import com.google.gerrit.common.PageLinks;
@@ -26,30 +26,27 @@
 import com.google.gerrit.reviewdb.AccountGroupAgreement;
 import com.google.gerrit.reviewdb.ContributorAgreement;
 import com.google.gwt.user.client.ui.Anchor;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
 import com.google.gwtexpui.safehtml.client.SafeHtml;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 
-class AgreementPanel extends Composite {
+public class MyAgreementsScreen extends SettingsScreen {
   private AgreementTable agreements;
 
-  AgreementPanel() {
-    final FlowPanel body = new FlowPanel();
+  @Override
+  protected void onInitUI() {
+    super.onInitUI();
 
     agreements = new AgreementTable();
-    body.add(agreements);
-    body.add(new Hyperlink(Util.C.newAgreement(), PageLinks.SETTINGS_NEW_AGREEMENT));
-
-    initWidget(body);
+    add(agreements);
+    add(new Hyperlink(Util.C.newAgreement(), PageLinks.SETTINGS_NEW_AGREEMENT));
   }
 
   @Override
   protected void onLoad() {
     super.onLoad();
-    Util.ACCOUNT_SVC.myAgreements(new GerritCallback<AgreementInfo>() {
-      public void onSuccess(final AgreementInfo result) {
+    Util.ACCOUNT_SVC.myAgreements(new ScreenLoadCallback<AgreementInfo>(this) {
+      public void preDisplay(final AgreementInfo result) {
         agreements.display(result);
       }
     });
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyContactInformationScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyContactInformationScreen.java
new file mode 100644
index 0000000..c542511
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyContactInformationScreen.java
@@ -0,0 +1,31 @@
+// 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;
+
+public class MyContactInformationScreen extends SettingsScreen {
+  private ContactPanelFull panel;
+
+  @Override
+  protected void onInitUI() {
+    super.onInitUI();
+    panel = new ContactPanelFull() {
+      @Override
+      void display() {
+        MyContactInformationScreen.this.display();
+      }
+    };
+    add(panel);
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGroupsPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGroupsScreen.java
similarity index 67%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGroupsPanel.java
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGroupsScreen.java
index 9ba20e8..b9fff04 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGroupsPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGroupsScreen.java
@@ -15,34 +15,26 @@
 package com.google.gerrit.client.account;
 
 import com.google.gerrit.client.admin.GroupTable;
-import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.FlowPanel;
 
 import java.util.List;
 
-class MyGroupsPanel extends Composite {
+public class MyGroupsScreen extends SettingsScreen {
   private GroupTable groups;
 
-  MyGroupsPanel() {
-    final FlowPanel body = new FlowPanel();
-
+  @Override
+  protected void onInitUI() {
+    super.onInitUI();
     groups = new GroupTable(false /* do not hyperlink to admin */);
-    body.add(groups);
-
-    initWidget(body);
+    add(groups);
   }
 
   @Override
   protected void onLoad() {
     super.onLoad();
-    refresh();
-  }
-
-  private void refresh() {
-    Util.ACCOUNT_SEC.myGroups(new GerritCallback<List<AccountGroup>>() {
-      public void onSuccess(final List<AccountGroup> result) {
+    Util.ACCOUNT_SEC.myGroups(new ScreenLoadCallback<List<AccountGroup>>(this) {
+      public void preDisplay(final List<AccountGroup> result) {
         groups.display(result);
       }
     });
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ExternalIdPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyIdentitiesScreen.java
similarity index 94%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ExternalIdPanel.java
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyIdentitiesScreen.java
index 84d8e8a..f3816f2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ExternalIdPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyIdentitiesScreen.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.client.auth.openid.OpenIdSignInDialog;
 import com.google.gerrit.client.auth.openid.OpenIdUtil;
 import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.client.ui.FancyFlexTable;
 import com.google.gerrit.common.auth.SignInMode;
 import com.google.gerrit.common.auth.openid.OpenIdUrls;
@@ -29,8 +30,6 @@
 import com.google.gwt.user.client.History;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.CheckBox;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
 
 import java.util.Collections;
@@ -39,16 +38,16 @@
 import java.util.List;
 import java.util.Set;
 
-class ExternalIdPanel extends Composite {
+public class MyIdentitiesScreen extends SettingsScreen {
   private IdTable identites;
   private Button deleteIdentity;
 
-  ExternalIdPanel() {
-    final FlowPanel body = new FlowPanel();
-    body.add(new UsernamePanel());
+  @Override
+  protected void onInitUI() {
+    super.onInitUI();
 
     identites = new IdTable();
-    body.add(identites);
+    add(identites);
 
     deleteIdentity = new Button(Util.C.buttonDeleteIdentity());
     deleteIdentity.setEnabled(false);
@@ -58,7 +57,7 @@
         identites.deleteChecked();
       }
     });
-    body.add(deleteIdentity);
+    add(deleteIdentity);
 
     switch (Gerrit.getConfig().getAuthType()) {
       case OPENID: {
@@ -70,21 +69,18 @@
             new OpenIdSignInDialog(SignInMode.LINK_IDENTIY, to, null).center();
           }
         });
-        body.add(linkIdentity);
+        add(linkIdentity);
         break;
       }
     }
-
-    initWidget(body);
   }
 
   @Override
   protected void onLoad() {
     super.onLoad();
-
     Util.ACCOUNT_SEC
-        .myExternalIds(new GerritCallback<List<AccountExternalId>>() {
-          public void onSuccess(final List<AccountExternalId> result) {
+        .myExternalIds(new ScreenLoadCallback<List<AccountExternalId>>(this) {
+          public void preDisplay(final List<AccountExternalId> result) {
             identites.display(result);
           }
         });
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPasswordScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPasswordScreen.java
new file mode 100644
index 0000000..2202ac8
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPasswordScreen.java
@@ -0,0 +1,176 @@
+// 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.account;
+
+import static com.google.gerrit.reviewdb.AccountExternalId.SCHEME_USERNAME;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.ScreenLoadCallback;
+import com.google.gerrit.reviewdb.AccountExternalId;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.i18n.client.LocaleInfo;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.Grid;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
+import com.google.gwtexpui.clippy.client.CopyableLabel;
+
+import java.util.List;
+
+public class MyPasswordScreen extends SettingsScreen {
+  private CopyableLabel password;
+  private Button generatePassword;
+  private Button clearPassword;
+  private AccountExternalId id;
+
+  @Override
+  protected void onInitUI() {
+    super.onInitUI();
+
+    password = new CopyableLabel("");
+    password.addStyleName(Gerrit.RESOURCES.css().accountPassword());
+
+    generatePassword = new Button(Util.C.buttonGeneratePassword());
+    generatePassword.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(ClickEvent event) {
+        doGeneratePassword();
+      }
+    });
+
+    clearPassword = new Button(Util.C.buttonClearPassword());
+    clearPassword.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(ClickEvent event) {
+        doClearPassword();
+      }
+    });
+
+    final Grid userInfo = new Grid(2, 2);
+    final CellFormatter fmt = userInfo.getCellFormatter();
+    userInfo.setStyleName(Gerrit.RESOURCES.css().infoBlock());
+    userInfo.addStyleName(Gerrit.RESOURCES.css().accountInfoBlock());
+    add(userInfo);
+
+    row(userInfo, 0, Util.C.userName(), new UsernameField());
+    row(userInfo, 1, Util.C.password(), password);
+
+    fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().topmost());
+    fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().topmost());
+    fmt.addStyleName(1, 0, Gerrit.RESOURCES.css().bottomheader());
+
+    final FlowPanel buttons = new FlowPanel();
+    buttons.add(generatePassword);
+    buttons.add(clearPassword);
+    add(buttons);
+  }
+
+  @Override
+  protected void onLoad() {
+    super.onLoad();
+
+    enableUI(false);
+    Util.ACCOUNT_SEC
+        .myExternalIds(new ScreenLoadCallback<List<AccountExternalId>>(this) {
+          public void preDisplay(final List<AccountExternalId> result) {
+            AccountExternalId id = null;
+            for (AccountExternalId i : result) {
+              if (i.isScheme(SCHEME_USERNAME)) {
+                id = i;
+                break;
+              }
+            }
+            display(id);
+          }
+        });
+  }
+
+  private void display(AccountExternalId id) {
+    String user, pass;
+    if (id != null) {
+      user = id.getSchemeRest();
+      pass = id.getPassword();
+    } else {
+      user = null;
+      pass = null;
+    }
+    this.id = id;
+
+    Gerrit.getUserAccount().setUserName(user);
+
+    password.setText(pass != null ? pass : "");
+    password.setVisible(pass != null);
+
+    enableUI(true);
+  }
+
+  private void row(final Grid info, final int row, final String name,
+      final Widget field) {
+    final CellFormatter fmt = info.getCellFormatter();
+    if (LocaleInfo.getCurrentLocale().isRTL()) {
+      info.setText(row, 1, name);
+      info.setWidget(row, 0, field);
+      fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().header());
+    } else {
+      info.setText(row, 0, name);
+      info.setWidget(row, 1, field);
+      fmt.addStyleName(row, 0, Gerrit.RESOURCES.css().header());
+    }
+  }
+
+  private void doGeneratePassword() {
+    if (id != null) {
+      enableUI(false);
+      Util.ACCOUNT_SEC.generatePassword(id.getKey(),
+          new GerritCallback<AccountExternalId>() {
+            public void onSuccess(final AccountExternalId result) {
+              display(result);
+            }
+
+            @Override
+            public void onFailure(final Throwable caught) {
+              enableUI(true);
+            }
+          });
+    }
+  }
+
+  private void doClearPassword() {
+    if (id != null) {
+      enableUI(false);
+      Util.ACCOUNT_SEC.clearPassword(id.getKey(),
+          new GerritCallback<AccountExternalId>() {
+            public void onSuccess(final AccountExternalId result) {
+              display(result);
+            }
+
+            @Override
+            public void onFailure(final Throwable caught) {
+              enableUI(true);
+            }
+          });
+    }
+  }
+
+  private void enableUI(boolean on) {
+    on &= id != null;
+
+    generatePassword.setEnabled(on);
+    clearPassword.setVisible(on && id.getPassword() != null);
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/PreferencePanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
similarity index 79%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/account/PreferencePanel.java
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
index 583aa2a..bb81f3f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/PreferencePanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
@@ -14,14 +14,12 @@
 
 package com.google.gerrit.client.account;
 
-import static com.google.gerrit.reviewdb.AccountGeneralPreferences.CONTEXT_CHOICES;
-import static com.google.gerrit.reviewdb.AccountGeneralPreferences.DEFAULT_CONTEXT;
 import static com.google.gerrit.reviewdb.AccountGeneralPreferences.DEFAULT_PAGESIZE;
 import static com.google.gerrit.reviewdb.AccountGeneralPreferences.PAGESIZE_CHOICES;
-import static com.google.gerrit.reviewdb.AccountGeneralPreferences.WHOLE_FILE_CONTEXT;
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.reviewdb.AccountGeneralPreferences;
 import com.google.gwt.event.dom.client.ChangeEvent;
@@ -31,21 +29,20 @@
 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.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.gwtjsonrpc.client.VoidResult;
 
-class PreferencePanel extends Composite {
+public class MyPreferencesScreen extends SettingsScreen {
   private CheckBox showSiteHeader;
   private CheckBox useFlashClipboard;
-  private ListBox defaultContext;
+  private CheckBox copySelfOnEmails;
   private ListBox maximumPageSize;
   private Button save;
 
-  PreferencePanel() {
-    final FlowPanel body = new FlowPanel();
+  @Override
+  protected void onInitUI() {
+    super.onInitUI();
 
     final ClickHandler onClickSave = new ClickHandler() {
       @Override
@@ -66,24 +63,15 @@
     useFlashClipboard = new CheckBox(Util.C.useFlashClipboard());
     useFlashClipboard.addClickHandler(onClickSave);
 
+    copySelfOnEmails = new CheckBox(Util.C.copySelfOnEmails());
+    copySelfOnEmails.addClickHandler(onClickSave);
+
     maximumPageSize = new ListBox();
     for (final short v : PAGESIZE_CHOICES) {
       maximumPageSize.addItem(Util.M.rowsPerPage(v), String.valueOf(v));
     }
     maximumPageSize.addChangeHandler(onChangeSave);
 
-    defaultContext = new ListBox();
-    for (final short v : CONTEXT_CHOICES) {
-      final String label;
-      if (v == WHOLE_FILE_CONTEXT) {
-        label = Util.C.contextWholeFile();
-      } else {
-        label = Util.M.lines(v);
-      }
-      defaultContext.addItem(label, String.valueOf(v));
-    }
-    defaultContext.addChangeHandler(onChangeSave);
-
     final int labelIdx, fieldIdx;
     if (LocaleInfo.getCurrentLocale().isRTL()) {
       labelIdx = 1;
@@ -103,15 +91,15 @@
     formGrid.setWidget(row, fieldIdx, useFlashClipboard);
     row++;
 
+    formGrid.setText(row, labelIdx, "");
+    formGrid.setWidget(row, fieldIdx, copySelfOnEmails);
+    row++;
+
     formGrid.setText(row, labelIdx, Util.C.maximumPageSizeFieldLabel());
     formGrid.setWidget(row, fieldIdx, maximumPageSize);
     row++;
 
-    formGrid.setText(row, labelIdx, Util.C.defaultContextFieldLabel());
-    formGrid.setWidget(row, fieldIdx, defaultContext);
-    row++;
-
-    body.add(formGrid);
+    add(formGrid);
 
     save = new Button(Util.C.buttonSaveChanges());
     save.setEnabled(false);
@@ -121,18 +109,15 @@
         doSave();
       }
     });
-    body.add(save);
-
-    initWidget(body);
+    add(save);
   }
 
   @Override
   protected void onLoad() {
     super.onLoad();
-    Util.ACCOUNT_SVC.myAccount(new GerritCallback<Account>() {
-      public void onSuccess(final Account result) {
+    Util.ACCOUNT_SVC.myAccount(new ScreenLoadCallback<Account>(this) {
+      public void preDisplay(final Account result) {
         display(result.getGeneralPreferences());
-        enable(true);
       }
     });
   }
@@ -140,15 +125,15 @@
   private void enable(final boolean on) {
     showSiteHeader.setEnabled(on);
     useFlashClipboard.setEnabled(on);
+    copySelfOnEmails.setEnabled(on);
     maximumPageSize.setEnabled(on);
-    defaultContext.setEnabled(on);
   }
 
   private void display(final AccountGeneralPreferences p) {
     showSiteHeader.setValue(p.isShowSiteHeader());
     useFlashClipboard.setValue(p.isUseFlashClipboard());
+    copySelfOnEmails.setValue(p.isCopySelfOnEmails());
     setListBox(maximumPageSize, DEFAULT_PAGESIZE, p.getMaximumPageSize());
-    setListBox(defaultContext, DEFAULT_CONTEXT, p.getDefaultContext());
   }
 
   private void setListBox(final ListBox f, final short defaultValue,
@@ -177,8 +162,8 @@
     final AccountGeneralPreferences p = new AccountGeneralPreferences();
     p.setShowSiteHeader(showSiteHeader.getValue());
     p.setUseFlashClipboard(useFlashClipboard.getValue());
+    p.setCopySelfOnEmails(copySelfOnEmails.getValue());
     p.setMaximumPageSize(getListBox(maximumPageSize, DEFAULT_PAGESIZE));
-    p.setDefaultContext(getListBox(defaultContext, DEFAULT_CONTEXT));
 
     enable(false);
     save.setEnabled(false);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyProfileScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyProfileScreen.java
new file mode 100644
index 0000000..65c2590
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyProfileScreen.java
@@ -0,0 +1,78 @@
+// Copyright (C) 2008 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.account;
+
+import static com.google.gerrit.client.FormatUtil.mediumFormat;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.reviewdb.Account;
+import com.google.gwt.i18n.client.LocaleInfo;
+import com.google.gwt.user.client.ui.Grid;
+import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
+
+public class MyProfileScreen extends SettingsScreen {
+  private int labelIdx, fieldIdx;
+  private Grid info;
+
+  @Override
+  protected void onInitUI() {
+    super.onInitUI();
+
+    if (LocaleInfo.getCurrentLocale().isRTL()) {
+      labelIdx = 1;
+      fieldIdx = 0;
+    } else {
+      labelIdx = 0;
+      fieldIdx = 1;
+    }
+
+    info = new Grid(5, 2);
+    info.setStyleName(Gerrit.RESOURCES.css().infoBlock());
+    info.addStyleName(Gerrit.RESOURCES.css().accountInfoBlock());
+    add(info);
+
+    infoRow(0, Util.C.userName());
+    infoRow(1, Util.C.fullName());
+    infoRow(2, Util.C.preferredEmail());
+    infoRow(3, Util.C.registeredOn());
+    infoRow(4, Util.C.accountId());
+
+    final CellFormatter fmt = info.getCellFormatter();
+    fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().topmost());
+    fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().topmost());
+    fmt.addStyleName(4, 0, Gerrit.RESOURCES.css().bottomheader());
+  }
+
+  @Override
+  protected void onLoad() {
+    super.onLoad();
+    display(Gerrit.getUserAccount());
+    display();
+  }
+
+  private void infoRow(final int row, final String name) {
+    info.setText(row, labelIdx, name);
+    info.getCellFormatter().addStyleName(row, 0,
+        Gerrit.RESOURCES.css().header());
+  }
+
+  void display(final Account account) {
+    info.setWidget(0, fieldIdx, new UsernameField());
+    info.setText(1, fieldIdx, account.getFullName());
+    info.setText(2, fieldIdx, account.getPreferredEmail());
+    info.setText(3, fieldIdx, mediumFormat(account.getRegisteredOn()));
+    info.setText(4, fieldIdx, account.getId().toString());
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MySshKeysScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MySshKeysScreen.java
new file mode 100644
index 0000000..b1d6d82
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MySshKeysScreen.java
@@ -0,0 +1,31 @@
+// 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;
+
+public class MySshKeysScreen extends SettingsScreen {
+  private SshPanel panel;
+
+  @Override
+  protected void onInitUI() {
+    super.onInitUI();
+    panel = new SshPanel() {
+      @Override
+      void display() {
+        MySshKeysScreen.this.display();
+      }
+    };
+    add(panel);
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ProjectWatchPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java
similarity index 71%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ProjectWatchPanel.java
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java
index 9fb3748..28099bf 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ProjectWatchPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java
@@ -16,6 +16,7 @@
 
 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.ProjectNameSuggestOracle;
@@ -36,10 +37,12 @@
 import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.CheckBox;
-import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.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.SuggestOracle.Suggestion;
 import com.google.gwtexpui.globalkey.client.NpTextBox;
 import com.google.gwtjsonrpc.client.VoidResult;
@@ -47,44 +50,45 @@
 import java.util.HashSet;
 import java.util.List;
 
-class ProjectWatchPanel extends Composite {
+public class MyWatchedProjectsScreen extends SettingsScreen {
   private WatchTable watches;
 
   private Button addNew;
+  private NpTextBox nameBox;
   private SuggestBox nameTxt;
+  private NpTextBox filterTxt;
   private Button delSel;
   private boolean submitOnSelection;
 
-  ProjectWatchPanel() {
-    final FlowPanel body = new FlowPanel();
+  @Override
+  protected void onInitUI() {
+    super.onInitUI();
 
     {
-      final FlowPanel fp = new FlowPanel();
-
-      final NpTextBox box = new NpTextBox();
-      nameTxt = new SuggestBox(new ProjectNameSuggestOracle(), box);
-      box.setVisibleLength(50);
-      box.setText(Util.C.defaultProjectName());
-      box.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
-      box.addFocusHandler(new FocusHandler() {
+      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(box.getText())) {
-            box.setText("");
-            box.removeStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
+          if (Util.C.defaultProjectName().equals(nameBox.getText())) {
+            nameBox.setText("");
+            nameBox.removeStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
           }
         }
       });
-      box.addBlurHandler(new BlurHandler() {
+      nameBox.addBlurHandler(new BlurHandler() {
         @Override
         public void onBlur(BlurEvent event) {
-          if ("".equals(box.getText())) {
-            box.setText(Util.C.defaultProjectName());
-            box.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
+          if ("".equals(nameBox.getText())) {
+            nameBox.setText(Util.C.defaultProjectName());
+            nameBox.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
           }
         }
       });
-      box.addKeyPressHandler(new KeyPressHandler() {
+      nameBox.addKeyPressHandler(new KeyPressHandler() {
         @Override
         public void onKeyPress(KeyPressEvent event) {
           submitOnSelection = false;
@@ -107,7 +111,37 @@
           }
         }
       });
-      fp.add(nameTxt);
+
+      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() {
@@ -116,26 +150,40 @@
           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);
+
+      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);
-      body.add(fp);
+      add(fp);
     }
 
     watches = new WatchTable();
-    body.add(watches);
-    {
-      final FlowPanel fp = new FlowPanel();
-      delSel = new Button(Util.C.buttonDeleteSshKey());
-      delSel.addClickHandler(new ClickHandler() {
-        @Override
-        public void onClick(final ClickEvent event) {
-          watches.deleteChecked();
-        }
-      });
-      fp.add(delSel);
-      body.add(fp);
-    }
+    add(watches);
 
-    initWidget(body);
+    delSel = new Button(Util.C.buttonDeleteSshKey());
+    delSel.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(final ClickEvent event) {
+        watches.deleteChecked();
+      }
+    });
+    add(delSel);
   }
 
   void doAddNew() {
@@ -145,11 +193,23 @@
       return;
     }
 
+    String filter = filterTxt.getText();
+    if (filter == null || filter.isEmpty()
+        || filter.equals(Util.C.defaultFilter())) {
+      filter = null;
+    }
+
     addNew.setEnabled(false);
-    Util.ACCOUNT_SVC.addProjectWatch(projectName,
+    nameBox.setEnabled(false);
+    filterTxt.setEnabled(false);
+
+    Util.ACCOUNT_SVC.addProjectWatch(projectName, filter,
         new GerritCallback<AccountProjectWatchInfo>() {
           public void onSuccess(final AccountProjectWatchInfo result) {
             addNew.setEnabled(true);
+            nameBox.setEnabled(true);
+            filterTxt.setEnabled(true);
+
             nameTxt.setText("");
             watches.insertWatch(result);
           }
@@ -157,6 +217,9 @@
           @Override
           public void onFailure(final Throwable caught) {
             addNew.setEnabled(true);
+            nameBox.setEnabled(true);
+            filterTxt.setEnabled(true);
+
             super.onFailure(caught);
           }
         });
@@ -166,8 +229,9 @@
   protected void onLoad() {
     super.onLoad();
     Util.ACCOUNT_SVC
-        .myProjectWatch(new GerritCallback<List<AccountProjectWatchInfo>>() {
-          public void onSuccess(final List<AccountProjectWatchInfo> result) {
+        .myProjectWatch(new ScreenLoadCallback<List<AccountProjectWatchInfo>>(
+            this) {
+          public void preDisplay(final List<AccountProjectWatchInfo> result) {
             watches.display(result);
           }
         });
@@ -177,8 +241,7 @@
     WatchTable() {
       table.setWidth("");
       table.insertRow(1);
-      table.setText(0, 2, com.google.gerrit.client.changes.Util.C
-          .changeTableColumnProject());
+      table.setText(0, 2, Util.C.watchedProjectName());
       table.setText(0, 3, Util.C.watchedProjectColumnEmailNotifications());
 
       final FlexCellFormatter fmt = table.getFlexCellFormatter();
@@ -253,8 +316,16 @@
     }
 
     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, new ProjectLink(k.getProject().getNameKey(), Status.NEW));
+      table.setWidget(row, 2, fp);
       {
         final CheckBox notifyNewChanges = new CheckBox();
         notifyNewChanges.addClickHandler(new ClickHandler() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/RegisterScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/RegisterScreen.java
index f43d1ac..8c99d45 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/RegisterScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/RegisterScreen.java
@@ -20,9 +20,13 @@
 import com.google.gerrit.client.ui.SmallHeading;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.Account.FieldName;
+import com.google.gwt.i18n.client.LocaleInfo;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.FormPanel;
+import com.google.gwt.user.client.ui.Grid;
 import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
 
 public class RegisterScreen extends AccountScreen {
   private final String nextToken;
@@ -65,6 +69,36 @@
     });
     formBody.add(contactGroup);
 
+    if (Gerrit.getUserAccount().getUserName() == null
+        && Gerrit.getConfig().canEdit(FieldName.USER_NAME)) {
+      final FlowPanel fp = new FlowPanel();
+      fp.setStyleName(Gerrit.RESOURCES.css().registerScreenSection());
+      fp.add(new SmallHeading(Util.C.welcomeUsernameHeading()));
+
+      final Grid userInfo = new Grid(1, 2);
+      final CellFormatter fmt = userInfo.getCellFormatter();
+      userInfo.setStyleName(Gerrit.RESOURCES.css().infoBlock());
+      userInfo.addStyleName(Gerrit.RESOURCES.css().accountInfoBlock());
+      fp.add(userInfo);
+
+      fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().topmost());
+      fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().topmost());
+      fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().bottomheader());
+
+      UsernameField field = new UsernameField();
+      if (LocaleInfo.getCurrentLocale().isRTL()) {
+        userInfo.setText(0, 1, Util.C.userName());
+        userInfo.setWidget(0, 0, field);
+        fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().header());
+      } else {
+        userInfo.setText(0, 0, Util.C.userName());
+        userInfo.setWidget(0, 1, field);
+        fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().header());
+      }
+
+      formBody.add(fp);
+    }
+
     final FlowPanel sshKeyGroup = new FlowPanel();
     sshKeyGroup.setStyleName(Gerrit.RESOURCES.css().registerScreenSection());
     sshKeyGroup.add(new SmallHeading(Util.C.welcomeSshKeyHeading()));
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java
new file mode 100644
index 0000000..9356018
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.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.client.account;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.ui.MenuScreen;
+import com.google.gerrit.common.PageLinks;
+
+public abstract class SettingsScreen extends MenuScreen {
+  public SettingsScreen() {
+    setRequiresSignIn(true);
+
+    link(Util.C.tabAccountSummary(), PageLinks.SETTINGS);
+    link(Util.C.tabPreferences(), PageLinks.SETTINGS_PREFERENCES);
+    link(Util.C.tabWatchedProjects(), PageLinks.SETTINGS_PROJECTS);
+    link(Util.C.tabContactInformation(), PageLinks.SETTINGS_CONTACT);
+    link(Util.C.tabSshKeys(), PageLinks.SETTINGS_SSHKEYS);
+    link(Util.C.tabHttpAccess(), PageLinks.SETTINGS_HTTP_PASSWORD);
+    link(Util.C.tabWebIdentities(), PageLinks.SETTINGS_WEBIDENT);
+    link(Util.C.tabMyGroups(), PageLinks.SETTINGS_MYGROUPS);
+    if (Gerrit.getConfig().isUseContributorAgreements()) {
+      link(Util.C.tabAgreements(), PageLinks.SETTINGS_AGREEMENTS);
+    }
+  }
+
+  @Override
+  protected void onInitUI() {
+    super.onInitUI();
+    setPageTitle(Util.C.settingsHeading());
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshPanel.java
index a320818..fba55b3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshPanel.java
@@ -41,6 +41,7 @@
 import com.google.gwt.user.client.ui.RootPanel;
 import com.google.gwt.user.client.ui.VerticalPanel;
 import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
+import com.google.gwtexpui.clippy.client.CopyableLabel;
 import com.google.gwtexpui.globalkey.client.NpTextArea;
 import com.google.gwtjsonrpc.client.RemoteJsonException;
 import com.google.gwtjsonrpc.client.VoidResult;
@@ -69,9 +70,10 @@
 
   private Panel serverKeys;
 
+  private int loadCount;
+
   SshPanel() {
     final FlowPanel body = new FlowPanel();
-    body.add(new UsernamePanel());
 
     showAddKeyBlock = new Button(Util.C.buttonShowAddSshKey());
     showAddKeyBlock.addClickHandler(new ClickHandler() {
@@ -319,6 +321,9 @@
         if (result.isEmpty() && keys.isVisible()) {
           showAddKeyBlock(true);
         }
+        if (++loadCount == 2) {
+          display();
+        }
       }
     });
 
@@ -328,10 +333,16 @@
         for (final SshHostKey keyInfo : result) {
           serverKeys.add(new SshHostKeyPanel(keyInfo));
         }
+        if (++loadCount == 2) {
+          display();
+        }
       }
     });
   }
 
+  void display() {
+  }
+
   @Override
   protected void onUnload() {
     if (appletLoadTimer != null) {
@@ -437,7 +448,11 @@
         fmt.addStyleName(row, 2, Gerrit.RESOURCES.css().sshKeyPanelInvalid());
       }
       table.setText(row, 3, k.getAlgorithm());
-      table.setText(row, 4, elide(k.getEncodedKey(), 40));
+
+      CopyableLabel keyLabel = new CopyableLabel(k.getSshPublicKey());
+      keyLabel.setPreviewText(elide(k.getEncodedKey(), 40));
+      table.setWidget(row, 4, keyLabel);
+
       table.setText(row, 5, k.getComment());
 
       fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().iconCell());
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
new file mode 100644
index 0000000..93ccb74
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java
@@ -0,0 +1,183 @@
+// 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.account;
+
+import com.google.gerrit.client.ErrorDialog;
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.TextSaveButtonListener;
+import com.google.gerrit.common.errors.InvalidUserNameException;
+import com.google.gerrit.reviewdb.Account;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.TextBox;
+import com.google.gwtexpui.clippy.client.CopyableLabel;
+import com.google.gwtexpui.globalkey.client.NpTextBox;
+import com.google.gwtjsonrpc.client.VoidResult;
+
+class UsernameField extends Composite {
+  private CopyableLabel userNameLbl;
+  private NpTextBox userNameTxt;
+  private Button setUserName;
+
+  UsernameField() {
+    String user = Gerrit.getUserAccount().getUserName();
+    userNameLbl = new CopyableLabel(user != null ? user : "");
+    userNameLbl.setStyleName(Gerrit.RESOURCES.css().accountUsername());
+
+    if (user != null || !canEditUserName()) {
+      initWidget(userNameLbl);
+
+    } else {
+      final FlowPanel body = new FlowPanel();
+      initWidget(body);
+      setStyleName(Gerrit.RESOURCES.css().usernameField());
+
+      userNameTxt = new NpTextBox();
+      userNameTxt.addKeyPressHandler(new UserNameValidator());
+      userNameTxt.addStyleName(Gerrit.RESOURCES.css().accountUsername());
+      userNameTxt.setVisibleLength(16);
+      userNameTxt.addKeyPressHandler(new KeyPressHandler() {
+        @Override
+        public void onKeyPress(KeyPressEvent event) {
+          if (event.getCharCode() == KeyCodes.KEY_ENTER) {
+            doSetUserName();
+          }
+        }
+      });
+
+      setUserName = new Button(Util.C.buttonSetUserName());
+      setUserName.setVisible(canEditUserName());
+      setUserName.setEnabled(false);
+      setUserName.addClickHandler(new ClickHandler() {
+        @Override
+        public void onClick(final ClickEvent event) {
+          doSetUserName();
+        }
+      });
+      new TextSaveButtonListener(userNameTxt, setUserName);
+
+      userNameLbl.setVisible(false);
+      body.add(userNameLbl);
+      body.add(userNameTxt);
+      body.add(setUserName);
+    }
+  }
+
+  private boolean canEditUserName() {
+    return Gerrit.getConfig().canEdit(Account.FieldName.USER_NAME);
+  }
+
+  private void doSetUserName() {
+    if (!canEditUserName()) {
+      return;
+    }
+
+    String newName = userNameTxt.getText();
+    if ("".equals(newName)) {
+      newName = null;
+    }
+    if (newName != null && !newName.matches(Account.USER_NAME_PATTERN)) {
+      invalidUserName();
+      return;
+    }
+
+    enableUI(false);
+
+    final String newUserName = newName;
+    Util.ACCOUNT_SEC.changeUserName(newUserName,
+        new GerritCallback<VoidResult>() {
+          public void onSuccess(final VoidResult result) {
+            Gerrit.getUserAccount().setUserName(newUserName);
+            userNameLbl.setText(newUserName);
+            userNameLbl.setVisible(true);
+            userNameTxt.setVisible(false);
+            setUserName.setVisible(false);
+          }
+
+          @Override
+          public void onFailure(final Throwable caught) {
+            enableUI(true);
+            if (InvalidUserNameException.MESSAGE.equals(caught.getMessage())) {
+              invalidUserName();
+            } else {
+              super.onFailure(caught);
+            }
+          }
+        });
+  }
+
+  private void invalidUserName() {
+    new ErrorDialog(Util.C.invalidUserName()).center();
+  }
+
+  private void enableUI(final boolean on) {
+    userNameTxt.setEnabled(on);
+    setUserName.setEnabled(on);
+  }
+
+  private final class UserNameValidator implements KeyPressHandler {
+    @Override
+    public void onKeyPress(final KeyPressEvent event) {
+      final char code = event.getCharCode();
+      switch (code) {
+        case KeyCodes.KEY_ALT:
+        case KeyCodes.KEY_BACKSPACE:
+        case KeyCodes.KEY_CTRL:
+        case KeyCodes.KEY_DELETE:
+        case KeyCodes.KEY_DOWN:
+        case KeyCodes.KEY_END:
+        case KeyCodes.KEY_ENTER:
+        case KeyCodes.KEY_ESCAPE:
+        case KeyCodes.KEY_HOME:
+        case KeyCodes.KEY_LEFT:
+        case KeyCodes.KEY_PAGEDOWN:
+        case KeyCodes.KEY_PAGEUP:
+        case KeyCodes.KEY_RIGHT:
+        case KeyCodes.KEY_SHIFT:
+        case KeyCodes.KEY_TAB:
+        case KeyCodes.KEY_UP:
+          // Allow these, even if one of their assigned codes is
+          // identical to an ASCII character we do not want to
+          // allow in the box.
+          //
+          // We still want to let the user move around the input box
+          // with their arrow keys, or to move between fields using tab.
+          // Invalid characters introduced will be caught through the
+          // server's own validation of the input data.
+          //
+          break;
+
+        default:
+          final TextBox box = (TextBox) event.getSource();
+          final String re;
+          if (box.getCursorPos() == 0)
+            re = Account.USER_NAME_PATTERN_FIRST;
+          else
+            re = Account.USER_NAME_PATTERN_REST;
+          if (!String.valueOf(code).matches("^" + re + "$")) {
+            event.preventDefault();
+            event.stopPropagation();
+          }
+      }
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernamePanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernamePanel.java
deleted file mode 100644
index 2ec20d0..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernamePanel.java
+++ /dev/null
@@ -1,297 +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.account;
-
-import static com.google.gerrit.reviewdb.AccountExternalId.SCHEME_USERNAME;
-
-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.common.errors.InvalidUserNameException;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountExternalId;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.dom.client.KeyPressHandler;
-import com.google.gwt.i18n.client.LocaleInfo;
-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.TextBox;
-import com.google.gwt.user.client.ui.Widget;
-import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
-import com.google.gwtexpui.clippy.client.CopyableLabel;
-import com.google.gwtexpui.globalkey.client.NpTextBox;
-import com.google.gwtjsonrpc.client.VoidResult;
-
-import java.util.List;
-
-public class UsernamePanel extends Composite {
-  private NpTextBox userNameTxt;
-  private Button changeUserName;
-
-  private CopyableLabel password;
-  private Button generatePassword;
-
-  private AccountExternalId.Key idKey;
-
-  UsernamePanel() {
-    final FlowPanel body = new FlowPanel();
-    initWidget(body);
-
-    userNameTxt = new NpTextBox();
-    userNameTxt.addKeyPressHandler(new UserNameValidator());
-    userNameTxt.addStyleName(Gerrit.RESOURCES.css().sshPanelUsername());
-    userNameTxt.setVisibleLength(16);
-    userNameTxt.setReadOnly(!canEditUserName());
-    userNameTxt.addKeyPressHandler(new KeyPressHandler() {
-      @Override
-      public void onKeyPress(KeyPressEvent event) {
-        if (event.getCharCode() == KeyCodes.KEY_ENTER) {
-          doChangeUserName();
-        }
-      }
-    });
-
-    changeUserName = new Button(Util.C.buttonChangeUserName());
-    changeUserName.setVisible(canEditUserName());
-    changeUserName.setEnabled(false);
-    changeUserName.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(final ClickEvent event) {
-        doChangeUserName();
-      }
-    });
-    new TextSaveButtonListener(userNameTxt, changeUserName);
-
-    password = new CopyableLabel("");
-    password.addStyleName(Gerrit.RESOURCES.css().sshPanelPassword());
-    password.setVisible(false);
-
-    generatePassword = new Button(Util.C.buttonGeneratePassword());
-    generatePassword.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(ClickEvent event) {
-        doGeneratePassword();
-      }
-    });
-
-    final Grid userInfo = new Grid(2, 3);
-    final CellFormatter fmt = userInfo.getCellFormatter();
-    userInfo.setStyleName(Gerrit.RESOURCES.css().infoBlock());
-    userInfo.addStyleName(Gerrit.RESOURCES.css().accountInfoBlock());
-    body.add(userInfo);
-
-    row(userInfo, 0, Util.C.userName(), userNameTxt, changeUserName);
-    row(userInfo, 1, Util.C.password(), password, generatePassword);
-
-    fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().topmost());
-    fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().topmost());
-    fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().topmost());
-
-    fmt.addStyleName(1, 0, Gerrit.RESOURCES.css().bottomheader());
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-
-    enableUI(false);
-    Util.ACCOUNT_SEC
-        .myExternalIds(new GerritCallback<List<AccountExternalId>>() {
-          public void onSuccess(final List<AccountExternalId> result) {
-            AccountExternalId id = null;
-            for (AccountExternalId i : result) {
-              if (i.isScheme(SCHEME_USERNAME)) {
-                id = i;
-                break;
-              }
-            }
-            display(id);
-          }
-        });
-  }
-
-  private void display(AccountExternalId id) {
-    String user, pass;
-    if (id != null) {
-      idKey = id.getKey();
-      user = id.getSchemeRest();
-      pass = id.getPassword();
-    } else {
-      idKey = null;
-      user = null;
-      pass = null;
-    }
-
-    Gerrit.getUserAccount().setUserName(user);
-    userNameTxt.setText(user);
-    userNameTxt.setEnabled(true);
-    generatePassword.setEnabled(idKey != null);
-
-    if (pass != null) {
-      password.setText(pass);
-      password.setVisible(true);
-    } else {
-      password.setVisible(false);
-    }
-  }
-
-  private void row(final Grid info, final int row, final String name,
-      final Widget field1, final Widget field2) {
-    final CellFormatter fmt = info.getCellFormatter();
-    if (LocaleInfo.getCurrentLocale().isRTL()) {
-      info.setText(row, 2, name);
-      info.setWidget(row, 1, field1);
-      info.setWidget(row, 0, field2);
-      fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().noborder());
-      fmt.addStyleName(row, 2, Gerrit.RESOURCES.css().header());
-    } else {
-      info.setText(row, 0, name);
-      info.setWidget(row, 1, field1);
-      info.setWidget(row, 2, field2);
-      fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().noborder());
-      fmt.addStyleName(row, 0, Gerrit.RESOURCES.css().header());
-    }
-  }
-
-  private boolean canEditUserName() {
-    return Gerrit.getConfig().canEdit(Account.FieldName.USER_NAME);
-  }
-
-  void doChangeUserName() {
-    if (!canEditUserName()) {
-      return;
-    }
-
-    String newName = userNameTxt.getText();
-    if ("".equals(newName)) {
-      newName = null;
-    }
-    if (newName != null && !newName.matches(Account.USER_NAME_PATTERN)) {
-      invalidUserName();
-      return;
-    }
-
-    enableUI(false);
-
-    final String newUserName = newName;
-    Util.ACCOUNT_SEC.changeUserName(newUserName,
-        new GerritCallback<VoidResult>() {
-          public void onSuccess(final VoidResult result) {
-            Gerrit.getUserAccount().setUserName(newUserName);
-            enableUI(true);
-          }
-
-          @Override
-          public void onFailure(final Throwable caught) {
-            enableUI(true);
-            if (InvalidUserNameException.MESSAGE.equals(caught.getMessage())) {
-              invalidUserName();
-            } else {
-              super.onFailure(caught);
-            }
-          }
-        });
-  }
-
-  void invalidUserName() {
-    userNameTxt.setFocus(true);
-    new ErrorDialog(Util.C.invalidUserName()).center();
-  }
-
-  void doGeneratePassword() {
-    if (idKey == null) {
-      return;
-    }
-
-    enableUI(false);
-
-    Util.ACCOUNT_SEC.generatePassword(idKey,
-        new GerritCallback<AccountExternalId>() {
-          public void onSuccess(final AccountExternalId result) {
-            enableUI(true);
-            display(result);
-          }
-
-          @Override
-          public void onFailure(final Throwable caught) {
-            enableUI(true);
-            if (InvalidUserNameException.MESSAGE.equals(caught.getMessage())) {
-              invalidUserName();
-            } else {
-              super.onFailure(caught);
-            }
-          }
-        });
-  }
-
-  private void enableUI(final boolean on) {
-    userNameTxt.setEnabled(on);
-    changeUserName.setEnabled(on);
-    generatePassword.setEnabled(on && idKey != null);
-  }
-
-  private final class UserNameValidator implements KeyPressHandler {
-    @Override
-    public void onKeyPress(final KeyPressEvent event) {
-      final char code = event.getCharCode();
-      switch (code) {
-        case KeyCodes.KEY_ALT:
-        case KeyCodes.KEY_BACKSPACE:
-        case KeyCodes.KEY_CTRL:
-        case KeyCodes.KEY_DELETE:
-        case KeyCodes.KEY_DOWN:
-        case KeyCodes.KEY_END:
-        case KeyCodes.KEY_ENTER:
-        case KeyCodes.KEY_ESCAPE:
-        case KeyCodes.KEY_HOME:
-        case KeyCodes.KEY_LEFT:
-        case KeyCodes.KEY_PAGEDOWN:
-        case KeyCodes.KEY_PAGEUP:
-        case KeyCodes.KEY_RIGHT:
-        case KeyCodes.KEY_SHIFT:
-        case KeyCodes.KEY_TAB:
-        case KeyCodes.KEY_UP:
-          // Allow these, even if one of their assigned codes is
-          // identical to an ASCII character we do not want to
-          // allow in the box.
-          //
-          // We still want to let the user move around the input box
-          // with their arrow keys, or to move between fields using tab.
-          // Invalid characters introduced will be caught through the
-          // server's own validation of the input data.
-          //
-          break;
-
-        default:
-          final TextBox box = (TextBox) event.getSource();
-          final String re;
-          if (box.getCursorPos() == 0)
-            re = Account.USER_NAME_PATTERN_FIRST;
-          else
-            re = Account.USER_NAME_PATTERN_REST;
-          if (!String.valueOf(code).matches("^" + re + "$")) {
-            event.preventDefault();
-            event.stopPropagation();
-          }
-      }
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ValidateEmailScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ValidateEmailScreen.java
index 79f67f3..4e54e85 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ValidateEmailScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ValidateEmailScreen.java
@@ -30,7 +30,7 @@
   @Override
   protected void onInitUI() {
     super.onInitUI();
-    setPageTitle(Util.C.accountSettingsHeading());
+    setPageTitle(Util.C.settingsHeading());
   }
 
   @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectRightsPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java
similarity index 93%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectRightsPanel.java
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java
index 75d6931..14449f9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectRightsPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.client.ui.AccountGroupSuggestOracle;
 import com.google.gerrit.client.ui.FancyFlexTable;
 import com.google.gerrit.client.ui.Hyperlink;
@@ -43,7 +44,6 @@
 import com.google.gwt.event.dom.client.KeyPressHandler;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.CheckBox;
-import com.google.gwt.user.client.ui.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;
@@ -59,9 +59,7 @@
 import java.util.List;
 import java.util.Map;
 
-public class ProjectRightsPanel extends Composite {
-  private Project.NameKey projectName;
-
+public class ProjectAccessScreen extends ProjectScreen {
   private Panel parentPanel;
   private Hyperlink parentName;
 
@@ -74,26 +72,25 @@
   private NpTextBox nameTxtBox;
   private SuggestBox nameTxt;
   private NpTextBox referenceTxt;
+  private FlowPanel addPanel;
 
-  private final FlowPanel addPanel = new FlowPanel();
+  public ProjectAccessScreen(final Project.NameKey toShow) {
+    super(toShow);
+  }
 
-  public ProjectRightsPanel(final Project.NameKey toShow) {
-    projectName = toShow;
-
-    final FlowPanel body = new FlowPanel();
-    initParent(body);
-    initRights(body);
-    initWidget(body);
+  @Override
+  protected void onInitUI() {
+    super.onInitUI();
+    initParent();
+    initRights();
   }
 
   @Override
   protected void onLoad() {
-    enableForm(false);
     super.onLoad();
-
-    Util.PROJECT_SVC.projectDetail(projectName,
-        new GerritCallback<ProjectDetail>() {
-          public void onSuccess(final ProjectDetail result) {
+    Util.PROJECT_SVC.projectDetail(getProjectKey(),
+        new ScreenLoadCallback<ProjectDetail>(this) {
+          public void preDisplay(final ProjectDetail result) {
             enableForm(true);
             display(result);
           }
@@ -112,16 +109,17 @@
     rangeMaxBox.setEnabled(canAdd);
   }
 
-  private void initParent(final Panel body) {
+  private void initParent() {
     parentPanel = new VerticalPanel();
     parentPanel.add(new SmallHeading(Util.C.headingParentProjectName()));
 
     parentName = new Hyperlink("", "");
     parentPanel.add(parentName);
-    body.add(parentPanel);
+    add(parentPanel);
   }
 
-  private void initRights(final Panel body) {
+  private void initRights() {
+    addPanel = new FlowPanel();
     addPanel.setStyleName(Gerrit.RESOURCES.css().addSshKeyPanel());
 
     final Grid addGrid = new Grid(5, 2);
@@ -144,7 +142,7 @@
     for (final ApprovalType at : Gerrit.getConfig().getApprovalTypes()
         .getActionTypes()) {
       final ApprovalCategory c = at.getCategory();
-      if (Gerrit.getConfig().getWildProject().equals(projectName)
+      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
@@ -228,10 +226,10 @@
       }
     });
 
-    body.add(new SmallHeading(Util.C.headingAccessRights()));
-    body.add(rights);
-    body.add(delRight);
-    body.add(addPanel);
+    add(new SmallHeading(Util.C.headingAccessRights()));
+    add(rights);
+    add(delRight);
+    add(addPanel);
 
     if (catBox.getItemCount() > 0) {
       catBox.setSelectedIndex(0);
@@ -250,8 +248,7 @@
     }
 
     parentPanel.setVisible(!isWild);
-    parentName.setTargetHistoryToken(Dispatcher.toProjectAdmin(parent,
-        ProjectAdminScreen.ACCESS_TAB));
+    parentName.setTargetHistoryToken(Dispatcher.toProjectAdmin(parent, ACCESS));
     parentName.setText(parent.get());
 
     rights.display(result.groups, result.rights);
@@ -262,7 +259,7 @@
 
   private void doDeleteRefRights(final HashSet<RefRight.Key> refRightIds) {
     if (!refRightIds.isEmpty()) {
-      Util.PROJECT_SVC.deleteRight(projectName, refRightIds,
+      Util.PROJECT_SVC.deleteRight(getProjectKey(), refRightIds,
           new GerritCallback<ProjectDetail>() {
         @Override
         public void onSuccess(final ProjectDetail result) {
@@ -338,8 +335,8 @@
     }
 
     addRight.setEnabled(false);
-    Util.PROJECT_SVC.addRight(projectName, at.getCategory().getId(), groupName,
-        refPattern, min.getValue(), max.getValue(),
+    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);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAdminScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAdminScreen.java
deleted file mode 100644
index 6be8657..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAdminScreen.java
+++ /dev/null
@@ -1,119 +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.admin;
-
-import com.google.gerrit.client.Dispatcher;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.rpc.ScreenLoadCallback;
-import com.google.gerrit.client.ui.Screen;
-import com.google.gerrit.common.data.ProjectDetail;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gwt.event.logical.shared.SelectionEvent;
-import com.google.gwt.event.logical.shared.SelectionHandler;
-import com.google.gwt.user.client.ui.LazyPanel;
-import com.google.gwt.user.client.ui.TabPanel;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class ProjectAdminScreen extends Screen {
-  static final String INFO_TAB = "info";
-  static final String BRANCH_TAB = "branches";
-  static final String ACCESS_TAB = "access";
-
-  private final Project.NameKey projectName;
-  private final String initialTabToken;
-
-  private List<String> tabTokens;
-  private TabPanel tabs;
-
-  public ProjectAdminScreen(final Project.NameKey toShow, final String token) {
-    projectName = toShow;
-    initialTabToken = token;
-  }
-
-  @Override
-  public boolean displayToken(String token) {
-    final int tabIdx = tabTokens.indexOf(token);
-    if (0 <= tabIdx) {
-      tabs.selectTab(tabIdx);
-      setToken(token);
-      return true;
-    } else {
-      return false;
-    }
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    Util.PROJECT_SVC.projectDetail(projectName,
-        new ScreenLoadCallback<ProjectDetail>(this) {
-          @Override
-          protected void preDisplay(final ProjectDetail result) {
-            display(result);
-            tabs.selectTab(tabTokens.indexOf(initialTabToken));
-          }
-        });
-  }
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    tabTokens = new ArrayList<String>();
-    tabs = new TabPanel();
-    tabs.setWidth("98%");
-    add(tabs);
-
-    tabs.add(new LazyPanel() {
-      @Override
-      protected ProjectInfoPanel createWidget() {
-        return new ProjectInfoPanel(projectName);
-      }
-    }, Util.C.projectAdminTabGeneral());
-    tabTokens.add(Dispatcher.toProjectAdmin(projectName, INFO_TAB));
-
-    if (!Gerrit.getConfig().getWildProject().equals(projectName)) {
-      tabs.add(new LazyPanel() {
-        @Override
-        protected ProjectBranchesPanel createWidget() {
-          return new ProjectBranchesPanel(projectName);
-        }
-      }, Util.C.projectAdminTabBranches());
-      tabTokens.add(Dispatcher.toProjectAdmin(projectName, BRANCH_TAB));
-    }
-
-    tabs.add(new LazyPanel() {
-      @Override
-      protected ProjectRightsPanel createWidget() {
-        return new ProjectRightsPanel(projectName);
-      }
-    }, Util.C.projectAdminTabAccess());
-    tabTokens.add(Dispatcher.toProjectAdmin(projectName, ACCESS_TAB));
-
-    tabs.addSelectionHandler(new SelectionHandler<Integer>() {
-      @Override
-      public void onSelection(final SelectionEvent<Integer> event) {
-        setToken(tabTokens.get(event.getSelectedItem()));
-      }
-    });
-  }
-
-
-  private void display(final ProjectDetail result) {
-    final Project project = result.project;
-    setPageTitle(Util.M.project(project.getName()));
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
similarity index 83%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesPanel.java
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
index 090b000..8bff3e5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
@@ -14,11 +14,14 @@
 
 package com.google.gerrit.client.admin;
 
+import com.google.gerrit.client.ConfirmationCallback;
+import com.google.gerrit.client.ConfirmationDialog;
 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.common.data.ListBranchesResult;
 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;
@@ -37,10 +40,9 @@
 import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.CheckBox;
-import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Grid;
-import com.google.gwt.user.client.ui.Panel;
+import com.google.gwt.user.client.ui.HTML;
 import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
 import com.google.gwtexpui.globalkey.client.NpTextBox;
 import com.google.gwtjsonrpc.client.RemoteJsonException;
@@ -49,33 +51,24 @@
 import java.util.List;
 import java.util.Set;
 
-public class ProjectBranchesPanel extends Composite {
-  private Project.NameKey projectName;
-
+public class ProjectBranchesScreen extends ProjectScreen {
   private BranchesTable branches;
   private Button delBranch;
   private Button addBranch;
   private NpTextBox nameTxtBox;
   private NpTextBox irevTxtBox;
+  private FlowPanel addPanel;
 
-  private final FlowPanel addPanel = new FlowPanel();
-
-  public ProjectBranchesPanel(final Project.NameKey toShow) {
-    final FlowPanel body = new FlowPanel();
-    initBranches(body);
-    initWidget(body);
-
-    projectName = toShow;
+  public ProjectBranchesScreen(final Project.NameKey toShow) {
+    super(toShow);
   }
 
   @Override
   protected void onLoad() {
-    enableForm(false);
     super.onLoad();
-
-    Util.PROJECT_SVC.listBranches(projectName,
-        new GerritCallback<ListBranchesResult>() {
-          public void onSuccess(final ListBranchesResult result) {
+    Util.PROJECT_SVC.listBranches(getProjectKey(),
+        new ScreenLoadCallback<ListBranchesResult>(this) {
+          public void preDisplay(final ListBranchesResult result) {
             enableForm(true);
             display(result.getBranches());
             addPanel.setVisible(result.getCanAdd());
@@ -95,7 +88,11 @@
     irevTxtBox.setEnabled(on);
   }
 
-  private void initBranches(final Panel body) {
+  @Override
+  protected void onInitUI() {
+    super.onInitUI();
+
+    addPanel = new FlowPanel();
     addPanel.setStyleName(Gerrit.RESOURCES.css().addSshKeyPanel());
 
     final Grid addGrid = new Grid(2, 2);
@@ -186,9 +183,9 @@
       }
     });
 
-    body.add(branches);
-    body.add(delBranch);
-    body.add(addPanel);
+    add(branches);
+    add(delBranch);
+    add(addPanel);
   }
 
   private void doAddNewBranch() {
@@ -216,7 +213,7 @@
     }
 
     addBranch.setEnabled(false);
-    Util.PROJECT_SVC.addBranch(projectName, branchName, rev,
+    Util.PROJECT_SVC.addBranch(getProjectKey(), branchName, rev,
         new GerritCallback<ListBranchesResult>() {
           public void onSuccess(final ListBranchesResult result) {
             addBranch.setEnabled(true);
@@ -264,31 +261,47 @@
     }
 
     void deleteChecked() {
+      final StringBuilder message = new StringBuilder();
+      message.append("<b>").append(Gerrit.C.branchDeletionConfirmationMessage()).append("</b>");
+      message.append("<p>");
       final HashSet<Branch.NameKey> ids = new HashSet<Branch.NameKey>();
       for (int row = 1; row < table.getRowCount(); row++) {
         final Branch k = getRowItem(row);
         if (k != null && table.getWidget(row, 1) instanceof CheckBox
             && ((CheckBox) table.getWidget(row, 1)).getValue()) {
+          if (!ids.isEmpty()) {
+            message.append(", <br>");
+          }
+          message.append(k.getName());
           ids.add(k.getNameKey());
         }
       }
+      message.append("</p>");
       if (ids.isEmpty()) {
         return;
       }
 
-      Util.PROJECT_SVC.deleteBranch(projectName, ids,
-          new GerritCallback<Set<Branch.NameKey>>() {
-            public void onSuccess(final Set<Branch.NameKey> deleted) {
-              for (int row = 1; row < table.getRowCount();) {
-                final Branch k = getRowItem(row);
-                if (k != null && deleted.contains(k.getNameKey())) {
-                  table.removeRow(row);
-                } else {
-                  row++;
+      ConfirmationDialog confirmationDialog =
+          new ConfirmationDialog(Gerrit.C.branchDeletionDialogTitle(),
+              new HTML(message.toString()), new ConfirmationCallback() {
+        @Override
+        public void onOk() {
+          Util.PROJECT_SVC.deleteBranch(getProjectKey(), ids,
+              new GerritCallback<Set<Branch.NameKey>>() {
+                public void onSuccess(final Set<Branch.NameKey> deleted) {
+                  for (int row = 1; row < table.getRowCount();) {
+                    final Branch k = getRowItem(row);
+                    if (k != null && deleted.contains(k.getNameKey())) {
+                      table.removeRow(row);
+                    } else {
+                      row++;
+                    }
+                  }
                 }
-              }
-            }
-          });
+              });
+        }
+      });
+      confirmationDialog.center();
     }
 
     void display(final List<Branch> result) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
similarity index 84%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoPanel.java
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
index 49566ba..d308fa6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
@@ -16,6 +16,7 @@
 
 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.SmallHeading;
 import com.google.gerrit.client.ui.TextSaveButtonListener;
 import com.google.gerrit.common.data.ProjectDetail;
@@ -28,15 +29,12 @@
 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.Composite;
-import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.ListBox;
 import com.google.gwt.user.client.ui.Panel;
 import com.google.gwt.user.client.ui.VerticalPanel;
 import com.google.gwtexpui.globalkey.client.NpTextArea;
 
-public class ProjectInfoPanel extends Composite {
-  private Project.NameKey projectName;
+public class ProjectInfoScreen extends ProjectScreen {
   private Project project;
 
   private Panel submitTypePanel;
@@ -49,7 +47,14 @@
   private NpTextArea descTxt;
   private Button saveProject;
 
-  public ProjectInfoPanel(final Project.NameKey toShow) {
+  public ProjectInfoScreen(final Project.NameKey toShow) {
+    super(toShow);
+  }
+
+  @Override
+  protected void onInitUI() {
+    super.onInitUI();
+
     saveProject = new Button(Util.C.buttonSaveChanges());
     saveProject.addClickHandler(new ClickHandler() {
       @Override
@@ -58,28 +63,18 @@
       }
     });
 
-    final FlowPanel body = new FlowPanel();
-    initDescription(body);
-    initSubmitType(body);
-    initAgreements(body);
-    body.add(saveProject);
-
-    initWidget(body);
-    projectName = toShow;
+    initDescription();
+    initSubmitType();
+    initAgreements();
+    add(saveProject);
   }
 
   @Override
   protected void onLoad() {
-    enableForm(false, false, false);
-    saveProject.setEnabled(false);
     super.onLoad();
-    refresh();
-  }
-
-  private void refresh() {
-    Util.PROJECT_SVC.projectDetail(projectName,
-        new GerritCallback<ProjectDetail>() {
-          public void onSuccess(final ProjectDetail result) {
+    Util.PROJECT_SVC.projectDetail(getProjectKey(),
+        new ScreenLoadCallback<ProjectDetail>(this) {
+          public void preDisplay(final ProjectDetail result) {
             enableForm(result.canModifyAgreements,
                 result.canModifyDescription, result.canModifyMergeType);
             saveProject.setVisible(
@@ -102,7 +97,7 @@
         canModifyAgreements || canModifyDescription || canModifyMergeType);
   }
 
-  private void initDescription(final Panel body) {
+  private void initDescription() {
     final VerticalPanel vp = new VerticalPanel();
     vp.add(new SmallHeading(Util.C.headingDescription()));
 
@@ -111,11 +106,11 @@
     descTxt.setCharacterWidth(60);
     vp.add(descTxt);
 
-    body.add(vp);
+    add(vp);
     new TextSaveButtonListener(descTxt, saveProject);
   }
 
-  private void initSubmitType(final Panel body) {
+  private void initSubmitType() {
     submitTypePanel = new VerticalPanel();
     submitTypePanel.add(new SmallHeading(Util.C.headingSubmitType()));
 
@@ -130,10 +125,10 @@
       }
     });
     submitTypePanel.add(submitType);
-    body.add(submitTypePanel);
+    add(submitTypePanel);
   }
 
-  private void initAgreements(final Panel body) {
+  private void initAgreements() {
     final ValueChangeHandler<Boolean> onChangeSave =
         new ValueChangeHandler<Boolean>() {
           @Override
@@ -153,7 +148,7 @@
     useSignedOffBy.addValueChangeHandler(onChangeSave);
     agreementsPanel.add(useSignedOffBy);
 
-    body.add(agreementsPanel);
+    add(agreementsPanel);
   }
 
   private void setSubmitType(final Project.SubmitType newSubmitType) {
@@ -203,12 +198,6 @@
                 result.canModifyDescription, result.canModifyMergeType);
             display(result);
           }
-
-          @Override
-          public void onFailure(final Throwable caught) {
-            refresh();
-            super.onFailure(caught);
-          }
         });
   }
 }
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 642712a..e676718 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
@@ -105,7 +105,7 @@
     }
 
     private String link(final Project item) {
-      return Dispatcher.toProjectAdmin(item.getNameKey(), ProjectAdminScreen.INFO_TAB);
+      return Dispatcher.toProjectAdmin(item.getNameKey(), ProjectScreen.INFO);
     }
 
     void display(final List<Project> result) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectScreen.java
new file mode 100644
index 0000000..2df1ba3
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectScreen.java
@@ -0,0 +1,39 @@
+// Copyright 2010 Google Inc. All Rights Reserved.
+
+package com.google.gerrit.client.admin;
+
+import static com.google.gerrit.client.Dispatcher.toProjectAdmin;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.ui.MenuScreen;
+import com.google.gerrit.reviewdb.Project;
+
+public abstract class ProjectScreen extends MenuScreen {
+  public static final String INFO = "info";
+  public static final String BRANCH = "branches";
+  public static final String ACCESS = "access";
+
+  private final Project.NameKey name;
+
+  public ProjectScreen(final Project.NameKey toShow) {
+    name = toShow;
+
+    final boolean isWild = toShow.equals(Gerrit.getConfig().getWildProject());
+
+    link(Util.C.projectAdminTabGeneral(), toProjectAdmin(name, INFO));
+    if (!isWild) {
+      link(Util.C.projectAdminTabBranches(), toProjectAdmin(name, BRANCH));
+    }
+    link(Util.C.projectAdminTabAccess(), toProjectAdmin(name, ACCESS));
+  }
+
+  protected Project.NameKey getProjectKey() {
+    return name;
+  }
+
+  @Override
+  protected void onInitUI() {
+    super.onInitUI();
+    setPageTitle(Util.M.project(name.get()));
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AbandonChangeDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AbandonChangeDialog.java
index 75a0c0d..2a83ed1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AbandonChangeDialog.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AbandonChangeDialog.java
@@ -21,19 +21,26 @@
 import com.google.gerrit.reviewdb.PatchSet;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
 import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwtexpui.globalkey.client.GlobalKey;
 import com.google.gwtexpui.globalkey.client.NpTextArea;
 import com.google.gwtexpui.user.client.AutoCenterDialogBox;
 
-public class AbandonChangeDialog extends AutoCenterDialogBox {
+public class AbandonChangeDialog extends AutoCenterDialogBox implements CloseHandler<PopupPanel>{
   private final FlowPanel panel;
   private final NpTextArea message;
   private final Button sendButton;
   private final Button cancelButton;
   private final PatchSet.Id psid;
+  private final AsyncCallback<ChangeDetail> callback;
+
+  private boolean buttonClicked = false;
 
   public AbandonChangeDialog(final PatchSet.Id psi,
       final AsyncCallback<ChangeDetail> callback) {
@@ -41,6 +48,7 @@
     setGlassEnabled(true);
 
     psid = psi;
+    this.callback = callback;
     addStyleName(Gerrit.RESOURCES.css().abandonChangeDialog());
     setText(Util.C.abandonChangeTitle());
 
@@ -70,6 +78,7 @@
         Util.MANAGE_SVC.abandonChange(psid, message.getText().trim(),
             new GerritCallback<ChangeDetail>() {
               public void onSuccess(ChangeDetail result) {
+                buttonClicked = true;
                 if (callback != null) {
                   callback.onSuccess(result);
                 }
@@ -87,9 +96,11 @@
     buttonPanel.add(sendButton);
 
     cancelButton = new Button(Util.C.buttonAbandonChangeCancel());
+    DOM.setStyleAttribute(cancelButton.getElement(), "marginLeft", "300px");
     cancelButton.addClickHandler(new ClickHandler() {
       @Override
       public void onClick(final ClickEvent event) {
+        buttonClicked = true;
         if (callback != null) {
           callback.onFailure(null);
         }
@@ -97,11 +108,25 @@
       }
     });
     buttonPanel.add(cancelButton);
+
+    addCloseHandler(this);
   }
 
   @Override
-  protected void onLoad() {
-    super.onLoad();
+  public void center() {
+    super.center();
+    GlobalKey.dialog(this);
     message.setFocus(true);
   }
+
+  @Override
+  public void onClose(CloseEvent<PopupPanel> event) {
+    if (!buttonClicked) {
+      // the dialog was closed without one of the buttons being pressed
+      // e.g. the user pressed ESC to close the dialog
+      if (callback != null) {
+        callback.onFailure(null);
+      }
+    }
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AllAbandonedChangesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AllAbandonedChangesScreen.java
deleted file mode 100644
index 39b6162..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AllAbandonedChangesScreen.java
+++ /dev/null
@@ -1,44 +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.changes;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.reviewdb.Change;
-
-
-public class AllAbandonedChangesScreen extends AllSingleListScreen {
-  public AllAbandonedChangesScreen(final String positionToken) {
-    super("all,abandoned", positionToken);
-  }
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    setWindowTitle(Gerrit.C.menuAllAbandoned());
-    setPageTitle(Util.C.allAbandonedChanges());
-  }
-
-  @Override
-  protected void loadPrev() {
-    Util.LIST_SVC.allClosedPrev(Change.Status.ABANDONED, pos, pageSize,
-        loadCallback());
-  }
-
-  @Override
-  protected void loadNext() {
-    Util.LIST_SVC.allClosedNext(Change.Status.ABANDONED, pos, pageSize,
-        loadCallback());
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AllMergedChangesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AllMergedChangesScreen.java
deleted file mode 100644
index 9c4931a..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AllMergedChangesScreen.java
+++ /dev/null
@@ -1,44 +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.changes;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.reviewdb.Change;
-
-
-public class AllMergedChangesScreen extends AllSingleListScreen {
-  public AllMergedChangesScreen(final String positionToken) {
-    super("all,merged", positionToken);
-  }
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    setWindowTitle(Gerrit.C.menuAllMerged());
-    setPageTitle(Util.C.allMergedChanges());
-  }
-
-  @Override
-  protected void loadPrev() {
-    Util.LIST_SVC.allClosedPrev(Change.Status.MERGED, pos, pageSize,
-        loadCallback());
-  }
-
-  @Override
-  protected void loadNext() {
-    Util.LIST_SVC.allClosedNext(Change.Status.MERGED, pos, pageSize,
-        loadCallback());
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AllOpenChangesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AllOpenChangesScreen.java
deleted file mode 100644
index 9405066..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AllOpenChangesScreen.java
+++ /dev/null
@@ -1,42 +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.changes;
-
-import com.google.gerrit.client.Gerrit;
-
-
-
-public class AllOpenChangesScreen extends AllSingleListScreen {
-  public AllOpenChangesScreen(final String positionToken) {
-    super("all,open", positionToken);
-  }
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    setWindowTitle(Gerrit.C.menuAllOpen());
-    setPageTitle(Util.C.allOpenChanges());
-  }
-
-  @Override
-  protected void loadPrev() {
-    Util.LIST_SVC.allOpenPrev(pos, pageSize, loadCallback());
-  }
-
-  @Override
-  protected void loadNext() {
-    Util.LIST_SVC.allOpenNext(pos, pageSize, loadCallback());
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ByProjectAbandonedChangesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ByProjectAbandonedChangesScreen.java
deleted file mode 100644
index 79d00ef..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ByProjectAbandonedChangesScreen.java
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.changes;
-
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.Project;
-
-
-public class ByProjectAbandonedChangesScreen extends AllSingleListScreen {
-  private final Project.NameKey projectKey;
-
-  public ByProjectAbandonedChangesScreen(final Project.NameKey proj,
-      final String positionToken) {
-    super("project,abandoned," + proj.toString(), positionToken);
-    projectKey = proj;
-  }
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    setPageTitle(Util.M.changesAbandonedInProject(projectKey.get()));
-  }
-
-  @Override
-  protected void loadPrev() {
-    Util.LIST_SVC.byProjectClosedPrev(projectKey, Change.Status.ABANDONED, pos,
-        pageSize, loadCallback());
-  }
-
-  @Override
-  protected void loadNext() {
-    Util.LIST_SVC.byProjectClosedNext(projectKey, Change.Status.ABANDONED, pos,
-        pageSize, loadCallback());
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ByProjectMergedChangesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ByProjectMergedChangesScreen.java
deleted file mode 100644
index 55caf90..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ByProjectMergedChangesScreen.java
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.changes;
-
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.Project;
-
-
-public class ByProjectMergedChangesScreen extends AllSingleListScreen {
-  private final Project.NameKey projectKey;
-
-  public ByProjectMergedChangesScreen(final Project.NameKey proj,
-      final String positionToken) {
-    super("project,merged," + proj.toString(), positionToken);
-    projectKey = proj;
-  }
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    setPageTitle(Util.M.changesMergedInProject(projectKey.get()));
-  }
-
-  @Override
-  protected void loadPrev() {
-    Util.LIST_SVC.byProjectClosedPrev(projectKey, Change.Status.MERGED, pos,
-        pageSize, loadCallback());
-  }
-
-  @Override
-  protected void loadNext() {
-    Util.LIST_SVC.byProjectClosedNext(projectKey, Change.Status.MERGED, pos,
-        pageSize, loadCallback());
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ByProjectOpenChangesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ByProjectOpenChangesScreen.java
deleted file mode 100644
index f9525a4..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ByProjectOpenChangesScreen.java
+++ /dev/null
@@ -1,44 +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.changes;
-
-import com.google.gerrit.reviewdb.Project;
-
-
-public class ByProjectOpenChangesScreen extends AllSingleListScreen {
-  private final Project.NameKey projectKey;
-
-  public ByProjectOpenChangesScreen(final Project.NameKey proj,
-      final String positionToken) {
-    super("project,open," + proj.toString(), positionToken);
-    projectKey = proj;
-  }
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    setPageTitle(Util.M.changesOpenInProject(projectKey.get()));
-  }
-
-  @Override
-  protected void loadPrev() {
-    Util.LIST_SVC.byProjectOpenPrev(projectKey, pos, pageSize, loadCallback());
-  }
-
-  @Override
-  protected void loadNext() {
-    Util.LIST_SVC.byProjectOpenNext(projectKey, pos, pageSize, loadCallback());
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
index 4e34d25..9a08932 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
@@ -25,6 +25,7 @@
   String changesRecentlyClosed();
 
   String starredHeading();
+  String watchedHeading();
   String draftsHeading();
   String allOpenChanges();
   String allAbandonedChanges();
@@ -46,6 +47,9 @@
   String changeTablePagePrev();
   String changeTablePageNext();
   String upToDashboard();
+  String expandCollapseDependencies();
+  String previousPatchSet();
+  String nextPatchSet();
   String keyPublishComments();
 
   String patchTableColumnName();
@@ -58,7 +62,8 @@
 
   String patchTablePrev();
   String patchTableNext();
-  String patchTableOpen();
+  String patchTableOpenDiff();
+  String patchTableOpenUnifiedDiff();
   String upToChangeIconLink();
   String prevPatchLinkIcon();
   String nextPatchLinkIcon();
@@ -75,6 +80,7 @@
   String changeInfoBlockOwner();
   String changeInfoBlockProject();
   String changeInfoBlockBranch();
+  String changeInfoBlockTopic();
   String changeInfoBlockUploaded();
   String changeInfoBlockUpdated();
   String changeInfoBlockStatus();
@@ -111,4 +117,7 @@
   String reviewed();
   String submitFailed();
   String buttonClose();
+
+  String buttonDiffAllSideBySide();
+  String buttonDiffAllUnified();
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
index 7211c94..59d0bd5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
@@ -4,6 +4,7 @@
 statusLongAbandoned = Abandoned
 
 starredHeading = Starred Changes
+watchedHeading = Open Changes of Watched Projects
 draftsHeading = Changes with unpublished drafts
 changesRecentlyClosed = Recently closed
 allOpenChanges = All open changes
@@ -26,6 +27,9 @@
 changeTablePagePrev = Previous page of changes
 changeTablePageNext = Next page of changes
 upToDashboard = Up to dashboard
+expandCollapseDependencies = Expands / Collapses dependencies section
+previousPatchSet = Previous patch set
+nextPatchSet = Next patch set
 keyPublishComments = Review and publish comments
 
 patchTableColumnName = File Path
@@ -38,7 +42,8 @@
 
 patchTablePrev = Previous file
 patchTableNext = Next file
-patchTableOpen = Open file
+patchTableOpenDiff = Open diff
+patchTableOpenUnifiedDiff = Open unified diff
 
 changeScreenIncludedIn =  Included in
 changeScreenDependencies =  Dependencies
@@ -52,6 +57,7 @@
 changeInfoBlockOwner = Owner
 changeInfoBlockProject = Project
 changeInfoBlockBranch = Branch
+changeInfoBlockTopic = Topic
 changeInfoBlockUploaded = Uploaded
 changeInfoBlockUpdated = Updated
 changeInfoBlockStatus = Status
@@ -92,3 +98,6 @@
 reviewed = Reviewed
 submitFailed = Submit Failed
 buttonClose = Close
+
+buttonDiffAllSideBySide = Diff All Side-by-Side
+buttonDiffAllUnified = Diff All Unified
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDescriptionBlock.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDescriptionBlock.java
index 4ca965e..0532326 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDescriptionBlock.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDescriptionBlock.java
@@ -14,42 +14,29 @@
 
 package com.google.gerrit.client.changes;
 
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.ui.CommentLinkProcessor;
 import com.google.gerrit.common.data.AccountInfoCache;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.PatchSetInfo;
 import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.HTML;
 import com.google.gwt.user.client.ui.HorizontalPanel;
-import com.google.gwtexpui.safehtml.client.SafeHtml;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 
 public class ChangeDescriptionBlock extends Composite {
   private final ChangeInfoBlock infoBlock;
-  private final HTML description;
+  private final CommitMessageBlock messageBlock;
 
   public ChangeDescriptionBlock() {
     infoBlock = new ChangeInfoBlock();
-    description = new HTML();
-    description.setStyleName(Gerrit.RESOURCES.css().changeScreenDescription());
+    messageBlock = new CommitMessageBlock();
 
     final HorizontalPanel hp = new HorizontalPanel();
     hp.add(infoBlock);
-    hp.add(description);
+    hp.add(messageBlock);
     initWidget(hp);
   }
 
   public void display(final Change chg, final PatchSetInfo info,
       final AccountInfoCache acc) {
     infoBlock.display(chg, acc);
-
-    SafeHtml msg = new SafeHtmlBuilder().append(info.getMessage());
-    msg = msg.linkify();
-    msg = CommentLinkProcessor.apply(msg);
-    msg = new SafeHtmlBuilder().openElement("p").append(msg).closeElement("p");
-    msg = msg.replaceAll("\n\n", "</p><p>");
-    msg = msg.replaceAll("\n", "<br />");
-    SafeHtml.set(description, msg);
+    messageBlock.display(info.getMessage());
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java
index 02165f7..0c7106e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java
@@ -18,6 +18,7 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.ui.AccountDashboardLink;
+import com.google.gerrit.client.ui.BranchLink;
 import com.google.gerrit.client.ui.ChangeLink;
 import com.google.gerrit.client.ui.ProjectLink;
 import com.google.gerrit.common.data.AccountInfoCache;
@@ -34,11 +35,12 @@
   private static final int R_OWNER = 1;
   private static final int R_PROJECT = 2;
   private static final int R_BRANCH = 3;
-  private static final int R_UPLOADED = 4;
-  private static final int R_UPDATED = 5;
-  private static final int R_STATUS = 6;
-  private static final int R_PERMALINK = 7;
-  private static final int R_CNT = 8;
+  private static final int R_TOPIC = 4;
+  private static final int R_UPLOADED = 5;
+  private static final int R_UPDATED = 6;
+  private static final int R_STATUS = 7;
+  private static final int R_PERMALINK = 8;
+  private static final int R_CNT = 9;
 
   private final Grid table;
 
@@ -51,6 +53,7 @@
     initRow(R_OWNER, Util.C.changeInfoBlockOwner());
     initRow(R_PROJECT, Util.C.changeInfoBlockProject());
     initRow(R_BRANCH, Util.C.changeInfoBlockBranch());
+    initRow(R_TOPIC, Util.C.changeInfoBlockTopic());
     initRow(R_UPLOADED, Util.C.changeInfoBlockUploaded());
     initRow(R_UPDATED, Util.C.changeInfoBlockUpdated());
     initRow(R_STATUS, Util.C.changeInfoBlockStatus());
@@ -73,10 +76,18 @@
 
   public void display(final Change chg, final AccountInfoCache acc) {
     final Branch.NameKey dst = chg.getDest();
-    table.setText(R_CHANGE_ID, 1, chg.getKey().get());
+
+    CopyableLabel changeIdLabel =
+        new CopyableLabel("Change-Id: " + chg.getKey().get());
+    changeIdLabel.setPreviewText(chg.getKey().get());
+    table.setWidget(R_CHANGE_ID, 1, changeIdLabel);
+
     table.setWidget(R_OWNER, 1, AccountDashboardLink.link(acc, chg.getOwner()));
     table.setWidget(R_PROJECT, 1, new ProjectLink(chg.getProject(), chg.getStatus()));
-    table.setText(R_BRANCH, 1, dst.getShortName());
+    table.setWidget(R_BRANCH, 1, new BranchLink(dst.getShortName(), chg
+        .getProject(), chg.getStatus(), dst.get(), null));
+    table.setWidget(R_TOPIC, 1, new BranchLink(chg.getTopic(),
+        chg.getProject(), chg.getStatus(), dst.get(), chg.getTopic()));
     table.setText(R_UPLOADED, 1, mediumFormat(chg.getCreatedOn()));
     table.setText(R_UPDATED, 1, mediumFormat(chg.getLastUpdatedOn()));
     table.setText(R_STATUS, 1, Util.toLongString(chg.getStatus()));
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 a700dd8..ae2dd11 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
@@ -18,7 +18,6 @@
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.client.ui.CommentPanel;
-import com.google.gerrit.client.ui.ComplexDisclosurePanel;
 import com.google.gerrit.client.ui.ExpandAllCommand;
 import com.google.gerrit.client.ui.LinkMenuBar;
 import com.google.gerrit.client.ui.NeedsSignInKeyCommand;
@@ -28,7 +27,6 @@
 import com.google.gerrit.common.data.AccountInfoCache;
 import com.google.gerrit.common.data.ChangeDetail;
 import com.google.gerrit.common.data.ChangeInfo;
-import com.google.gerrit.common.data.GitwebLink;
 import com.google.gerrit.common.data.ToggleStarRequest;
 import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.reviewdb.Change;
@@ -40,11 +38,9 @@
 import com.google.gwt.event.dom.client.KeyPressEvent;
 import com.google.gwt.event.shared.HandlerRegistration;
 import com.google.gwt.i18n.client.LocaleInfo;
-import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.DisclosurePanel;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Image;
-import com.google.gwt.user.client.ui.InlineLabel;
 import com.google.gwt.user.client.ui.Label;
 import com.google.gwt.user.client.ui.Panel;
 import com.google.gwtexpui.globalkey.client.GlobalKey;
@@ -61,7 +57,6 @@
 
   private Image starChange;
   private boolean starred;
-  private PatchSet.Id currentPatchSet;
   private ChangeDescriptionBlock descriptionBlock;
   private ApprovalTable approvals;
 
@@ -72,7 +67,7 @@
   private ChangeTable.Section dependsOn;
   private ChangeTable.Section neededBy;
 
-  private FlowPanel patchSetPanels;
+  private PatchSetsBlock patchSetsBlock;
 
   private Panel comments;
 
@@ -121,6 +116,7 @@
     super.registerKeys();
     regNavigation = GlobalKey.add(this, keysNavigation);
     regAction = GlobalKey.add(this, keysAction);
+    patchSetsBlock.setRegisterKeys(true);
   }
 
   public void refresh() {
@@ -150,6 +146,7 @@
     keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation());
     keysAction = new KeyCommandSet(Gerrit.C.sectionActions());
     keysNavigation.add(new DashboardKeyCommand(0, 'u', Util.C.upToDashboard()));
+    keysNavigation.add(new ExpandCollapseDependencySectionKeyCommand(0, 'd', Util.C.expandCollapseDependencies()));
 
     if (Gerrit.isSignedIn()) {
       keysAction.add(new StarKeyCommand(0, 's', Util.C.changeTableStar()));
@@ -195,8 +192,8 @@
     dependenciesPanel.setWidth("95%");
     add(dependenciesPanel);
 
-    patchSetPanels = new FlowPanel();
-    add(patchSetPanels);
+    patchSetsBlock = new PatchSetsBlock(this);
+    add(patchSetsBlock);
 
     comments = new FlowPanel();
     comments.setStyleName(Gerrit.RESOURCES.css().changeComments());
@@ -245,7 +242,7 @@
     approvals.display(detail.getChange(), detail.getMissingApprovals(), detail
         .getApprovals());
 
-    addPatchSets(detail);
+    patchSetsBlock.display(detail);
     addComments(detail);
 
     // If any dependency change is still open, show our dependency list.
@@ -264,40 +261,6 @@
     dependenciesPanel.setOpen(depsOpen);
   }
 
-  private void addPatchSets(final ChangeDetail detail) {
-    patchSetPanels.clear();
-
-    final PatchSet currps = detail.getCurrentPatchSet();
-    final GitwebLink gw = Gerrit.getConfig().getGitwebLink();
-    for (final PatchSet ps : detail.getPatchSets()) {
-      final ComplexDisclosurePanel panel =
-          new ComplexDisclosurePanel(Util.M.patchSetHeader(ps.getPatchSetId()),
-              ps == currps);
-      final PatchSetPanel psp = new PatchSetPanel(this, detail, ps);
-      panel.setContent(psp);
-
-      final InlineLabel revtxt = new InlineLabel(ps.getRevision().get() + " ");
-      revtxt.addStyleName(Gerrit.RESOURCES.css().patchSetRevision());
-      panel.getHeader().add(revtxt);
-      if (gw != null) {
-        final Anchor revlink =
-            new Anchor("(gitweb)", false, gw.toRevision(detail.getChange()
-                .getProject(), ps));
-        revlink.addStyleName(Gerrit.RESOURCES.css().patchSetLink());
-        panel.getHeader().add(revlink);
-      }
-
-      if (ps == currps) {
-        psp.ensureLoaded(detail.getCurrentPatchSetDetail());
-      } else {
-        panel.addOpenHandler(psp);
-      }
-      add(panel);
-      patchSetPanels.add(panel);
-    }
-    currentPatchSet = currps.getId();
-  }
-
   private void addComments(final ChangeDetail detail) {
     comments.clear();
 
@@ -398,6 +361,17 @@
     }
   }
 
+  public class ExpandCollapseDependencySectionKeyCommand extends KeyCommand {
+    public ExpandCollapseDependencySectionKeyCommand(int mask, char key, String help) {
+      super(mask, key, help);
+    }
+
+    @Override
+    public void onKeyPress(KeyPressEvent event) {
+      dependenciesPanel.setOpen(!dependenciesPanel.isOpen());
+    }
+  }
+
   public class StarKeyCommand extends NeedsSignInKeyCommand {
     public StarKeyCommand(int mask, char key, String help) {
       super(mask, key, help);
@@ -416,8 +390,9 @@
 
     @Override
     public void onKeyPress(final KeyPressEvent event) {
-      Gerrit.display("change,publish," + currentPatchSet.toString(),
-          new PublishCommentScreen(currentPatchSet));
+      PatchSet.Id currentPatchSetId = patchSetsBlock.getCurrentPatchSet().getId();
+      Gerrit.display("change,publish," + currentPatchSetId.toString(),
+          new PublishCommentScreen(currentPatchSetId));
     }
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
index ed45a64..76b9f49 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.client.patches.PatchUtil;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.AccountDashboardLink;
+import com.google.gerrit.client.ui.BranchLink;
 import com.google.gerrit.client.ui.ChangeLink;
 import com.google.gerrit.client.ui.NavigationTable;
 import com.google.gerrit.client.ui.NeedsSignInKeyCommand;
@@ -226,7 +227,8 @@
     table.setWidget(row, C_OWNER, link(c.getOwner()));
     table.setWidget(row, C_PROJECT, new ProjectLink(c.getProject().getKey(), c
         .getStatus()));
-    table.setText(row, C_BRANCH, c.getBranch());
+    table.setWidget(row, C_BRANCH, new BranchLink(c.getProject().getKey(), c
+        .getStatus(), c.getBranch(), c.getTopic()));
     table.setText(row, C_LAST_UPDATE, shortFormat(c.getLastUpdatedOn()));
     setRowItem(row, c);
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.java
new file mode 100644
index 0000000..48e257d
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.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.client.changes;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.ui.CommentLinkProcessor;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.ScrollPanel;
+import com.google.gwtexpui.safehtml.client.SafeHtml;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+
+public class CommitMessageBlock extends Composite {
+  private final HTML description;
+
+  public CommitMessageBlock() {
+    this(null);
+  }
+
+  public CommitMessageBlock(String height) {
+    description = new HTML();
+    description.setStyleName(Gerrit.RESOURCES.css().changeScreenDescription());
+    if (height != null) {
+      ScrollPanel scrollPanel = new ScrollPanel();
+      scrollPanel.setHeight(height);
+      scrollPanel.add(description);
+      initWidget(scrollPanel);
+    } else {
+      initWidget(description);
+    }
+  }
+
+  public void display(final String commitMessage) {
+    SafeHtml msg = new SafeHtmlBuilder().append(commitMessage);
+    msg = msg.linkify();
+    msg = CommentLinkProcessor.apply(msg);
+    msg = new SafeHtmlBuilder().openElement("p").append(msg).closeElement("p");
+    msg = msg.replaceAll("\n\n", "</p><p>");
+    msg = msg.replaceAll("\n", "<br />");
+    SafeHtml.set(description, msg);
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/MineDraftsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/MineDraftsScreen.java
deleted file mode 100644
index 2b935ba..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/MineDraftsScreen.java
+++ /dev/null
@@ -1,38 +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.changes;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.common.PageLinks;
-
-
-public class MineDraftsScreen extends MineSingleListScreen {
-  public MineDraftsScreen() {
-    super(PageLinks.MINE_DRAFTS);
-  }
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    setWindowTitle(Gerrit.C.menyMyDrafts());
-    setPageTitle(Util.C.draftsHeading());
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    Util.LIST_SVC.myDraftChanges(loadCallback());
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/MineStarredScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/MineStarredScreen.java
deleted file mode 100644
index df4d30d..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/MineStarredScreen.java
+++ /dev/null
@@ -1,38 +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.changes;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.common.PageLinks;
-
-
-public class MineStarredScreen extends MineSingleListScreen {
-  public MineStarredScreen() {
-    super(PageLinks.MINE_STARRED);
-  }
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    setWindowTitle(Gerrit.C.menuMyStarredChanges());
-    setPageTitle(Util.C.starredHeading());
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    Util.LIST_SVC.myStarredChanges(loadCallback());
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AllSingleListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java
similarity index 97%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AllSingleListScreen.java
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java
index 941d762..6ca102f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AllSingleListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java
@@ -31,7 +31,7 @@
 import java.util.List;
 
 
-public abstract class AllSingleListScreen extends Screen {
+public abstract class PagedSingleListScreen extends Screen {
   protected static final String MIN_SORTKEY = "";
   protected static final String MAX_SORTKEY = "z";
 
@@ -46,7 +46,7 @@
   protected boolean useLoadPrev;
   protected String pos;
 
-  protected AllSingleListScreen(final String anchorToken,
+  protected PagedSingleListScreen(final String anchorToken,
       final String positionToken) {
     anchorPrefix = anchorToken;
     useLoadPrev = positionToken.startsWith("p,");
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java
similarity index 78%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetPanel.java
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java
index 56b440e..da4b78b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java
@@ -18,7 +18,12 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.AccountDashboardLink;
+import com.google.gerrit.client.ui.ComplexDisclosurePanel;
+import com.google.gerrit.client.ui.PatchLink;
+import com.google.gerrit.client.ui.PatchLink.SideBySide;
+import com.google.gerrit.client.ui.PatchLink.Unified;
 import com.google.gerrit.common.data.ChangeDetail;
+import com.google.gerrit.common.data.GitwebLink;
 import com.google.gerrit.common.data.PatchSetDetail;
 import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.reviewdb.AccountGeneralPreferences;
@@ -26,6 +31,7 @@
 import com.google.gerrit.reviewdb.Branch;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.ChangeMessage;
+import com.google.gerrit.reviewdb.Patch;
 import com.google.gerrit.reviewdb.PatchSet;
 import com.google.gerrit.reviewdb.PatchSetInfo;
 import com.google.gerrit.reviewdb.Project;
@@ -39,8 +45,8 @@
 import com.google.gwt.event.logical.shared.OpenHandler;
 import com.google.gwt.user.client.Window;
 import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.DisclosurePanel;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Grid;
@@ -50,9 +56,10 @@
 import com.google.gwtexpui.clippy.client.CopyableLabel;
 
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.Set;
 
-class PatchSetPanel extends Composite implements OpenHandler<DisclosurePanel> {
+class PatchSetComplexDisclosurePanel extends ComplexDisclosurePanel implements OpenHandler<DisclosurePanel> {
   private static final int R_AUTHOR = 0;
   private static final int R_COMMITTER = 1;
   private static final int R_DOWNLOAD = 2;
@@ -66,14 +73,48 @@
   private Grid infoTable;
   private Panel actionsPanel;
   private PatchTable patchTable;
+  private final Set<ClickHandler> registeredClickHandler =  new HashSet<ClickHandler>();
 
-  PatchSetPanel(final ChangeScreen parent, final ChangeDetail detail,
+  /**
+   * Creates a closed complex disclosure panel for a patch set.
+   * The patch set details are loaded when the complex disclosure panel is opened.
+   */
+  PatchSetComplexDisclosurePanel(final ChangeScreen parent, final ChangeDetail detail,
       final PatchSet ps) {
+    this(parent, detail, ps, false);
+    addOpenHandler(this);
+  }
+
+  /**
+   * Creates an open complex disclosure panel for a patch set.
+   */
+  PatchSetComplexDisclosurePanel(final ChangeScreen parent, final ChangeDetail detail,
+      final PatchSetDetail psd) {
+    this(parent, detail, psd.getPatchSet(), true);
+    ensureLoaded(psd);
+  }
+
+  private PatchSetComplexDisclosurePanel(final ChangeScreen parent, final ChangeDetail detail,
+      final PatchSet ps, boolean isOpen) {
+    super(Util.M.patchSetHeader(ps.getPatchSetId()), isOpen);
     changeScreen = parent;
     changeDetail = detail;
     patchSet = ps;
     body = new FlowPanel();
-    initWidget(body);
+    setContent(body);
+
+    final GitwebLink gw = Gerrit.getConfig().getGitwebLink();
+
+    final InlineLabel revtxt = new InlineLabel(ps.getRevision().get() + " ");
+    revtxt.addStyleName(Gerrit.RESOURCES.css().patchSetRevision());
+    getHeader().add(revtxt);
+    if (gw != null) {
+      final Anchor revlink =
+          new Anchor("(gitweb)", false, gw.toRevision(detail.getChange()
+              .getProject(), ps));
+      revlink.addStyleName(Gerrit.RESOURCES.css().patchSetLink());
+      getHeader().add(revlink);
+    }
   }
 
   /**
@@ -106,7 +147,7 @@
 
     patchTable = new PatchTable();
     patchTable.setSavePointerId("PatchTable " + patchSet.getId());
-    patchTable.display(info.getKey(), detail.getPatches());
+    patchTable.display(detail);
 
     body.add(infoTable);
 
@@ -119,7 +160,12 @@
         populateActions(detail);
       }
     }
+    populateDiffAllActions(detail);
     body.add(patchTable);
+
+    for(ClickHandler clickHandler : registeredClickHandler) {
+      patchTable.addClickHandler(clickHandler);
+    }
   }
 
   private void displayDownload() {
@@ -366,6 +412,36 @@
     }
   }
 
+  private void populateDiffAllActions(final PatchSetDetail detail) {
+    final Button diffAllSideBySide = new Button(Util.C.buttonDiffAllSideBySide());
+    diffAllSideBySide.addClickHandler(new ClickHandler() {
+
+      @Override
+      public void onClick(ClickEvent event) {
+        for (Patch p : detail.getPatches()) {
+          SideBySide link = new PatchLink.SideBySide(p.getFileName(), p.getKey(), 0, null, null);
+          Window.open(Window.Location.getPath() + "#"
+              + link.getTargetHistoryToken(), "_blank", null);
+        }
+      }
+    });
+    actionsPanel.add(diffAllSideBySide);
+
+    final Button diffAllUnified = new Button(Util.C.buttonDiffAllUnified());
+    diffAllUnified.addClickHandler(new ClickHandler() {
+
+      @Override
+      public void onClick(ClickEvent event) {
+        for (Patch p : detail.getPatches()) {
+          Unified link = new PatchLink.Unified(p.getFileName(), p.getKey(), 0, null, null);
+          Window.open(Window.Location.getPath() + "#"
+              + link.getTargetHistoryToken(), "_blank", null);
+        }
+      }
+    });
+    actionsPanel.add(diffAllUnified);
+  }
+
   private void populateReviewAction() {
     final Button b = new Button(Util.C.buttonReview());
     b.addClickHandler(new ClickHandler() {
@@ -385,6 +461,7 @@
           new GerritCallback<PatchSetDetail>() {
             public void onSuccess(final PatchSetDetail result) {
               ensureLoaded(result);
+              patchTable.setRegisterKeys(true);
             }
           });
     }
@@ -417,4 +494,27 @@
     }
     changeScreen.display(result);
   }
+
+  public PatchSet getPatchSet() {
+    return patchSet;
+  }
+
+  /**
+   * Adds a click handler to the patch table.
+   * If the patch table is not yet initialized it is guaranteed that the click handler
+   * is added to the patch table after initialization.
+   */
+  public void addClickHandler(final ClickHandler clickHandler) {
+    registeredClickHandler.add(clickHandler);
+    if (patchTable != null) {
+      patchTable.addClickHandler(clickHandler);
+    }
+  }
+
+  /** Activates / Deactivates the key navigation and the highlighting of the current row for the patch table */
+  public void setActive(boolean active) {
+    if (patchTable != null) {
+      patchTable.setActive(active);
+    }
+  }
 }
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
new file mode 100644
index 0000000..b0f06a4
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetsBlock.java
@@ -0,0 +1,235 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.changes;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.common.data.ChangeDetail;
+import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.logical.shared.OpenEvent;
+import com.google.gwt.event.logical.shared.OpenHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.DisclosurePanel;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwtexpui.globalkey.client.GlobalKey;
+import com.google.gwtexpui.globalkey.client.KeyCommand;
+import com.google.gwtexpui.globalkey.client.KeyCommandSet;
+
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Composite that displays the patch sets of a change. This composite ensures
+ * that keyboard navigation to each changed file in all patch sets is possible.
+ */
+public class PatchSetsBlock extends Composite {
+
+  private final HashMap<PatchSet.Id, PatchSetComplexDisclosurePanel> patchSetPanels =
+      new HashMap<PatchSet.Id, PatchSetComplexDisclosurePanel>();
+
+  private final ChangeScreen parent;
+  private final FlowPanel body;
+  private HandlerRegistration regNavigation;
+
+  /**
+   * the patch set id of the patch set for which is the keyboard navigation is
+   * currently enabled
+   */
+  private PatchSet.Id activePatchSetId;
+
+  /** the patch set id of the current (latest) patch set */
+  private PatchSet.Id currentPatchSetId;
+
+  /** Patch sets on this change, in order. */
+  private List<PatchSet> patchSets;
+
+  PatchSetsBlock(final ChangeScreen parent) {
+    this.parent = parent;
+    body = new FlowPanel();
+    initWidget(body);
+  }
+
+  /** Adds UI elements for each patch set of the given change to this composite. */
+  public void display(final ChangeDetail detail) {
+    clear();
+
+    final PatchSet currps = detail.getCurrentPatchSet();
+    currentPatchSetId = currps.getId();
+    patchSets = detail.getPatchSets();
+
+    for (final PatchSet ps : patchSets) {
+      if (ps == currps) {
+        add(new PatchSetComplexDisclosurePanel(parent, detail, detail
+            .getCurrentPatchSetDetail()));
+      } else {
+        add(new PatchSetComplexDisclosurePanel(parent, detail, ps));
+      }
+    }
+  }
+
+  private void clear() {
+    setRegisterKeys(false);
+    body.clear();
+    patchSetPanels.clear();
+  }
+
+  /**
+   * Adds the given patch set panel to this composite and ensures that handler
+   * to activate / deactivate keyboard navigation for the patch set panel are
+   * registered.
+   */
+  private void add(final PatchSetComplexDisclosurePanel patchSetPanel) {
+    body.add(patchSetPanel);
+
+    final PatchSet.Id id = patchSetPanel.getPatchSet().getId();
+    ActivationHandler activationHandler = new ActivationHandler(id);
+    patchSetPanel.addOpenHandler(activationHandler);
+    patchSetPanel.addClickHandler(activationHandler);
+    patchSetPanels.put(id, patchSetPanel);
+  }
+
+  public void setRegisterKeys(final boolean on) {
+    if (on) {
+      KeyCommandSet keysNavigation =
+          new KeyCommandSet(Gerrit.C.sectionNavigation());
+      keysNavigation.add(new PreviousPatchSetKeyCommand(0, 'p', Util.C
+          .previousPatchSet()));
+      keysNavigation.add(new NextPatchSetKeyCommand(0, 'n', Util.C
+          .nextPatchSet()));
+      regNavigation = GlobalKey.add(this, keysNavigation);
+      if (activePatchSetId != null) {
+        activate(activePatchSetId);
+      } else {
+        activate(currentPatchSetId);
+      }
+    } else {
+      if (regNavigation != null) {
+        regNavigation.removeHandler();
+        regNavigation = null;
+      }
+      deactivate();
+    }
+  }
+
+  @Override
+  protected void onUnload() {
+    setRegisterKeys(false);
+    super.onUnload();
+  }
+
+  /**
+   * Activates keyboard navigation for the patch set panel that displays the
+   * patch set with the given patch set id.
+   * The keyboard navigation for the previously active patch set panel is
+   * automatically deactivated.
+   * This method also ensures that the current row is only highlighted in the
+   * table of the active patch set panel.
+   */
+  private void activate(final PatchSet.Id patchSetId) {
+    if (!patchSetId.equals(activePatchSetId)) {
+      deactivate();
+      PatchSetComplexDisclosurePanel patchSetPanel =
+          patchSetPanels.get(patchSetId);
+      patchSetPanel.setOpen(true);
+      patchSetPanel.setActive(true);
+      activePatchSetId = patchSetId;
+    }
+  }
+
+  /** Deactivates the keyboard navigation for the currently active patch set panel. */
+  private void deactivate() {
+    if (activePatchSetId != null) {
+      PatchSetComplexDisclosurePanel patchSetPanel =
+          patchSetPanels.get(activePatchSetId);
+      patchSetPanel.setActive(false);
+      activePatchSetId = null;
+    }
+  }
+
+  public PatchSet getCurrentPatchSet() {
+    PatchSetComplexDisclosurePanel patchSetPanel =
+        patchSetPanels.get(currentPatchSetId);
+    if (patchSetPanel != null) {
+      return patchSetPanel.getPatchSet();
+    } else {
+      return null;
+    }
+  }
+
+  private int indexOf(PatchSet.Id id) {
+    for (int i = 0; i < patchSets.size(); i++) {
+      if (patchSets.get(i).getId().equals(id)) {
+        return i;
+      }
+    }
+    return -1;
+  }
+
+  private class ActivationHandler implements OpenHandler<DisclosurePanel>,
+      ClickHandler {
+
+    private final PatchSet.Id patchSetId;
+
+    ActivationHandler(PatchSet.Id patchSetId) {
+      this.patchSetId = patchSetId;
+    }
+
+    @Override
+    public void onOpen(OpenEvent<DisclosurePanel> event) {
+      // when a patch set panel is opened by the user
+      // it should automatically become active
+      activate(patchSetId);
+    }
+
+    @Override
+    public void onClick(ClickEvent event) {
+      // when a user clicks on a patch table the corresponding
+      // patch set panel should automatically become active
+      activate(patchSetId);
+    }
+
+  }
+
+  public class PreviousPatchSetKeyCommand extends KeyCommand {
+    public PreviousPatchSetKeyCommand(int mask, char key, String help) {
+      super(mask, key, help);
+    }
+
+    @Override
+    public void onKeyPress(final KeyPressEvent event) {
+      int index = indexOf(activePatchSetId) - 1;
+      if (0 <= index) {
+        activate(patchSets.get(index).getId());
+      }
+    }
+  }
+
+  public class NextPatchSetKeyCommand extends KeyCommand {
+    public NextPatchSetKeyCommand(int mask, char key, String help) {
+      super(mask, key, help);
+    }
+
+    @Override
+    public void onKeyPress(final KeyPressEvent event) {
+      int index = indexOf(activePatchSetId) + 1;
+      if (index < patchSets.size()) {
+        activate(patchSets.get(index).getId());
+      }
+    }
+  }
+}
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 bc14bdb..f448b67 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
@@ -19,13 +19,14 @@
 import com.google.gerrit.client.ui.InlineHyperlink;
 import com.google.gerrit.client.ui.NavigationTable;
 import com.google.gerrit.client.ui.PatchLink;
+import com.google.gerrit.common.data.PatchSetDetail;
 import com.google.gerrit.reviewdb.Patch;
-import com.google.gerrit.reviewdb.PatchSet;
 import com.google.gerrit.reviewdb.Patch.Key;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyPressEvent;
 import com.google.gwt.user.client.Command;
 import com.google.gwt.user.client.DeferredCommand;
 import com.google.gwt.user.client.IncrementalCommand;
@@ -35,32 +36,47 @@
 import com.google.gwt.user.client.ui.Label;
 import com.google.gwt.user.client.ui.Widget;
 import com.google.gwt.user.client.ui.HTMLTable.Cell;
+import com.google.gwtexpui.globalkey.client.KeyCommand;
 import com.google.gwtexpui.progress.client.ProgressBar;
 import com.google.gwtexpui.safehtml.client.SafeHtml;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 import com.google.gwtorm.client.KeyUtil;
 
+import java.util.ArrayList;
 import java.util.List;
 
 public class PatchTable extends Composite {
   private final FlowPanel myBody;
-  private PatchSet.Id psid;
+  private PatchSetDetail detail;
   private Command onLoadCommand;
   private MyTable myTable;
   private String savePointerId;
   private List<Patch> patchList;
 
+  private List<ClickHandler> clickHandlers;
+  private boolean active;
+  private boolean registerKeys;
+
   public PatchTable() {
     myBody = new FlowPanel();
     initWidget(myBody);
   }
 
-  public void display(final PatchSet.Id id, final List<Patch> list) {
-    psid = id;
-    myTable = null;
-    patchList = list;
+  public int indexOf(Patch.Key patch) {
+    for (int i = 0; i < patchList.size(); i++) {
+      if (patchList.get(i).getKey().equals(patch)) {
+        return i;
+      }
+    }
+    return -1;
+  }
 
-    final DisplayCommand cmd = new DisplayCommand(list);
+  public void display(PatchSetDetail detail) {
+    this.detail = detail;
+    this.patchList = detail.getPatches();
+    myTable = null;
+
+    final DisplayCommand cmd = new DisplayCommand(patchList);
     if (cmd.execute()) {
       cmd.initMeter();
       DeferredCommand.addCommand(cmd);
@@ -85,20 +101,66 @@
     }
   }
 
+  public void addClickHandler(final ClickHandler clickHandler) {
+    if (myTable != null) {
+      myTable.addClickHandler(clickHandler);
+    } else {
+      if (clickHandlers == null) {
+        clickHandlers = new ArrayList<ClickHandler>(2);
+      }
+      clickHandlers.add(clickHandler);
+    }
+  }
+
   public void setRegisterKeys(final boolean on) {
-    myTable.setRegisterKeys(on);
+    registerKeys = on;
+    if (myTable != null) {
+      myTable.setRegisterKeys(on);
+    }
   }
 
   public void movePointerTo(final Patch.Key k) {
     myTable.movePointerTo(k);
   }
 
+  public void setActive(boolean active) {
+    this.active = active;
+    if (myTable != null) {
+      myTable.setActive(active);
+    }
+  }
+
   public void notifyDraftDelta(final Patch.Key k, final int delta) {
     if (myTable != null) {
       myTable.notifyDraftDelta(k, delta);
     }
   }
 
+  private void setMyTable(MyTable table) {
+    myBody.clear();
+    myBody.add(table);
+    myTable = table;
+
+    if (clickHandlers != null) {
+      for (ClickHandler ch : clickHandlers) {
+        myTable.addClickHandler(ch);
+      }
+      clickHandlers = null;
+    }
+
+    if (active) {
+      myTable.setActive(true);
+      active = false;
+    }
+
+    if (registerKeys) {
+      myTable.setRegisterKeys(registerKeys);
+      registerKeys = false;
+    }
+
+    myTable.finishDisplay();
+  }
+
   /**
    * @return a link to the previous file in this patch set, or null.
    */
@@ -133,9 +195,9 @@
     PatchLink link;
     if (patchType == PatchScreen.Type.SIDE_BY_SIDE
         && patch.getPatchType() == Patch.PatchType.UNIFIED) {
-      link = new PatchLink.SideBySide("", thisKey, index, this);
+      link = new PatchLink.SideBySide("", thisKey, index, detail, this);
     } else {
-      link = new PatchLink.Unified("", thisKey, index, this);
+      link = new PatchLink.Unified("", thisKey, index, detail, this);
     }
     SafeHtmlBuilder text = new SafeHtmlBuilder();
     text.append(before);
@@ -170,13 +232,16 @@
     private static final int C_PATH = 2;
     private static final int C_DRAFT = 3;
     private static final int C_SIDEBYSIDE = 4;
+    private int activeRow = -1;
 
     MyTable() {
       keysNavigation.add(new PrevKeyCommand(0, 'k', Util.C.patchTablePrev()));
       keysNavigation.add(new NextKeyCommand(0, 'j', Util.C.patchTableNext()));
-      keysNavigation.add(new OpenKeyCommand(0, 'o', Util.C.patchTableOpen()));
+      keysNavigation.add(new OpenKeyCommand(0, 'o', Util.C.patchTableOpenDiff()));
       keysNavigation.add(new OpenKeyCommand(0, KeyCodes.KEY_ENTER, Util.C
-          .patchTableOpen()));
+          .patchTableOpenDiff()));
+      keysNavigation.add(new OpenUnifiedDiffKeyCommand(0, 'O', Util.C
+          .patchTableOpenUnifiedDiff()));
 
       table.addClickHandler(new ClickHandler() {
         @Override
@@ -190,6 +255,10 @@
       setSavePointerId(PatchTable.this.savePointerId);
     }
 
+    public void addClickHandler(final ClickHandler clickHandler) {
+      table.addClickHandler(clickHandler);
+    }
+
     void updateReviewedStatus(final Patch.Key patchKey, boolean reviewed) {
       final int row = findRow(patchKey);
       if (0 <= row) {
@@ -234,17 +303,35 @@
       super.movePointerTo(oldId);
     }
 
+    /** Activates / Deactivates the key navigation and the highlighting of the current row for this table */
+    public void setActive(boolean active) {
+      if (active) {
+        if(activeRow > 0 && getCurrentRow() != activeRow) {
+          super.movePointerTo(activeRow);
+          activeRow = -1;
+        }
+      } else {
+        if(getCurrentRow() > 0) {
+          activeRow = getCurrentRow();
+          super.movePointerTo(-1);
+        }
+      }
+      setRegisterKeys(active);
+    }
+
     void initializeRow(int row) {
       Patch patch = PatchTable.this.patchList.get(row - 1);
       setRowItem(row, patch);
 
       Widget nameCol;
       if (patch.getPatchType() == Patch.PatchType.UNIFIED) {
-        nameCol = new PatchLink.SideBySide(patch.getFileName(), patch.getKey(), row - 1,
-            PatchTable.this);
+        nameCol =
+            new PatchLink.SideBySide(patch.getFileName(), patch.getKey(),
+                row - 1, detail, PatchTable.this);
       } else {
-        nameCol = new PatchLink.Unified(patch.getFileName(), patch.getKey(), row - 1,
-            PatchTable.this);
+        nameCol =
+            new PatchLink.Unified(patch.getFileName(), patch.getKey(), row - 1,
+                detail, PatchTable.this);
       }
       if (patch.getSourceFileName() != null) {
         final String text;
@@ -266,16 +353,16 @@
 
       int C_UNIFIED = C_SIDEBYSIDE + 1;
       if (patch.getPatchType() == Patch.PatchType.UNIFIED) {
-        table.setWidget(row, C_SIDEBYSIDE,
-            new PatchLink.SideBySide(Util.C.patchTableDiffSideBySide(), patch.getKey(), row - 1,
-                PatchTable.this));
+        table.setWidget(row, C_SIDEBYSIDE, new PatchLink.SideBySide(Util.C
+            .patchTableDiffSideBySide(), patch.getKey(), row - 1, detail,
+            PatchTable.this));
 
       } else if (patch.getPatchType() == Patch.PatchType.BINARY) {
         C_UNIFIED = C_SIDEBYSIDE + 2;
       }
-      table.setWidget(row, C_UNIFIED,
-          new PatchLink.Unified(Util.C.patchTableDiffUnified(), patch.getKey(), row - 1,
-              PatchTable.this));
+      table.setWidget(row, C_UNIFIED, new PatchLink.Unified(Util.C
+          .patchTableDiffUnified(), patch.getKey(), row - 1, detail,
+          PatchTable.this));
     }
 
     void appendHeader(final SafeHtmlBuilder m) {
@@ -462,6 +549,29 @@
         ((InlineHyperlink) link).go();
       }
     }
+
+    private final class OpenUnifiedDiffKeyCommand extends KeyCommand {
+
+      public OpenUnifiedDiffKeyCommand(int mask, char key, String help) {
+        super(mask, key, help);
+      }
+
+      @Override
+      public void onKeyPress(KeyPressEvent event) {
+        Widget link = table.getWidget(getCurrentRow(), C_PATH);
+        if (link instanceof FlowPanel) {
+          link = ((FlowPanel) link).getWidget(0);
+        }
+        if (link instanceof PatchLink.Unified) {
+          ((InlineHyperlink) link).go();
+        } else {
+          link = table.getWidget(getCurrentRow(), C_SIDEBYSIDE + 1);
+          if (link instanceof PatchLink.Unified) {
+            ((InlineHyperlink) link).go();
+          }
+        }
+      }
+    }
   }
 
   private final class DisplayCommand implements IncrementalCommand {
@@ -530,10 +640,8 @@
     }
 
     void showTable() {
-      PatchTable.this.myBody.clear();
-      PatchTable.this.myBody.add(table);
-      PatchTable.this.myTable = table;
-      table.finishDisplay();
+      setMyTable(table);
+
       if (PatchTable.this.onLoadCommand != null) {
         PatchTable.this.onLoadCommand.execute();
         PatchTable.this.onLoadCommand = null;
@@ -542,7 +650,7 @@
 
     void initMeter() {
       if (meter == null) {
-        meter = new ProgressBar(Util.M.loadingPatchSet(psid.get()));
+        meter = new ProgressBar(Util.M.loadingPatchSet(detail.getPatchSet().getId().get()));
         PatchTable.this.myBody.clear();
         PatchTable.this.myBody.add(meter);
       }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java
index d0d148e..0335178 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java
@@ -281,10 +281,7 @@
           draftsPanel.add(panel);
           // Parent table can be null here since we are not showing any
           // next/previous links
-          panel.add(new PatchLink.SideBySide(fn, patchKey, 0, null /*
-                                                                    * parent
-                                                                    * table
-                                                                    */));
+          panel.add(new PatchLink.SideBySide(fn, patchKey, 0, null, null));
           priorFile = fn;
         }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeQueryResultsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java
similarity index 67%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeQueryResultsScreen.java
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java
index 001a06f3..bcd2ed2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeQueryResultsScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java
@@ -19,16 +19,23 @@
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.ChangeInfo;
 import com.google.gerrit.common.data.SingleListChangeInfo;
+import com.google.gerrit.reviewdb.RevId;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwtorm.client.KeyUtil;
 
 
+public class QueryScreen extends PagedSingleListScreen {
+  public static QueryScreen forQuery(String query) {
+    return forQuery(query, PageLinks.TOP);
+  }
 
-public class ChangeQueryResultsScreen extends AllSingleListScreen {
+  public static QueryScreen forQuery(String query, String position) {
+    return new QueryScreen(KeyUtil.encode(query), position);
+  }
+
   private final String query;
 
-  public ChangeQueryResultsScreen(final String encQuery,
-      final String positionToken) {
+  public QueryScreen(final String encQuery, final String positionToken) {
     super("q," + encQuery, positionToken);
     query = KeyUtil.decode(encQuery);
   }
@@ -45,13 +52,13 @@
     return new GerritCallback<SingleListChangeInfo>() {
       public final void onSuccess(final SingleListChangeInfo result) {
         if (isAttached()) {
-          if (result.getChanges().size() == 1) {
+          if (result.getChanges().size() == 1 && isSingleQuery(query)) {
             final ChangeInfo c = result.getChanges().get(0);
             Gerrit.display(PageLinks.toChange(c), new ChangeScreen(c));
           } else {
             Gerrit.setQueryString(query);
             display(result);
-            ChangeQueryResultsScreen.this.display();
+            QueryScreen.this.display();
           }
         }
       }
@@ -67,4 +74,26 @@
   protected void loadNext() {
     Util.LIST_SVC.allQueryNext(query, pos, pageSize, loadCallback());
   }
+
+  private static boolean isSingleQuery(String query) {
+    if (query.matches("^[1-9][0-9]*$")) {
+      // Legacy numeric identifier.
+      //
+      return true;
+    }
+
+    if (query.matches("^[iI][0-9a-f]{4,}$")) {
+      // Newer style Change-Id.
+      //
+      return true;
+    }
+
+    if (query.matches("^([0-9a-fA-F]{4," + RevId.LEN + "})$")) {
+      // Commit SHA-1 of any change.
+      //
+      return true;
+    }
+
+    return false;
+  }
 }
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 a68fc15..d44aa78 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
@@ -113,6 +113,29 @@
 }
 
 
+/** MenuScreen **/
+.menuScreenMenuBar {
+  background: topMenuColor;
+  padding-top: 0.5em;
+  padding-bottom: 10em;
+  padding-left: 0.5em;
+  padding-right: 0.5em;
+  border-right: 1px solid black;
+  margin-right: 0.5em;
+}
+
+.menuScreenMenuBar .menuItem {
+  white-space: nowrap;
+  display: block;
+  border-right: none;
+  padding: 0.2em;
+}
+
+.menuScreenMenuBar .menuItem.activeRow {
+  background: selectionColor;
+}
+
+
 /** CommentPanel **/
 .commentPanelBorder {
   border-top: 1px solid lightgray;
@@ -961,11 +984,14 @@
 
 
 /** AccountSettings  **/
-.sshPanelUsername {
+.usernameField {
+  white-space: nowrap;
+}
+.accountUsername {
   font-family: mono-font;
   font-size: small;
 }
-.sshPanelPassword {
+.accountPassword {
   font-family: mono-font;
   font-size: small;
 }
@@ -1003,6 +1029,15 @@
   font-weight: bold;
 }
 
+.addWatchPanel {
+  margin-top: 10px;
+  padding: 5px 5px 5px 5px;
+}
+.watchedProjectFilter {
+  margin-left: 1em;
+  color: grey;
+}
+
 .addSshKeyPanel {
   margin-top: 10px;
   background-color: trimColor;
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
new file mode 100644
index 0000000..6aae855
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/NavLinks.java
@@ -0,0 +1,105 @@
+// 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.PatchTable;
+import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.ui.ChangeLink;
+import com.google.gerrit.client.ui.InlineHyperlink;
+import com.google.gerrit.reviewdb.Change;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.Grid;
+import com.google.gwt.user.client.ui.HasHorizontalAlignment;
+import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
+import com.google.gwtexpui.globalkey.client.KeyCommand;
+import com.google.gwtexpui.globalkey.client.KeyCommandSet;
+import com.google.gwtexpui.safehtml.client.SafeHtml;
+
+class NavLinks extends Composite {
+  private final KeyCommandSet keys;
+  private final Grid table;
+
+  private InlineHyperlink prev;
+  private InlineHyperlink next;
+
+  private KeyCommand prevKey;
+  private KeyCommand nextKey;
+
+  NavLinks(KeyCommandSet kcs, Change.Id forChange) {
+    keys = kcs;
+    table = new Grid(1, 3);
+    initWidget(table);
+
+    final CellFormatter fmt = table.getCellFormatter();
+    table.setStyleName(Gerrit.RESOURCES.css().sideBySideScreenLinkTable());
+    fmt.setHorizontalAlignment(0, 0, HasHorizontalAlignment.ALIGN_LEFT);
+    fmt.setHorizontalAlignment(0, 1, HasHorizontalAlignment.ALIGN_CENTER);
+    fmt.setHorizontalAlignment(0, 2, HasHorizontalAlignment.ALIGN_RIGHT);
+
+    final ChangeLink up = new ChangeLink("", forChange);
+    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);
+    } else {
+      prev = null;
+      next = null;
+    }
+
+    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);
+    }
+
+    if (next != null) {
+      if (keys != null && nextKey == null) {
+        nextKey = new KeyCommand(0, ']', PatchUtil.C.nextFileHelp()) {
+          @Override
+          public void onKeyPress(KeyPressEvent event) {
+            next.go();
+          }
+        };
+        keys.add(nextKey);
+      }
+      table.setWidget(0, 2, next);
+    } else {
+      if (keys != null && nextKey != null) {
+        keys.remove(nextKey);
+        nextKey = null;
+      }
+      table.clearCell(0, 2);
+    }
+  }
+}
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 dfcbb7e..6c45138 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
@@ -14,25 +14,23 @@
 
 package com.google.gerrit.client.patches;
 
-import static com.google.gerrit.reviewdb.AccountGeneralPreferences.WHOLE_FILE_CONTEXT;
-
 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.ChangeLink;
 import com.google.gerrit.client.ui.InlineHyperlink;
 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.PatchScriptSettings;
 import com.google.gerrit.common.data.PatchSetDetail;
 import com.google.gerrit.prettify.client.ClientSideFormatter;
 import com.google.gerrit.prettify.common.PrettyFactory;
+import com.google.gerrit.reviewdb.AccountDiffPreference;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.Patch;
 import com.google.gerrit.reviewdb.PatchSet;
@@ -49,15 +47,12 @@
 import com.google.gwt.user.client.rpc.AsyncCallback;
 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.HasHorizontalAlignment;
+import com.google.gwt.user.client.ui.HorizontalPanel;
 import com.google.gwt.user.client.ui.Label;
-import com.google.gwt.user.client.ui.Widget;
-import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
+import com.google.gwt.user.client.ui.VerticalPanel;
 import com.google.gwtexpui.globalkey.client.GlobalKey;
 import com.google.gwtexpui.globalkey.client.KeyCommand;
 import com.google.gwtexpui.globalkey.client.KeyCommandSet;
-import com.google.gwtexpui.safehtml.client.SafeHtml;
 import com.google.gwtjsonrpc.client.VoidResult;
 
 public abstract class PatchScreen extends Screen implements
@@ -66,8 +61,8 @@
 
   public static class SideBySide extends PatchScreen {
     public SideBySide(final Patch.Key id, final int patchIndex,
-        final PatchTable patchTable) {
-      super(id, patchIndex, patchTable);
+        final PatchSetDetail patchSetDetail, final PatchTable patchTable) {
+      super(id, patchIndex, patchSetDetail, patchTable);
     }
 
     @Override
@@ -83,11 +78,11 @@
 
   public static class Unified extends PatchScreen {
     public Unified(final Patch.Key id, final int patchIndex,
-        final PatchTable patchTable) {
-      super(id, patchIndex, patchTable);
-      final PatchScriptSettings s = settingsPanel.getValue();
-      s.getPrettySettings().setSyntaxHighlighting(false);
-      settingsPanel.setValue(s);
+        final PatchSetDetail patchSetDetail, final PatchTable patchTable) {
+      super(id, patchIndex, patchSetDetail, patchTable);
+      final AccountDiffPreference dp = settingsPanel.getValue();
+      dp.setSyntaxHighlighting(false);
+      settingsPanel.setValue(dp);
     }
 
     @Override
@@ -104,6 +99,7 @@
   // Which patch set id's are being diff'ed
   private static PatchSet.Id diffSideA = null;
   private static PatchSet.Id diffSideB = null;
+
   private static Boolean historyOpen = null;
   private static final OpenHandler<DisclosurePanel> cacheOpenState =
       new OpenHandler<DisclosurePanel>() {
@@ -124,6 +120,7 @@
   private static Change.Id currentChangeId = null;
 
   protected final Patch.Key patchKey;
+  protected PatchSetDetail patchSetDetail;
   protected PatchTable fileList;
   protected PatchSet.Id idSideA;
   protected PatchSet.Id idSideB;
@@ -134,6 +131,9 @@
   private FlowPanel contentPanel;
   private Label noDifference;
   private AbstractPatchContentTable contentTable;
+  private CommitMessageBlock commitMessageBlock;
+  private NavLinks topNav;
+  private NavLinks bottomNav;
 
   private int rpcSequence;
   private PatchScript lastScript;
@@ -151,9 +151,6 @@
   /** Link to the screen for the next file, null if not applicable */
   private InlineHyperlink nextFileLink;
 
-  private static final char SHORTCUT_PREVIOUS_FILE = '[';
-  private static final char SHORTCUT_NEXT_FILE = ']';
-
   /**
    * How this patch should be displayed in the patch screen.
    */
@@ -162,8 +159,9 @@
   }
 
   protected PatchScreen(final Patch.Key id, final int patchIndex,
-      final PatchTable patchTable) {
+      final PatchSetDetail detail, final PatchTable patchTable) {
     patchKey = id;
+    patchSetDetail = detail;
     fileList = patchTable;
 
     // If we have any diff side stored, make sure they are applicable to the
@@ -182,9 +180,9 @@
 
     settingsPanel = new PatchScriptSettingsPanel();
     settingsPanel
-        .addValueChangeHandler(new ValueChangeHandler<PatchScriptSettings>() {
+        .addValueChangeHandler(new ValueChangeHandler<AccountDiffPreference>() {
           @Override
-          public void onValueChange(ValueChangeEvent<PatchScriptSettings> event) {
+          public void onValueChange(ValueChangeEvent<AccountDiffPreference> event) {
             update(event.getValue());
           }
         });
@@ -207,9 +205,9 @@
     lastScript = null;
   }
 
-  private void update(PatchScriptSettings s) {
-    if (lastScript != null && canReuse(s, lastScript)) {
-      lastScript.setSettings(s);
+  private void update(AccountDiffPreference dp) {
+    if (lastScript != null && canReuse(dp, lastScript)) {
+      lastScript.setDiffPrefs(dp);
       RpcStatus.INSTANCE.onRpcStart(null);
       settingsPanel.setEnabled(false);
       DeferredCommand.addCommand(new Command() {
@@ -227,24 +225,24 @@
     }
   }
 
-  private boolean canReuse(PatchScriptSettings s, PatchScript last) {
-    if (last.getSettings().getWhitespace() != s.getWhitespace()) {
+  private boolean canReuse(AccountDiffPreference dp, PatchScript last) {
+    if (last.getDiffPrefs().getIgnoreWhitespace() != dp.getIgnoreWhitespace()) {
       // Whitespace ignore setting requires server computation.
       return false;
     }
 
-    final int ctx = s.getContext();
-    if (ctx == WHOLE_FILE_CONTEXT && !last.getA().isWholeFile()) {
+    final int ctx = dp.getContext();
+    if (ctx == AccountDiffPreference.WHOLE_FILE_CONTEXT && !last.getA().isWholeFile()) {
       // We don't have the entire file here, so we can't render it.
       return false;
     }
 
-    if (last.getSettings().getContext() < ctx && !last.getA().isWholeFile()) {
+    if (last.getDiffPrefs().getContext() < ctx && !last.getA().isWholeFile()) {
       // We don't have sufficient context.
       return false;
     }
 
-    if (s.getPrettySettings().isSyntaxHighlighting()
+    if (dp.isSyntaxHighlighting()
         && !last.getA().isWholeFile()) {
       // We need the whole file to syntax highlight accurately.
       return false;
@@ -271,8 +269,17 @@
         || (historyOpen != null && historyOpen));
     historyPanel.addOpenHandler(cacheOpenState);
     historyPanel.addCloseHandler(cacheCloseState);
-    add(historyPanel);
-    add(settingsPanel);
+
+
+    VerticalPanel vp = new VerticalPanel();
+    vp.add(historyPanel);
+    vp.add(settingsPanel);
+    commitMessageBlock = new CommitMessageBlock("6em");
+    HorizontalPanel hp = new HorizontalPanel();
+    hp.setWidth("100%");
+    hp.add(vp);
+    hp.add(commitMessageBlock);
+    add(hp);
 
     noDifference = new Label(PatchUtil.C.noDifference());
     noDifference.setStyleName(Gerrit.RESOURCES.css().patchNoDifference());
@@ -281,35 +288,23 @@
     contentTable = createContentTable();
     contentTable.fileList = fileList;
 
-    add(createNextPrevLinks());
+    topNav =
+        new NavLinks(keysNavigation, patchKey.getParentKey().getParentKey());
+    bottomNav = new NavLinks(null, patchKey.getParentKey().getParentKey());
+
+    add(topNav);
     contentPanel = new FlowPanel();
     contentPanel.setStyleName(Gerrit.RESOURCES.css()
         .sideBySideScreenSideBySideTable());
     contentPanel.add(noDifference);
     contentPanel.add(contentTable);
     add(contentPanel);
-    add(createNextPrevLinks());
+    add(bottomNav);
 
-    // This must be done after calling createNextPrevLinks(), which initializes
-    // these fields
-    if (previousFileLink != null) {
-      installLinkShortCut(previousFileLink, SHORTCUT_PREVIOUS_FILE, PatchUtil.C
-          .previousFileHelp());
+    if (fileList != null) {
+      topNav.display(patchIndex, getPatchScreenType(), fileList);
+      bottomNav.display(patchIndex, getPatchScreenType(), fileList);
     }
-    if (nextFileLink != null) {
-      installLinkShortCut(nextFileLink, SHORTCUT_NEXT_FILE, PatchUtil.C
-          .nextFileHelp());
-    }
-  }
-
-  private void installLinkShortCut(final InlineHyperlink link, char shortcut,
-      String help) {
-    keysNavigation.add(new KeyCommand(0, shortcut, help) {
-      @Override
-      public void onKeyPress(KeyPressEvent event) {
-        link.go();
-      }
-    });
   }
 
   void setReviewedByCurrentUser(boolean reviewed) {
@@ -331,36 +326,28 @@
         });
   }
 
-  private Widget createNextPrevLinks() {
-    final Grid table = new Grid(1, 3);
-    final CellFormatter fmt = table.getCellFormatter();
-    table.setStyleName(Gerrit.RESOURCES.css().sideBySideScreenLinkTable());
-    fmt.setHorizontalAlignment(0, 0, HasHorizontalAlignment.ALIGN_LEFT);
-    fmt.setHorizontalAlignment(0, 1, HasHorizontalAlignment.ALIGN_CENTER);
-    fmt.setHorizontalAlignment(0, 2, HasHorizontalAlignment.ALIGN_RIGHT);
-
-    if (fileList != null) {
-      previousFileLink =
-          fileList.getPreviousPatchLink(patchIndex, getPatchScreenType());
-      table.setWidget(0, 0, previousFileLink);
-
-      nextFileLink =
-          fileList.getNextPatchLink(patchIndex, getPatchScreenType());
-      table.setWidget(0, 2, nextFileLink);
-    }
-
-    final ChangeLink up =
-        new ChangeLink("", patchKey.getParentKey().getParentKey());
-    SafeHtml.set(up, SafeHtml.asis(Util.C.upToChangeIconLink()));
-    table.setWidget(0, 1, up);
-
-    return table;
-  }
-
   @Override
   protected void onLoad() {
     super.onLoad();
-    refresh(true);
+    if (patchSetDetail == null) {
+      Util.DETAIL_SVC.patchSetDetail(idSideB,
+          new GerritCallback<PatchSetDetail>() {
+            @Override
+            public void onSuccess(PatchSetDetail result) {
+              patchSetDetail = result;
+              if (fileList == null) {
+                fileList = new PatchTable();
+                fileList.display(result);
+                patchIndex = fileList.indexOf(patchKey);
+                topNav.display(patchIndex, getPatchScreenType(), fileList);
+                bottomNav.display(patchIndex, getPatchScreenType(), fileList);
+              }
+              refresh(true);
+            }
+          });
+    } else {
+      refresh(true);
+    }
   }
 
   @Override
@@ -418,6 +405,20 @@
     setWindowTitle(PatchUtil.M.patchWindowTitle(cid.abbreviate(), fileName));
     setPageTitle(PatchUtil.M.patchPageTitle(cid.abbreviate(), path));
 
+    if (idSideB.equals(patchSetDetail.getPatchSet().getId())) {
+      commitMessageBlock.setVisible(true);
+      commitMessageBlock.display(patchSetDetail.getInfo().getMessage());
+    } else {
+      commitMessageBlock.setVisible(false);
+      Util.DETAIL_SVC.patchSetDetail(idSideB,
+          new GerritCallback<PatchSetDetail>() {
+            @Override
+            public void onSuccess(PatchSetDetail result) {
+              commitMessageBlock.display(result.getInfo().getMessage());
+            }
+          });
+    }
+
     historyTable.display(script.getHistory());
     historyPanel.setVisible(true);
 
@@ -501,7 +502,7 @@
         Util.DETAIL_SVC.patchSetDetail(psid,
             new GerritCallback<PatchSetDetail>() {
               public void onSuccess(final PatchSetDetail result) {
-                fileList.display(psid, result.getPatches());
+                fileList.display(result);
               }
             });
       }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java
index 5a461ed..758b0f0 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
@@ -14,16 +14,12 @@
 
 package com.google.gerrit.client.patches;
 
-import static com.google.gerrit.reviewdb.AccountGeneralPreferences.DEFAULT_CONTEXT;
-import static com.google.gerrit.reviewdb.AccountGeneralPreferences.WHOLE_FILE_CONTEXT;
-
 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.NpIntTextBox;
-import com.google.gerrit.common.data.PatchScriptSettings;
-import com.google.gerrit.common.data.PatchScriptSettings.Whitespace;
-import com.google.gerrit.prettify.common.PrettySettings;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountGeneralPreferences;
+import com.google.gerrit.reviewdb.AccountDiffPreference;
+import com.google.gerrit.reviewdb.AccountDiffPreference.Whitespace;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.KeyCodes;
@@ -43,15 +39,16 @@
 import com.google.gwt.user.client.ui.HasWidgets;
 import com.google.gwt.user.client.ui.ListBox;
 import com.google.gwt.user.client.ui.Widget;
+import com.google.gwtjsonrpc.client.VoidResult;
 
 public class PatchScriptSettingsPanel extends Composite implements
-    HasValueChangeHandlers<PatchScriptSettings> {
+    HasValueChangeHandlers<AccountDiffPreference> {
   private static MyUiBinder uiBinder = GWT.create(MyUiBinder.class);
 
   interface MyUiBinder extends UiBinder<Widget, PatchScriptSettingsPanel> {
   }
 
-  private PatchScriptSettings value;
+  private AccountDiffPreference value;
   private boolean enableIntralineDifference = true;
   private boolean enableSmallFileFeatures = true;
 
@@ -71,7 +68,7 @@
   CheckBox intralineDifference;
 
   @UiField
-  CheckBox showFullFile;
+  ListBox context;
 
   @UiField
   CheckBox whitespaceErrors;
@@ -85,9 +82,24 @@
   @UiField
   Button update;
 
+  /**
+   * Counts +1 for every setEnabled(true) and -1 for every setEnabled(false)
+   *
+   * The purpose is to prevent enabling widgets too early. It might happen that
+   * setEnabled(false) is called from this class and from an event handler
+   * of ValueChangeEvent in another class. The first setEnabled(true) would then
+   * enable widgets too early i.e. before the second setEnabled(true) is called.
+   *
+   * With this counter the setEnabled(true) will enable widgets only when
+   * setEnabledCounter == 0. Until it is less than zero setEnabled(true) will
+   * not enable the widgets.
+   */
+  private int setEnabledCounter;
+
   public PatchScriptSettingsPanel() {
     initWidget(uiBinder.createAndBindUi(this));
     initIgnoreWhitespace(ignoreWhitespace);
+    initContext(context);
     if (!Gerrit.isSignedIn()) {
       reviewed.setVisible(false);
     }
@@ -103,42 +115,41 @@
     tabWidth.addKeyPressHandler(onEnter);
     colWidth.addKeyPressHandler(onEnter);
 
-    final PatchScriptSettings s = new PatchScriptSettings();
-    if (Gerrit.isSignedIn()) {
-      final Account u = Gerrit.getUserAccount();
-      final AccountGeneralPreferences pref = u.getGeneralPreferences();
-      s.setContext(pref.getDefaultContext());
+    if (Gerrit.isSignedIn() && Gerrit.getAccountDiffPreference() != null) {
+      setValue(Gerrit.getAccountDiffPreference());
     } else {
-      s.setContext(DEFAULT_CONTEXT);
+      setValue(AccountDiffPreference.createDefault(null));
     }
-    setValue(s);
   }
 
   @Override
   public HandlerRegistration addValueChangeHandler(
-      ValueChangeHandler<PatchScriptSettings> handler) {
+      ValueChangeHandler<AccountDiffPreference> handler) {
     return super.addHandler(handler, ValueChangeEvent.getType());
   }
 
   public void setEnabled(final boolean on) {
-    for (Widget w : (HasWidgets) getWidget()) {
-      if (w instanceof FocusWidget) {
-        ((FocusWidget) w).setEnabled(on);
-      }
+    if (on) {
+      setEnabledCounter++;
+    } else {
+      setEnabledCounter--;
     }
-    toggleEnabledStatus(on);
+    if (on && setEnabledCounter == 0 || !on) {
+      for (Widget w : (HasWidgets) getWidget()) {
+        if (w instanceof FocusWidget) {
+          ((FocusWidget) w).setEnabled(on);
+        }
+      }
+      toggleEnabledStatus(on);
+    };
   }
 
   public void setEnableSmallFileFeatures(final boolean on) {
     enableSmallFileFeatures = on;
     if (enableSmallFileFeatures) {
-      final PrettySettings p = getValue().getPrettySettings();
-
-      syntaxHighlighting.setValue(p.isSyntaxHighlighting());
-      showFullFile.setValue(getValue().getContext() == WHOLE_FILE_CONTEXT);
+      syntaxHighlighting.setValue(value.isSyntaxHighlighting());
     } else {
       syntaxHighlighting.setValue(false);
-      showFullFile.setValue(false);
     }
     toggleEnabledStatus(update.isEnabled());
   }
@@ -146,8 +157,7 @@
   public void setEnableIntralineDifference(final boolean on) {
     enableIntralineDifference = on;
     if (enableIntralineDifference) {
-      final PrettySettings p = getValue().getPrettySettings();
-      intralineDifference.setValue(p.isIntralineDifference());
+      intralineDifference.setValue(value.isIntralineDifference());
     } else {
       intralineDifference.setValue(false);
     }
@@ -157,41 +167,36 @@
   private void toggleEnabledStatus(final boolean on) {
     intralineDifference.setEnabled(on & enableIntralineDifference);
     syntaxHighlighting.setEnabled(on & enableSmallFileFeatures);
-    showFullFile.setEnabled(on & enableSmallFileFeatures);
 
     final String title =
         enableSmallFileFeatures ? null : PatchUtil.C.disabledOnLargeFiles();
     syntaxHighlighting.setTitle(title);
-    showFullFile.setTitle(title);
   }
 
   public CheckBox getReviewedCheckBox() {
     return reviewed;
   }
 
-  public PatchScriptSettings getValue() {
+  public AccountDiffPreference getValue() {
     return value;
   }
 
-  public void setValue(final PatchScriptSettings s) {
-    final PrettySettings p = s.getPrettySettings();
-
-    setIgnoreWhitespace(s.getWhitespace());
+  public void setValue(final AccountDiffPreference dp) {
+    setIgnoreWhitespace(dp.getIgnoreWhitespace());
     if (enableSmallFileFeatures) {
-      showFullFile.setValue(s.getContext() == WHOLE_FILE_CONTEXT);
-      syntaxHighlighting.setValue(p.isSyntaxHighlighting());
+      syntaxHighlighting.setValue(dp.isSyntaxHighlighting());
     } else {
-      showFullFile.setValue(false);
       syntaxHighlighting.setValue(false);
     }
+    setContext(dp.getContext());
 
-    tabWidth.setIntValue(p.getTabSize());
-    colWidth.setIntValue(p.getLineLength());
-    intralineDifference.setValue(p.isIntralineDifference());
-    whitespaceErrors.setValue(p.isShowWhiteSpaceErrors());
-    showTabs.setValue(p.isShowTabs());
+    tabWidth.setIntValue(dp.getTabSize());
+    colWidth.setIntValue(dp.getLineLength());
+    intralineDifference.setValue(dp.isIntralineDifference());
+    whitespaceErrors.setValue(dp.isShowWhitespaceErrors());
+    showTabs.setValue(dp.isShowTabs());
 
-    value = s;
+    value = dp;
   }
 
   @UiHandler("update")
@@ -200,33 +205,38 @@
   }
 
   private void update() {
-    PatchScriptSettings s = new PatchScriptSettings(getValue());
-    PrettySettings p = s.getPrettySettings();
+    AccountDiffPreference dp = new AccountDiffPreference(value);
+    dp.setIgnoreWhitespace(getIgnoreWhitespace());
+    dp.setContext(getContext());
+    dp.setTabSize(tabWidth.getIntValue());
+    dp.setLineLength(colWidth.getIntValue());
+    dp.setSyntaxHighlighting(syntaxHighlighting.getValue());
+    dp.setIntralineDifference(intralineDifference.getValue());
+    dp.setShowWhitespaceErrors(whitespaceErrors.getValue());
+    dp.setShowTabs(showTabs.getValue());
 
-    s.setWhitespace(getIgnoreWhitespace());
-    if (showFullFile.getValue()) {
-      s.setContext(WHOLE_FILE_CONTEXT);
-    } else if (Gerrit.isSignedIn()) {
-      final Account u = Gerrit.getUserAccount();
-      final AccountGeneralPreferences pref = u.getGeneralPreferences();
-      if (pref.getDefaultContext() == WHOLE_FILE_CONTEXT) {
-        s.setContext(DEFAULT_CONTEXT);
-      } else {
-        s.setContext(pref.getDefaultContext());
-      }
-    } else {
-      s.setContext(DEFAULT_CONTEXT);
+    value = dp;
+    fireEvent(new ValueChangeEvent<AccountDiffPreference>(dp) {});
+
+    if (Gerrit.isSignedIn()) {
+      persistDiffPreferences();
     }
+  }
 
-    p.setTabSize(tabWidth.getIntValue());
-    p.setLineLength(colWidth.getIntValue());
-    p.setSyntaxHighlighting(syntaxHighlighting.getValue());
-    p.setIntralineDifference(intralineDifference.getValue());
-    p.setShowWhiteSpaceErrors(whitespaceErrors.getValue());
-    p.setShowTabs(showTabs.getValue());
+  private void persistDiffPreferences() {
+    setEnabled(false);
+    Util.ACCOUNT_SVC.changeDiffPreferences(value, new GerritCallback<VoidResult>() {
+      @Override
+      public void onSuccess(VoidResult result) {
+        Gerrit.setAccountDiffPreference(value);
+        setEnabled(true);
+      }
 
-    value = s;
-    fireEvent(new ValueChangeEvent<PatchScriptSettings>(s) {});
+      @Override
+      public void onFailure(Throwable caught) {
+        setEnabled(true);
+      }
+    });
   }
 
   private void initIgnoreWhitespace(ListBox ws) {
@@ -240,12 +250,24 @@
         Whitespace.IGNORE_ALL_SPACE.name());
   }
 
+  private void initContext(ListBox context) {
+    for (final short v : AccountDiffPreference.CONTEXT_CHOICES) {
+      final String label;
+      if (v == AccountDiffPreference.WHOLE_FILE_CONTEXT) {
+        label = Util.C.contextWholeFile();
+      } else {
+        label = Util.M.lines(v);
+      }
+      context.addItem(label, String.valueOf(v));
+    }
+  }
+
   private Whitespace getIgnoreWhitespace() {
     final int sel = ignoreWhitespace.getSelectedIndex();
     if (0 <= sel) {
       return Whitespace.valueOf(ignoreWhitespace.getValue(sel));
     }
-    return value.getWhitespace();
+    return value.getIgnoreWhitespace();
   }
 
   private void setIgnoreWhitespace(Whitespace s) {
@@ -257,4 +279,23 @@
     }
     ignoreWhitespace.setSelectedIndex(0);
   }
+
+  private short getContext() {
+    final int sel = context.getSelectedIndex();
+    if (0 <= sel) {
+      return Short.parseShort(context.getValue(sel));
+    }
+    return (short) value.getContext();
+  }
+
+  private void setContext(int ctx) {
+    String v = String.valueOf(ctx);
+    for (int i = 0; i < context.getItemCount(); i++) {
+      if (context.getValue(i).equals(v)) {
+        context.setSelectedIndex(i);
+        return;
+      }
+    }
+    context.setSelectedIndex(0);
+  }
 }
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 9d7303d..7bbc8fe 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
@@ -49,6 +49,7 @@
   .controls .gwt-ListBox {
     font-size: fontSize;
     padding: 0;
+    margin-right: 1em;
   }
 
   .updateButton {
@@ -61,13 +62,24 @@
 <g:HTMLPanel>
 <table class='{style.controls}'>
   <tr valign='top'>
-    <td colspan='2'>
-      <ui:msg>
-      Ignore Whitespace:
-      <g:ListBox
-        ui:field='ignoreWhitespace'
-        visibleItemCount='1'
-        tabIndex='1'/>
+    <ui:msg>
+      <td align='right'>Ignore Whitespace:</td>
+      <td align='right'>
+        <g:ListBox
+          ui:field='ignoreWhitespace'
+          visibleItemCount='1'
+          tabIndex='1'/>
+      </td>
+    </ui:msg>
+
+    <td align='right'>
+      <ui:msg>Tab Width:
+      <my:NpIntTextBox
+        ui:field='tabWidth'
+        width='2em'
+        visibleLength='2'
+        maxLength='2'
+        tabIndex='3'/>
       </ui:msg>
     </td>
 
@@ -75,20 +87,13 @@
       <g:CheckBox
           ui:field='syntaxHighlighting'
           text='Syntax Coloring'
-          tabIndex='4'>
+          tabIndex='5'>
         <ui:attribute name='text'/>
       </g:CheckBox>
       <br/>
       <g:CheckBox
           ui:field='intralineDifference'
           text='Intraline Difference'
-          tabIndex='5'>
-        <ui:attribute name='text'/>
-      </g:CheckBox>
-      <br/>
-      <g:CheckBox
-          ui:field='showFullFile'
-          text='Show Full File'
           tabIndex='6'>
         <ui:attribute name='text'/>
       </g:CheckBox>
@@ -131,28 +136,27 @@
   </tr>
 
   <tr valign='top'>
-    <td>
-      <ui:msg>Tab Width:
-      <my:NpIntTextBox
-        ui:field='tabWidth'
-        width='2em'
-        visibleLength='2'
-        maxLength='2'
-        tabIndex='2'/>
-      </ui:msg>
-    </td>
+    <ui:msg>
+      <td align='right'>Context:</td>
+      <td align='right'>
+        <g:ListBox
+            ui:field='context'
+            visibleItemCount='1'
+            tabIndex='2'/>
+      </td>
+    </ui:msg>
 
-    <td>
+    <td align='right'>
       <ui:msg>Columns:
       <my:NpIntTextBox
         ui:field='colWidth'
         width='2.5em'
         visibleLength='3'
         maxLength='3'
-        tabIndex='3'/>
+        tabIndex='4'/>
       </ui:msg>
     </td>
-  </tr>    
+  </tr>
 </table>
 </g:HTMLPanel>
 </ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SideBySideTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SideBySideTable.java
index 9b42573..1bc4dd5 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
@@ -75,7 +75,7 @@
     final ArrayList<PatchLine> lines = new ArrayList<PatchLine>();
     final SafeHtmlBuilder nc = new SafeHtmlBuilder();
     final boolean intraline =
-        script.getSettings().getPrettySettings().isIntralineDifference()
+        script.getDiffPrefs().isIntralineDifference()
             && script.hasIntralineDifference();
 
     appendHeader(script, nc);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedDiffTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedDiffTable.java
index b2be30a..508e508 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedDiffTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedDiffTable.java
@@ -133,7 +133,7 @@
     }
 
     final boolean syntaxHighlighting =
-        script.getSettings().getPrettySettings().isSyntaxHighlighting();
+        script.getDiffPrefs().isSyntaxHighlighting();
     final ArrayList<PatchLine> lines = new ArrayList<PatchLine>();
     for (final EditList.Hunk hunk : script.getHunks()) {
       appendHunkHeader(nc, hunk);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/BranchLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/BranchLink.java
new file mode 100644
index 0000000..01d7d8b
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/BranchLink.java
@@ -0,0 +1,84 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.ui;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.QueryScreen;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.Branch;
+import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.Project;
+
+/** Link to the open changes of a project. */
+public class BranchLink extends InlineHyperlink {
+  private final String query;
+
+  public BranchLink(Project.NameKey project, Change.Status status,
+      String branch, String topic) {
+    this(text(branch, topic), query(project, status, branch, topic));
+  }
+
+  public BranchLink(String text, Project.NameKey project, Change.Status status,
+      String branch, String topic) {
+    this(text, query(project, status, branch, topic));
+  }
+
+  private BranchLink(String text, String query) {
+    super(text, PageLinks.toChangeQuery(query));
+    this.query = query;
+  }
+
+  @Override
+  public void go() {
+    Gerrit.display(getTargetHistoryToken(), createScreen());
+  }
+
+  private Screen createScreen() {
+    return QueryScreen.forQuery(query);
+  }
+
+  private static String text(String branch, String topic) {
+    if (topic != null && !topic.isEmpty()) {
+      return branch + " (" + topic + ")";
+    } else {
+      return branch;
+    }
+  }
+
+  private static String query(Project.NameKey project, Change.Status status,
+      String branch, String topic) {
+    String query = PageLinks.projectQuery(project, status);
+
+    if (branch.startsWith(Branch.R_REFS)) {
+      if (branch.startsWith(Branch.R_HEADS)) {
+        query += " " + PageLinks.op("branch", //
+            branch.substring(Branch.R_HEADS.length()));
+      } else {
+        query += " " + PageLinks.op("ref", branch);
+      }
+    } else {
+      // Assume it was clipped already by the caller.  This
+      // happens for example inside of the ChangeInfo object.
+      //
+      query += " " + PageLinks.op("branch", branch);
+    }
+
+    if (topic != null && !topic.isEmpty()) {
+      query += " " + PageLinks.op("topic", topic);
+    }
+
+    return query;
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuBar.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuBar.java
index fa3deba..0353281 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuBar.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuBar.java
@@ -47,6 +47,18 @@
     body.clear();
   }
 
+  public LinkMenuItem find(String targetToken) {
+    for (Widget w : body) {
+      if (w instanceof LinkMenuItem) {
+        LinkMenuItem m = (LinkMenuItem) w;
+        if (targetToken.equals(m.getTargetHistoryToken())) {
+          return m;
+        }
+      }
+    }
+    return null;
+  }
+
   public void add(final Widget i) {
     if (body.getWidgetCount() > 0) {
       final Widget p = body.getWidget(body.getWidgetCount() - 1);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/MenuScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/MenuScreen.java
new file mode 100644
index 0000000..704185c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/MenuScreen.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.client.ui;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HorizontalPanel;
+import com.google.gwt.user.client.ui.Widget;
+
+public abstract class MenuScreen extends Screen {
+  private final LinkMenuBar menu;
+  private final FlowPanel body;
+
+  public MenuScreen() {
+    menu = new LinkMenuBar();
+    menu.setStyleName(Gerrit.RESOURCES.css().menuScreenMenuBar());
+    body = new FlowPanel();
+  }
+
+  @Override
+  protected void onInitUI() {
+    super.onInitUI();
+
+    HorizontalPanel hp = new HorizontalPanel();
+    hp.add(menu);
+    hp.add(body);
+    super.add(hp);
+  }
+
+  @Override
+  public void setToken(String token) {
+    LinkMenuItem self = menu.find(token);
+    if (self != null) {
+      self.addStyleName(Gerrit.RESOURCES.css().activeRow());
+    }
+    super.setToken(token);
+  }
+
+  @Override
+  protected void add(final Widget w) {
+    body.add(w);
+  }
+
+  protected void link(String text, String target) {
+    final LinkMenuItem item = new LinkMenuItem(text, target);
+    item.setStyleName(Gerrit.RESOURCES.css().menuItem());
+    menu.add(item);
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NavigationTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NavigationTable.java
index 1beae9d..e5cc0ea 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NavigationTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NavigationTable.java
@@ -148,6 +148,7 @@
       }
     } else if (clear) {
       table.setWidget(currentRow, C_ARROW, null);
+      pointer.removeFromParent();
     }
     currentRow = newRow;
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/PatchLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/PatchLink.java
index 6a34826..9979edf 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/PatchLink.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/PatchLink.java
@@ -16,11 +16,13 @@
 
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.changes.PatchTable;
+import com.google.gerrit.common.data.PatchSetDetail;
 import com.google.gerrit.reviewdb.Patch;
 
 public abstract class PatchLink extends InlineHyperlink {
   protected Patch.Key patchKey;
   protected int patchIndex;
+  protected PatchSetDetail patchSetDetail;
   protected PatchTable parentPatchTable;
 
   /**
@@ -28,14 +30,17 @@
    * @param patchKey The key for this patch
    * @param patchIndex The index of the current patch in the patch set
    * @param historyToken The history token
+   * @parma patchSetDetail Detailed information about the patch set.
    * @param parentPatchTable The table used to display this link
    */
   public PatchLink(final String text, final Patch.Key patchKey,
       final int patchIndex, final String historyToken,
-      PatchTable parentPatchTable) {
+      final PatchSetDetail patchSetDetail,
+      final PatchTable parentPatchTable) {
     super(text, historyToken);
     this.patchKey = patchKey;
     this.patchIndex = patchIndex;
+    this.patchSetDetail = patchSetDetail;
     this.parentPatchTable = parentPatchTable;
   }
 
@@ -45,23 +50,26 @@
         getTargetHistoryToken(), //
         patchKey, //
         patchIndex, //
+        patchSetDetail, //
         parentPatchTable //
         );
   }
 
   public static class SideBySide extends PatchLink {
     public SideBySide(final String text, final Patch.Key patchKey,
-        final int patchIndex, PatchTable parentPatchTable) {
-      super(text, patchKey, patchIndex, Dispatcher
-          .toPatchSideBySide(patchKey), parentPatchTable);
+        final int patchIndex, PatchSetDetail patchSetDetail,
+        PatchTable parentPatchTable) {
+      super(text, patchKey, patchIndex, Dispatcher.toPatchSideBySide(patchKey),
+          patchSetDetail, parentPatchTable);
     }
   }
 
   public static class Unified extends PatchLink {
     public Unified(final String text, final Patch.Key patchKey,
-        final int patchIndex, PatchTable parentPatchTable) {
-      super(text, patchKey, patchIndex,
-          Dispatcher.toPatchUnified(patchKey), parentPatchTable);
+        final int patchIndex, PatchSetDetail patchSetDetail,
+        PatchTable parentPatchTable) {
+      super(text, patchKey, patchIndex, Dispatcher.toPatchUnified(patchKey),
+          patchSetDetail, parentPatchTable);
     }
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectLink.java
index 8ed7bf1..b2c4c3e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectLink.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectLink.java
@@ -15,9 +15,7 @@
 package com.google.gerrit.client.ui;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.ByProjectAbandonedChangesScreen;
-import com.google.gerrit.client.changes.ByProjectMergedChangesScreen;
-import com.google.gerrit.client.changes.ByProjectOpenChangesScreen;
+import com.google.gerrit.client.changes.QueryScreen;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.Project;
@@ -34,7 +32,7 @@
 
   public ProjectLink(final String text, final Project.NameKey proj,
       Change.Status stat) {
-    super(text, PageLinks.toProject(proj, stat));
+    super(text, PageLinks.toChangeQuery(PageLinks.projectQuery(proj, stat)));
     status = stat;
     project = proj;
   }
@@ -45,17 +43,6 @@
   }
 
   private Screen createScreen() {
-    switch (status) {
-      case ABANDONED:
-        return new ByProjectAbandonedChangesScreen(project, "n,z");
-
-      case MERGED:
-        return new ByProjectMergedChangesScreen(project, "n,z");
-
-      case NEW:
-      case SUBMITTED:
-      default:
-        return new ByProjectOpenChangesScreen(project, "n,z");
-    }
+    return QueryScreen.forQuery(PageLinks.projectQuery(project, status));
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java
index a68ab06..a52b4ba 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java
@@ -62,7 +62,12 @@
 
   protected void setPageTitle(final String text) {
     final String old = headerText.getText();
-    headerText.setText(text);
+    if (text.isEmpty()) {
+      header.setVisible(false);
+    } else {
+      headerText.setText(text);
+      header.setVisible(true);
+    }
     if (windowTitle == null || windowTitle == old) {
       setWindowTitle(text);
     }
@@ -72,7 +77,7 @@
     header.insert(w, 0);
   }
 
-  protected final void add(final Widget w) {
+  protected void add(final Widget w) {
     body.add(w);
   }
 
diff --git a/gerrit-httpd/pom.xml b/gerrit-httpd/pom.xml
index 7ee59a2..b02b142 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.3</version>
+    <version>2.1.4-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
new file mode 100644
index 0000000..b79971c
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ChangeQueryServlet.java
@@ -0,0 +1,112 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd;
+
+import com.google.gerrit.server.query.change.QueryProcessor;
+import com.google.gerrit.server.query.change.QueryProcessor.OutputFormat;
+import com.google.gson.Gson;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import java.io.IOException;
+
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@Singleton
+public class ChangeQueryServlet extends HttpServlet {
+  private final Provider<QueryProcessor> processor;
+
+  @Inject
+  ChangeQueryServlet(Provider<QueryProcessor> processor) {
+    this.processor = processor;
+  }
+
+  @Override
+  protected void doGet(HttpServletRequest req, HttpServletResponse rsp)
+      throws IOException {
+    rsp.setContentType("text/json");
+    rsp.setCharacterEncoding("UTF-8");
+
+    QueryProcessor p = processor.get();
+    OutputFormat format = OutputFormat.JSON;
+    try {
+      format = OutputFormat.valueOf(get(req, "format", format.toString()));
+    } catch (IllegalArgumentException err) {
+      error(rsp, "invalid format");
+      return;
+    }
+
+    switch (format) {
+      case JSON:
+        rsp.setContentType("text/json");
+        rsp.setCharacterEncoding("UTF-8");
+        break;
+
+      case TEXT:
+        rsp.setContentType("text/plain");
+        rsp.setCharacterEncoding("UTF-8");
+        break;
+
+      default:
+        error(rsp, "invalid format");
+        return;
+    }
+
+    p.setIncludeCurrentPatchSet(get(req, "current-patch-set", false));
+    p.setIncludePatchSets(get(req, "patch-sets", false));
+    p.setOutput(rsp.getOutputStream(), format);
+    p.query(get(req, "q", "status:open"));
+  }
+
+  private static void error(HttpServletResponse rsp, String message)
+      throws IOException {
+    ErrorMessage em = new ErrorMessage();
+    em.message = message;
+
+    ServletOutputStream out = rsp.getOutputStream();
+    try {
+      out.write(new Gson().toJson(em).getBytes("UTF-8"));
+      out.write('\n');
+      out.flush();
+    } finally {
+      out.close();
+    }
+  }
+
+  private static String get(HttpServletRequest req, String name, String val) {
+    String v = req.getParameter(name);
+    if (v == null || v.isEmpty()) {
+      return val;
+    }
+    return v;
+  }
+
+  private static boolean get(HttpServletRequest req, String name, boolean val) {
+    String v = req.getParameter(name);
+    if (v == null || v.isEmpty()) {
+      return val;
+    }
+    return "true".equalsIgnoreCase(v);
+  }
+
+  public static class ErrorMessage {
+    public final String type = "error";
+    public String message;
+  }
+}
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 336d2fe..3f7f68d 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
@@ -115,7 +115,8 @@
         StringBuilder r = new StringBuilder();
         r.append(urlProvider.get());
         r.append('#');
-        r.append(PageLinks.toProject(dst, Change.Status.NEW));
+        r.append(PageLinks.toChangeQuery(PageLinks.projectQuery(dst,
+            Change.Status.NEW)));
         rsp.sendRedirect(r.toString());
       }
     });
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
index 0c25404..a55c5a7 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.httpd.raw.SshInfoServlet;
 import com.google.gerrit.httpd.raw.StaticServlet;
 import com.google.gerrit.httpd.raw.ToolServlet;
-import com.google.gerrit.reviewdb.RevId;
 import com.google.gwtexpui.server.CacheControlFilter;
 import com.google.inject.Key;
 import com.google.inject.Provider;
@@ -47,6 +46,7 @@
     serve("/Gerrit/*").with(legacyGerritScreen());
     serve("/cat/*").with(CatServlet.class);
     serve("/logout").with(HttpLogoutServlet.class);
+    serve("/query").with(ChangeQueryServlet.class);
     serve("/signout").with(HttpLogoutServlet.class);
     serve("/ssh_info").with(SshInfoServlet.class);
     serve("/static/*").with(StaticServlet.class);
@@ -60,15 +60,16 @@
     serve("/com/google/gerrit/launcher/*").with(notFound());
     serve("/servlet/*").with(notFound());
 
-    serve("/all").with(screen(PageLinks.ALL_MERGED));
+    serve("/all").with(query("status:merged"));
     serve("/mine").with(screen(PageLinks.MINE));
-    serve("/open").with(screen(PageLinks.ALL_OPEN));
+    serve("/open").with(query("status:open"));
     serve("/settings").with(screen(PageLinks.SETTINGS));
-    serve("/starred").with(screen(PageLinks.MINE_STARRED));
+    serve("/watched").with(query("is:watched status:open"));
+    serve("/starred").with(query("is:starred"));
 
     serveRegex( //
         "^/([1-9][0-9]*)/?$", //
-        "^/r/(I?[0-9a-fA-F]{4," + RevId.LEN + "})/?$" //
+        "^/r/(.+)/?$" //
     ).with(changeQuery());
   }
 
@@ -121,6 +122,18 @@
     });
   }
 
+  private Key<HttpServlet> query(final String query) {
+    return key(new HttpServlet() {
+      private static final long serialVersionUID = 1L;
+
+      @Override
+      protected void doGet(final HttpServletRequest req,
+          final HttpServletResponse rsp) throws IOException {
+        toGerrit(PageLinks.toChangeQuery(query), req, rsp);
+      }
+    });
+  }
+
   private Key<HttpServlet> key(final HttpServlet servlet) {
     final Key<HttpServlet> srv =
         Key.get(HttpServlet.class, UniqueAnnotations.create());
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 17a7ba5..93b6d09 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
@@ -29,6 +29,7 @@
 import com.google.gerrit.server.RemotePeer;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.ChangeUserName;
+import com.google.gerrit.server.account.ClearPassword;
 import com.google.gerrit.server.account.GeneratePassword;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.CanonicalWebUrl;
@@ -139,6 +140,7 @@
     bind(AccountManager.class);
     bind(ChangeUserName.CurrentUser.class);
     factory(ChangeUserName.Factory.class);
+    factory(ClearPassword.Factory.class);
     factory(GeneratePassword.Factory.class);
 
     bind(SocketAddress.class).annotatedWith(RemotePeer.class).toProvider(
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
index ff81620..58f589a 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
@@ -136,7 +136,7 @@
     }
     rdr.append(token);
 
-    webSession.get().login(arsp, false);
+    webSession.get().login(arsp, true /* persistent cookie */);
     rsp.sendRedirect(rdr.toString());
   }
 
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 08fcaa8..a59d013 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
@@ -63,7 +63,7 @@
 
     result.success = true;
     result.isNew = res.isNew();
-    webSession.get().login(res, false);
+    webSession.get().login(res, true /* persistent cookie */);
     callback.onSuccess(result);
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdModule.java
index 6f2caa3..a928cb8 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdModule.java
@@ -14,33 +14,13 @@
 
 package com.google.gerrit.httpd.auth.openid;
 
-import static java.util.concurrent.TimeUnit.MINUTES;
-
 import com.google.gerrit.httpd.rpc.RpcServletModule;
-import com.google.gerrit.server.cache.Cache;
-import com.google.gerrit.server.cache.CacheModule;
-import com.google.inject.TypeLiteral;
 import com.google.inject.servlet.ServletModule;
 
-import java.util.List;
-
 /** Servlets and RPC support related to OpenID authentication. */
 public class OpenIdModule extends ServletModule {
   @Override
   protected void configureServlets() {
-    install(new CacheModule() {
-      @SuppressWarnings("unchecked")
-      @Override
-      protected void configure() {
-        final TypeLiteral<Cache<String, List>> type =
-            new TypeLiteral<Cache<String, List>>() {};
-        core(type, "openid") //
-            .maxAge(5, MINUTES) // don't cache too long, might be stale
-            .memoryLimit(64) // short TTL means we won't have many entries
-        ;
-      }
-    });
-
     serve("/" + OpenIdServiceImpl.RETURN_URL).with(OpenIdLoginServlet.class);
     serve("/" + XrdsServlet.LOCATION).with(XrdsServlet.class);
     filter("/").through(XrdsFilter.class);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
index 3bef30f..068855f 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
@@ -26,8 +26,6 @@
 import com.google.gerrit.server.UrlEncoded;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
-import com.google.gerrit.server.cache.Cache;
-import com.google.gerrit.server.cache.SelfPopulatingCache;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.ConfigUtil;
@@ -37,7 +35,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
-import com.google.inject.name.Named;
 
 import org.eclipse.jgit.lib.Config;
 import org.openid4java.consumer.ConsumerException;
@@ -104,7 +101,6 @@
   private final AccountManager accountManager;
   private final ConsumerManager manager;
   private final List<OpenIdProviderPattern> allowedOpenIDs;
-  private final SelfPopulatingCache<String, List> discoveryCache;
 
   /** Maximum age, in seconds, before forcing re-authentication of account. */
   private final int papeMaxAuthAge;
@@ -113,7 +109,6 @@
   OpenIdServiceImpl(final Provider<WebSession> cf,
       final Provider<IdentifiedUser> iu,
       @CanonicalWebUrl @Nullable final Provider<String> up,
-      @Named("openid") final Cache<String, List> openidCache,
       @GerritServerConfig final Config config, final AuthConfig ac,
       final AccountManager am) throws ConsumerException, MalformedURLException {
 
@@ -149,19 +144,6 @@
     allowedOpenIDs = ac.getAllowedOpenIDs();
     papeMaxAuthAge = (int) ConfigUtil.getTimeUnit(config, //
         "auth", null, "maxOpenIdSessionAge", -1, TimeUnit.SECONDS);
-
-    discoveryCache = new SelfPopulatingCache<String, List>(openidCache) {
-      @Override
-      protected List createEntry(final String url) throws Exception {
-        try {
-          final List<?> list = manager.discover(url);
-          return list != null && !list.isEmpty() ? list : null;
-        } catch (DiscoveryException e) {
-          log.error("Cannot discover OpenID " + url, e);
-          return null;
-        }
-      }
-    };
   }
 
   public void discover(final String openidIdentifier, final SignInMode mode,
@@ -522,7 +504,13 @@
 
   private State init(final String openidIdentifier, final SignInMode mode,
       final boolean remember, final String returnToken) {
-    final List<?> list = discoveryCache.get(openidIdentifier);
+    final List<?> list;
+    try {
+      list = manager.discover(openidIdentifier);
+    } catch (DiscoveryException e) {
+      log.error("Cannot discover OpenID " + openidIdentifier, e);
+      return null;
+    }
     if (list == null || list.isEmpty()) {
       return null;
     }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
index d3a9059..4521348 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
@@ -158,6 +158,10 @@
       w.write(HPD_ID + ".account=");
       json(((IdentifiedUser) user).getAccount(), w);
       w.write(";");
+      w.write(HPD_ID + ".accountDiffPref=");
+      json(((IdentifiedUser) user).getAccountDiffPreference(), w);
+      w.write(";");
+
       final byte[] userData = w.toString().getBytes("UTF-8");
 
       raw = concat(page.part1, userData, page.part2);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java
index b428c9a..120de30 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.httpd.rpc;
 
 import com.google.gerrit.common.errors.CorruptEntityException;
+import com.google.gerrit.common.errors.InvalidQueryException;
 import com.google.gerrit.common.errors.NoSuchEntityException;
 import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.reviewdb.ReviewDb;
@@ -64,6 +65,8 @@
       if (r != null) {
         callback.onSuccess(r);
       }
+    } catch (InvalidQueryException e) {
+      callback.onFailure(e);
     } catch (NoSuchProjectException e) {
       callback.onFailure(new NoSuchEntityException());
     } catch (NoSuchGroupException e) {
@@ -116,8 +119,9 @@
      *         {@link AsyncCallback#onFailure(Throwable)}.
      * @throws NoSuchProjectException
      * @throws NoSuchGroupException
+     * @throws InvalidQueryException
      */
     T run(ReviewDb db) throws OrmException, Failure, NoSuchProjectException,
-        NoSuchGroupException;
+        NoSuchGroupException, InvalidQueryException;
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/ChangeListServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/ChangeListServiceImpl.java
index d71a781..332e262 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/ChangeListServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/ChangeListServiceImpl.java
@@ -14,30 +14,29 @@
 
 package com.google.gerrit.httpd.rpc;
 
-import static com.google.gerrit.reviewdb.AccountExternalId.SCHEME_USERNAME;
-
 import com.google.gerrit.common.data.AccountDashboardInfo;
 import com.google.gerrit.common.data.ChangeInfo;
 import com.google.gerrit.common.data.ChangeListService;
 import com.google.gerrit.common.data.SingleListChangeInfo;
 import com.google.gerrit.common.data.ToggleStarRequest;
+import com.google.gerrit.common.errors.InvalidQueryException;
 import com.google.gerrit.common.errors.NoSuchEntityException;
 import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountExternalId;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.ChangeAccess;
-import com.google.gerrit.reviewdb.PatchLineComment;
-import com.google.gerrit.reviewdb.PatchSet;
 import com.google.gerrit.reviewdb.PatchSetApproval;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.RevId;
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.reviewdb.StarredChange;
-import com.google.gerrit.reviewdb.TrackingId;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountInfoCacheFactory;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeDataSource;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.ChangeQueryRewriter;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwtjsonrpc.client.VoidResult;
 import com.google.gwtorm.client.OrmException;
@@ -90,15 +89,22 @@
   private final ChangeControl.Factory changeControlFactory;
   private final AccountInfoCacheFactory.Factory accountInfoCacheFactory;
 
+  private final ChangeQueryBuilder.Factory queryBuilder;
+  private final Provider<ChangeQueryRewriter> queryRewriter;
+
   @Inject
   ChangeListServiceImpl(final Provider<ReviewDb> schema,
       final Provider<CurrentUser> currentUser,
       final ChangeControl.Factory changeControlFactory,
-      final AccountInfoCacheFactory.Factory accountInfoCacheFactory) {
+      final AccountInfoCacheFactory.Factory accountInfoCacheFactory,
+      final ChangeQueryBuilder.Factory queryBuilder,
+      final Provider<ChangeQueryRewriter> queryRewriter) {
     super(schema, currentUser);
     this.currentUser = currentUser;
     this.changeControlFactory = changeControlFactory;
     this.accountInfoCacheFactory = accountInfoCacheFactory;
+    this.queryBuilder = queryBuilder;
+    this.queryRewriter = queryRewriter;
   }
 
   private boolean canRead(final Change c) {
@@ -109,107 +115,13 @@
     }
   }
 
-  public void allOpenPrev(final String pos, final int pageSize,
-      final AsyncCallback<SingleListChangeInfo> callback) {
-    run(callback, new QueryPrev(pageSize, pos) {
-      @Override
-      ResultSet<Change> query(ReviewDb db, int slim, String sortKey)
-          throws OrmException {
-        return db.changes().allOpenPrev(sortKey, slim);
-      }
-    });
-  }
-
-  public void allOpenNext(final String pos, final int pageSize,
-      final AsyncCallback<SingleListChangeInfo> callback) {
-    run(callback, new QueryNext(pageSize, pos) {
-      @Override
-      ResultSet<Change> query(ReviewDb db, int slim, String sortKey)
-          throws OrmException {
-        return db.changes().allOpenNext(sortKey, slim);
-      }
-    });
-  }
-
-  public void byProjectOpenPrev(final Project.NameKey project,
-      final String pos, final int pageSize,
-      final AsyncCallback<SingleListChangeInfo> callback) {
-    run(callback, new QueryPrev(pageSize, pos) {
-      @Override
-      ResultSet<Change> query(ReviewDb db, int slim, String sortKey)
-          throws OrmException {
-        return db.changes().byProjectOpenPrev(project, sortKey, slim);
-      }
-    });
-  }
-
-  public void byProjectOpenNext(final Project.NameKey project,
-      final String pos, final int pageSize,
-      final AsyncCallback<SingleListChangeInfo> callback) {
-    run(callback, new QueryNext(pageSize, pos) {
-      @Override
-      ResultSet<Change> query(ReviewDb db, int slim, String sortKey)
-          throws OrmException {
-        return db.changes().byProjectOpenNext(project, sortKey, slim);
-      }
-    });
-  }
-
-  public void byProjectClosedPrev(final Project.NameKey project,
-      final Change.Status s, final String pos, final int pageSize,
-      final AsyncCallback<SingleListChangeInfo> callback) {
-    run(callback, new QueryPrev(pageSize, pos) {
-      @Override
-      ResultSet<Change> query(ReviewDb db, int slim, String sortKey)
-          throws OrmException {
-        return db.changes().byProjectClosedPrev(s.getCode(), project, sortKey,
-            slim);
-      }
-    });
-  }
-
-  public void byProjectClosedNext(final Project.NameKey project,
-      final Change.Status s, final String pos, final int pageSize,
-      final AsyncCallback<SingleListChangeInfo> callback) {
-    run(callback, new QueryNext(pageSize, pos) {
-      @Override
-      ResultSet<Change> query(ReviewDb db, int slim, String sortKey)
-          throws OrmException {
-        return db.changes().byProjectClosedNext(s.getCode(), project, sortKey,
-            slim);
-      }
-    });
-  }
-
-  public void allClosedPrev(final Change.Status s, final String pos,
-      final int pageSize, final AsyncCallback<SingleListChangeInfo> callback) {
-    run(callback, new QueryPrev(pageSize, pos) {
-      @Override
-      ResultSet<Change> query(ReviewDb db, int lim, String key)
-          throws OrmException {
-        return db.changes().allClosedPrev(s.getCode(), key, lim);
-      }
-    });
-  }
-
-  public void allClosedNext(final Change.Status s, final String pos,
-      final int pageSize, final AsyncCallback<SingleListChangeInfo> callback) {
-    run(callback, new QueryNext(pageSize, pos) {
-      @Override
-      ResultSet<Change> query(ReviewDb db, int lim, String key)
-          throws OrmException {
-        return db.changes().allClosedNext(s.getCode(), key, lim);
-      }
-    });
-  }
-
   @Override
   public void allQueryPrev(final String query, final String pos,
       final int pageSize, final AsyncCallback<SingleListChangeInfo> callback) {
     run(callback, new QueryPrev(pageSize, pos) {
       @Override
       ResultSet<Change> query(ReviewDb db, int lim, String key)
-          throws OrmException {
+          throws OrmException, InvalidQueryException {
         return searchQuery(db, query, lim, key, QUERY_PREV);
       }
     });
@@ -221,88 +133,70 @@
     run(callback, new QueryNext(pageSize, pos) {
       @Override
       ResultSet<Change> query(ReviewDb db, int lim, String key)
-          throws OrmException {
+          throws OrmException, InvalidQueryException {
         return searchQuery(db, query, lim, key, QUERY_NEXT);
       }
     });
   }
 
+  @SuppressWarnings("unchecked")
   private ResultSet<Change> searchQuery(final ReviewDb db, String query,
       final int limit, final String key, final Comparator<Change> cmp)
-      throws OrmException {
-    List<Change> result = new ArrayList<Change>();
-    final HashSet<Change.Id> want = new HashSet<Change.Id>();
-    query = query.trim();
+      throws OrmException, InvalidQueryException {
+    try {
+      final ChangeQueryBuilder builder = queryBuilder.create(currentUser.get());
+      final Predicate<ChangeData> visibleToMe = builder.is_visible();
+      Predicate<ChangeData> q = builder.parse(query);
+      q = Predicate.and(q, //
+          cmp == QUERY_PREV //
+              ? builder.sortkey_after(key) //
+              : builder.sortkey_before(key), //
+          builder.limit(limit), //
+          visibleToMe //
+          );
 
-    if (query.matches("^[1-9][0-9]*$")) {
-      want.add(Change.Id.parse(query));
-
-    } else if (query.matches("^[iI][0-9a-f]{4,}.*$")) {
-      if (query.startsWith("i")) {
-        query = "I" + query.substring(1);
-      }
-      final Change.Key a = new Change.Key(query);
-      final Change.Key b = a.max();
-      filterBySortKey(result, db.changes().byKeyRange(a, b), cmp, key);
-      Collections.sort(result, cmp);
-      if (limit < result.size()) {
-        result = result.subList(0, limit);
+      ChangeQueryRewriter rewriter = queryRewriter.get();
+      Predicate<ChangeData> s = rewriter.rewrite(q);
+      if (!(s instanceof ChangeDataSource)) {
+        s = rewriter.rewrite(Predicate.and(builder.status_open(), q));
       }
 
-    } else if (query.matches("^([0-9a-fA-F]{4," + RevId.LEN + "})$")) {
-      final RevId id = new RevId(query);
-      final ResultSet<PatchSet> patches;
-      if (id.isComplete()) {
-        patches = db.patchSets().byRevision(id);
+      if (s instanceof ChangeDataSource) {
+        ArrayList<Change> r = new ArrayList();
+        HashSet<Change.Id> want = new HashSet<Change.Id>();
+        for (ChangeData d : ((ChangeDataSource) s).read()) {
+          if (d.hasChange()) {
+            // Checking visibleToMe here should be unnecessary, the
+            // query should have already performed it.  But we don't
+            // want to trust the query rewriter that much yet.
+            //
+            if (visibleToMe.match(d)) {
+              r.add(d.getChange());
+            }
+          } else {
+            want.add(d.getId());
+          }
+        }
+
+        // Here we have to check canRead. Its impossible to
+        // do that test without the change object, and it being
+        // missing above means we have to compute it ourselves.
+        //
+        if (!want.isEmpty()) {
+          for (Change c : db.changes().get(want)) {
+            if (canRead(c)) {
+              r.add(c);
+            }
+          }
+        }
+
+        Collections.sort(r, cmp);
+        return new ListResultSet<Change>(r);
       } else {
-        patches = db.patchSets().byRevisionRange(id, id.max());
+        throw new InvalidQueryException("Not Supported", s.toString());
       }
-      for (PatchSet p : patches) {
-        want.add(p.getId().getParentKey());
-      }
-    } else if (query.contains("owner:")) {
-      String[] parsedQuery = query.split(":");
-      if (parsedQuery.length > 1) {
-        filterBySortKey(result, changesCreatedBy(db, parsedQuery[1]), cmp, key);
-      }
-    } else if (query.contains("reviewer:")) {
-      String[] parsedQuery = query.split(":");
-      if (parsedQuery.length > 1) {
-        want.addAll(changesReviewedBy(db, parsedQuery[1]));
-      }
-    } else if (query.contains("bug:") || query.contains("tr:")) {
-      String[] parsedQuery = query.split(":");
-      if (parsedQuery.length > 1) {
-        want.addAll(changesReferencingTr(db, parsedQuery[1]));
-      }
-    }
-
-    if (result.isEmpty() && want.isEmpty()) {
-      return new ListResultSet<Change>(Collections.<Change> emptyList());
-    }
-
-    filterBySortKey(result, db.changes().get(want), cmp, key);
-    Collections.sort(result, cmp);
-    if (limit < result.size()) {
-      result = result.subList(0, limit);
-    }
-    return new ListResultSet<Change>(result);
-  }
-
-  private static void filterBySortKey(final List<Change> dst,
-      final Iterable<Change> src, final Comparator<Change> cmp, final String key) {
-    if (cmp == QUERY_PREV) {
-      for (Change c : src) {
-        if (c.getSortKey().compareTo(key) > 0) {
-          dst.add(c);
-        }
-      }
-    } else /* cmp == QUERY_NEXT */{
-      for (Change c : src) {
-        if (c.getSortKey().compareTo(key) < 0) {
-          dst.add(c);
-        }
-      }
+    } catch (QueryParseException e) {
+      throw new InvalidQueryException(e.getMessage(), query);
     }
   }
 
@@ -362,45 +256,6 @@
     });
   }
 
-  public void myStarredChanges(
-      final AsyncCallback<SingleListChangeInfo> callback) {
-    run(callback, new Action<SingleListChangeInfo>() {
-      public SingleListChangeInfo run(final ReviewDb db) throws OrmException {
-        final AccountInfoCacheFactory ac = accountInfoCacheFactory.create();
-        final SingleListChangeInfo d = new SingleListChangeInfo();
-        final Set<Change.Id> starred = currentUser.get().getStarredChanges();
-        d.setChanges(filter(db.changes().get(starred), starred, ac));
-        Collections.sort(d.getChanges(), new Comparator<ChangeInfo>() {
-          public int compare(final ChangeInfo o1, final ChangeInfo o2) {
-            return o1.getLastUpdatedOn().compareTo(o2.getLastUpdatedOn());
-          }
-        });
-        d.setAccounts(ac.create());
-        return d;
-      }
-    });
-  }
-
-  public void myDraftChanges(final AsyncCallback<SingleListChangeInfo> callback) {
-    run(callback, new Action<SingleListChangeInfo>() {
-      public SingleListChangeInfo run(final ReviewDb db) throws OrmException {
-        final Account.Id me = getAccountId();
-        final AccountInfoCacheFactory ac = accountInfoCacheFactory.create();
-        final SingleListChangeInfo d = new SingleListChangeInfo();
-        final Set<Change.Id> starred = currentUser.get().getStarredChanges();
-        final Set<Change.Id> drafted = draftedBy(db, me);
-        d.setChanges(filter(db.changes().get(drafted), starred, ac));
-        Collections.sort(d.getChanges(), new Comparator<ChangeInfo>() {
-          public int compare(final ChangeInfo o1, final ChangeInfo o2) {
-            return o1.getLastUpdatedOn().compareTo(o2.getLastUpdatedOn());
-          }
-        });
-        d.setAccounts(ac.create());
-        return d;
-      }
-    });
-  }
-
   public void toggleStars(final ToggleStarRequest req,
       final AsyncCallback<VoidResult> callback) {
     run(callback, new Action<VoidResult>() {
@@ -449,102 +304,6 @@
     return r;
   }
 
-  private static Set<Change.Id> draftedBy(final ReviewDb db, final Account.Id me)
-      throws OrmException {
-    final Set<Change.Id> existing = new HashSet<Change.Id>();
-    if (me != null) {
-      for (final PatchLineComment sc : db.patchComments().draftByAuthor(me)) {
-        final Change.Id c =
-            sc.getKey().getParentKey().getParentKey().getParentKey();
-        existing.add(c);
-      }
-    }
-    return existing;
-  }
-
-  /**
-   * @return a set of all the account ID's matching the given user name in
-   *         either of the following columns: ssh name, email address, full name
-   */
-  private static Set<Account.Id> getAccountSources(final ReviewDb db,
-      final String userName) throws OrmException {
-    Set<Account.Id> result = new HashSet<Account.Id>();
-    String a = userName;
-    String b = userName + "\u9fa5";
-    addAll(result, db.accounts().suggestByFullName(a, b, 10));
-    for (AccountExternalId extId : db.accountExternalIds().suggestByKey(
-        new AccountExternalId.Key(SCHEME_USERNAME, a),
-        new AccountExternalId.Key(SCHEME_USERNAME, b), 10)) {
-      result.add(extId.getAccountId());
-    }
-    for (AccountExternalId extId : db.accountExternalIds()
-        .suggestByEmailAddress(a, b, 10)) {
-      result.add(extId.getAccountId());
-    }
-    return result;
-  }
-
-  private static void addAll(Set<Account.Id> result, ResultSet<Account> rs) {
-    for (Account account : rs) {
-      result.add(account.getId());
-    }
-  }
-
-  /**
-   * @return a set of all the changes created by userName. This method tries to
-   *         find userName in 1) the ssh user names, 2) the full names and 3)
-   *         the email addresses. The returned changes are unique and sorted by
-   *         time stamp, newer first.
-   */
-  private List<Change> changesCreatedBy(final ReviewDb db, final String userName)
-      throws OrmException {
-    final List<Change> resultChanges = new ArrayList<Change>();
-    for (Account.Id account : getAccountSources(db, userName)) {
-      for (Change change : db.changes().byOwnerOpen(account)) {
-        resultChanges.add(change);
-      }
-      for (Change change : db.changes().byOwnerClosedAll(account)) {
-        resultChanges.add(change);
-      }
-    }
-    return resultChanges;
-  }
-
-  /**
-   * @return a set of all the changes reviewed by userName. This method tries to
-   *         find userName in 1) the ssh user names, 2) the full names and the
-   *         email addresses. The returned changes are unique and sorted by time
-   *         stamp, newer first.
-   */
-  private Set<Change.Id> changesReviewedBy(final ReviewDb db,
-      final String userName) throws OrmException {
-    final Set<Change.Id> resultChanges = new HashSet<Change.Id>();
-    for (Account.Id account : getAccountSources(db, userName)) {
-      for (PatchSetApproval a : db.patchSetApprovals().openByUser(account)) {
-        resultChanges.add(a.getPatchSetId().getParentKey());
-      }
-      for (PatchSetApproval a : db.patchSetApprovals().closedByUserAll(account)) {
-        resultChanges.add(a.getPatchSetId().getParentKey());
-      }
-    }
-    return resultChanges;
-  }
-
-  /**
-   * @return a set of all the changes referencing tracking id. This method find
-   *         all changes with a reference to the given external tracking id.
-   *         The returned changes are unique and sorted by time stamp, newer first.
-   */
-  private Set<Change.Id> changesReferencingTr(final ReviewDb db,
-      final String trackingId) throws OrmException {
-    final Set<Change.Id> resultChanges = new HashSet<Change.Id>();
-    for (final TrackingId tr : db.trackingIds().byTrackingId(
-        new TrackingId.Id(trackingId))) {
-      resultChanges.add(tr.getChangeId());
-    }
-    return resultChanges;
-  }
-
   private abstract class QueryNext implements Action<SingleListChangeInfo> {
     protected final String pos;
     protected final int limit;
@@ -556,30 +315,22 @@
       this.slim = limit + 1;
     }
 
-    public SingleListChangeInfo run(final ReviewDb db) throws OrmException {
+    public SingleListChangeInfo run(final ReviewDb db) throws OrmException,
+        InvalidQueryException {
       final AccountInfoCacheFactory ac = accountInfoCacheFactory.create();
       final SingleListChangeInfo d = new SingleListChangeInfo();
       final Set<Change.Id> starred = currentUser.get().getStarredChanges();
 
-      boolean results = true;
-      String sortKey = pos;
       final ArrayList<ChangeInfo> list = new ArrayList<ChangeInfo>();
-      while (results && list.size() < slim) {
-        results = false;
-        final ResultSet<Change> rs = query(db, slim, sortKey);
-        for (final Change c : rs) {
-          results = true;
-          if (canRead(c)) {
-            final ChangeInfo ci = new ChangeInfo(c);
-            ac.want(ci.getOwner());
-            ci.setStarred(starred.contains(ci.getId()));
-            list.add(ci);
-            if (list.size() == slim) {
-              rs.close();
-              break;
-            }
-          }
-          sortKey = c.getSortKey();
+      final ResultSet<Change> rs = query(db, slim, pos);
+      for (final Change c : rs) {
+        final ChangeInfo ci = new ChangeInfo(c);
+        ac.want(ci.getOwner());
+        ci.setStarred(starred.contains(ci.getId()));
+        list.add(ci);
+        if (list.size() == slim) {
+          rs.close();
+          break;
         }
       }
 
@@ -598,7 +349,7 @@
     }
 
     abstract ResultSet<Change> query(final ReviewDb db, final int slim,
-        String sortKey) throws OrmException;
+        String sortKey) throws OrmException, InvalidQueryException;
   }
 
   private abstract class QueryPrev extends QueryNext {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
index 11c2d9d..77662a1 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
@@ -37,6 +37,7 @@
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.ChangeUserName;
+import com.google.gerrit.server.account.ClearPassword;
 import com.google.gerrit.server.account.GeneratePassword;
 import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.config.AuthConfig;
@@ -75,6 +76,7 @@
   private final AccountManager accountManager;
   private final boolean useContactInfo;
 
+  private final ClearPassword.Factory clearPasswordFactory;
   private final GeneratePassword.Factory generatePasswordFactory;
   private final ChangeUserName.CurrentUser changeUserNameFactory;
   private final DeleteExternalIds.Factory deleteExternalIdsFactory;
@@ -88,6 +90,7 @@
       final RegisterNewEmailSender.Factory esf, final SshKeyCache skc,
       final AccountByEmailCache abec, final AccountCache uac,
       final AccountManager am,
+      final ClearPassword.Factory clearPasswordFactory,
       final GeneratePassword.Factory generatePasswordFactory,
       final ChangeUserName.CurrentUser changeUserNameFactory,
       final DeleteExternalIds.Factory deleteExternalIdsFactory,
@@ -106,6 +109,7 @@
 
     useContactInfo = contactStore != null && contactStore.isEnabled();
 
+    this.clearPasswordFactory = clearPasswordFactory;
     this.generatePasswordFactory = generatePasswordFactory;
     this.changeUserNameFactory = changeUserNameFactory;
     this.deleteExternalIdsFactory = deleteExternalIdsFactory;
@@ -183,6 +187,12 @@
     Handler.wrap(generatePasswordFactory.create(key)).to(callback);
   }
 
+  @Override
+  public void clearPassword(AccountExternalId.Key key,
+      AsyncCallback<AccountExternalId> callback) {
+    Handler.wrap(clearPasswordFactory.create(key)).to(callback);
+  }
+
   public void myExternalIds(AsyncCallback<List<AccountExternalId>> callback) {
     externalIdDetailFactory.create().to(callback);
   }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java
index 9c1f530..4fd8e28 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java
@@ -17,9 +17,11 @@
 import com.google.gerrit.common.data.AccountProjectWatchInfo;
 import com.google.gerrit.common.data.AccountService;
 import com.google.gerrit.common.data.AgreementInfo;
+import com.google.gerrit.common.errors.InvalidQueryException;
 import com.google.gerrit.common.errors.NoSuchEntityException;
 import com.google.gerrit.httpd.rpc.BaseServiceImplementation;
 import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.AccountDiffPreference;
 import com.google.gerrit.reviewdb.AccountGeneralPreferences;
 import com.google.gerrit.reviewdb.AccountProjectWatch;
 import com.google.gerrit.reviewdb.Project;
@@ -28,8 +30,11 @@
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwtjsonrpc.client.VoidResult;
+import com.google.gwtorm.client.OrmDuplicateKeyException;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -46,24 +51,37 @@
   private final AccountCache accountCache;
   private final ProjectControl.Factory projectControlFactory;
   private final AgreementInfoFactory.Factory agreementInfoFactory;
+  private final ChangeQueryBuilder.Factory queryBuilder;
 
   @Inject
   AccountServiceImpl(final Provider<ReviewDb> schema,
       final Provider<IdentifiedUser> identifiedUser,
       final AccountCache accountCache,
       final ProjectControl.Factory projectControlFactory,
-      final AgreementInfoFactory.Factory agreementInfoFactory) {
+      final AgreementInfoFactory.Factory agreementInfoFactory,
+      final ChangeQueryBuilder.Factory queryBuilder) {
     super(schema, identifiedUser);
     this.currentUser = identifiedUser;
     this.accountCache = accountCache;
     this.projectControlFactory = projectControlFactory;
     this.agreementInfoFactory = agreementInfoFactory;
+    this.queryBuilder = queryBuilder;
   }
 
   public void myAccount(final AsyncCallback<Account> callback) {
     callback.onSuccess(currentUser.get().getAccount());
   }
 
+  @Override
+  public void myDiffPreferences(AsyncCallback<AccountDiffPreference> callback) {
+    run(callback, new Action<AccountDiffPreference>() {
+      @Override
+      public AccountDiffPreference run(ReviewDb db) throws OrmException {
+        return currentUser.get().getAccountDiffPreference();
+      }
+    });
+  }
+
   public void changePreferences(final AccountGeneralPreferences pref,
       final AsyncCallback<VoidResult> callback) {
     run(callback, new Action<VoidResult>() {
@@ -80,6 +98,23 @@
     });
   }
 
+  @Override
+  public void changeDiffPreferences(final AccountDiffPreference diffPref,
+      AsyncCallback<VoidResult> callback) {
+    run(callback, new Action<VoidResult>(){
+      public VoidResult run(ReviewDb db) throws OrmException {
+        Account.Id accountId = getAccountId();
+        if (!diffPref.getAccountId().equals(getAccountId())) {
+          throw new IllegalArgumentException("diffPref.getAccountId() "
+              + diffPref.getAccountId() + " doesn't match"
+              + " the accountId of the signed in user " + getAccountId());
+        }
+        db.accountDiffPreferences().upsert(Collections.singleton(diffPref));
+        return VoidResult.INSTANCE;
+      }
+    });
+  }
+
   public void myProjectWatch(
       final AsyncCallback<List<AccountProjectWatchInfo>> callback) {
     run(callback, new Action<List<AccountProjectWatchInfo>>() {
@@ -109,19 +144,33 @@
     });
   }
 
-  public void addProjectWatch(final String projectName,
+  public void addProjectWatch(final String projectName, final String filter,
       final AsyncCallback<AccountProjectWatchInfo> callback) {
     run(callback, new Action<AccountProjectWatchInfo>() {
       public AccountProjectWatchInfo run(ReviewDb db) throws OrmException,
-          NoSuchProjectException {
+          NoSuchProjectException, InvalidQueryException {
         final Project.NameKey nameKey = new Project.NameKey(projectName);
         final ProjectControl ctl = projectControlFactory.validateFor(nameKey);
 
-        final AccountProjectWatch watch =
-            new AccountProjectWatch(
-                new AccountProjectWatch.Key(((IdentifiedUser) ctl
-                    .getCurrentUser()).getAccountId(), nameKey));
-        db.accountProjectWatches().insert(Collections.singleton(watch));
+        if (filter != null) {
+          try {
+            ChangeQueryBuilder builder = queryBuilder.create(currentUser.get());
+            builder.setAllowFile(true);
+            builder.parse(filter);
+          } catch (QueryParseException badFilter) {
+            throw new InvalidQueryException(badFilter.getMessage(), filter);
+          }
+        }
+
+        AccountProjectWatch watch =
+            new AccountProjectWatch(new AccountProjectWatch.Key(
+                ((IdentifiedUser) ctl.getCurrentUser()).getAccountId(),
+                nameKey, filter));
+        try {
+          db.accountProjectWatches().insert(Collections.singleton(watch));
+        } catch (OrmDuplicateKeyException alreadyHave) {
+          watch = db.accountProjectWatches().get(watch.getKey());
+        }
         return new AccountProjectWatchInfo(watch, ctl.getProject());
       }
     });
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChange.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChange.java
index 5f0851e..4a4d9d1 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChange.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChange.java
@@ -127,7 +127,6 @@
       // Email the reviewers
       final AbandonedSender cm = abandonedSenderFactory.create(change);
       cm.setFrom(currentUser.getAccountId());
-      cm.setReviewDb(db);
       cm.setChangeMessage(cmsg);
       cm.send();
     }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/SubmitAction.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/SubmitAction.java
index 8e64469..4ab9072 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/SubmitAction.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/SubmitAction.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.MergeOp;
 import com.google.gerrit.server.git.MergeQueue;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.CanSubmitResult;
@@ -44,8 +45,8 @@
   private final FunctionState.Factory functionState;
   private final IdentifiedUser user;
   private final ChangeDetailFactory.Factory changeDetailFactory;
-  @Inject
-  private ChangeControl.Factory changeControlFactory;
+  private final ChangeControl.Factory changeControlFactory;
+  private final MergeOp.Factory opFactory;
 
   private final PatchSet.Id patchSetId;
 
@@ -53,13 +54,17 @@
   SubmitAction(final ReviewDb db, final MergeQueue mq, final ApprovalTypes at,
       final FunctionState.Factory fs, final IdentifiedUser user,
       final ChangeDetailFactory.Factory changeDetailFactory,
+      final ChangeControl.Factory changeControlFactory,
+      final MergeOp.Factory opFactory,
       @Assisted final PatchSet.Id patchSetId) {
     this.db = db;
     this.merger = mq;
     this.approvalTypes = at;
     this.functionState = fs;
     this.user = user;
+    this.changeControlFactory = changeControlFactory;
     this.changeDetailFactory = changeDetailFactory;
+    this.opFactory = opFactory;
 
     this.patchSetId = patchSetId;
   }
@@ -76,7 +81,7 @@
     CanSubmitResult err =
         changeControl.canSubmit(patchSetId, db, approvalTypes, functionState);
     if (err == CanSubmitResult.OK) {
-      ChangeUtil.submit(patchSetId, user, db, merger);
+      ChangeUtil.submit(opFactory, patchSetId, user, db, merger);
       return changeDetailFactory.create(changeId).call();
     } else {
       throw new IllegalStateException(err.getMessage());
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 d04f4ef..ae36388 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
@@ -130,7 +130,6 @@
     final AddReviewerSender cm;
     cm = addReviewerSenderFactory.create(control.getChange());
     cm.setFrom(currentUser.getAccountId());
-    cm.setReviewDb(db);
     cm.addReviewers(added);
     cm.send();
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java
index b0ee4d9..da52596 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java
@@ -20,11 +20,11 @@
 import com.google.gerrit.common.data.ApprovalTypes;
 import com.google.gerrit.common.data.PatchDetailService;
 import com.google.gerrit.common.data.PatchScript;
-import com.google.gerrit.common.data.PatchScriptSettings;
 import com.google.gerrit.common.errors.NoSuchEntityException;
 import com.google.gerrit.httpd.rpc.BaseServiceImplementation;
 import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.AccountDiffPreference;
 import com.google.gerrit.reviewdb.AccountPatchReview;
 import com.google.gerrit.reviewdb.ApprovalCategory;
 import com.google.gerrit.reviewdb.ApprovalCategoryValue;
@@ -92,13 +92,13 @@
   }
 
   public void patchScript(final Patch.Key patchKey, final PatchSet.Id psa,
-      final PatchSet.Id psb, final PatchScriptSettings s,
+      final PatchSet.Id psb, final AccountDiffPreference dp,
       final AsyncCallback<PatchScript> callback) {
     if (psb == null) {
       callback.onFailure(new NoSuchEntityException());
       return;
     }
-    patchScriptFactoryFactory.create(patchKey, psa, psb, s).to(callback);
+    patchScriptFactoryFactory.create(patchKey, psa, psb, dp).to(callback);
   }
 
   public void saveDraft(final PatchLineComment comment,
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 bca7599..a58c7b9 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
@@ -16,15 +16,14 @@
 
 import com.google.gerrit.common.data.CommentDetail;
 import com.google.gerrit.common.data.PatchScript;
-import com.google.gerrit.common.data.PatchScriptSettings;
 import com.google.gerrit.common.data.PatchScript.DisplayMethod;
-import com.google.gerrit.common.data.PatchScriptSettings.Whitespace;
 import com.google.gerrit.prettify.common.EditList;
 import com.google.gerrit.prettify.common.SparseFileContent;
-import com.google.gerrit.reviewdb.AccountGeneralPreferences;
+import com.google.gerrit.reviewdb.AccountDiffPreference;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.Patch;
 import com.google.gerrit.reviewdb.PatchLineComment;
+import com.google.gerrit.reviewdb.AccountDiffPreference.Whitespace;
 import com.google.gerrit.reviewdb.Patch.PatchType;
 import com.google.gerrit.server.FileTypeRegistry;
 import com.google.gerrit.server.patch.PatchListEntry;
@@ -66,7 +65,7 @@
 
   private Repository db;
   private Change change;
-  private PatchScriptSettings settings;
+  private AccountDiffPreference diffPrefs;
   private ObjectId aId;
   private ObjectId bId;
 
@@ -92,11 +91,11 @@
     this.change = c;
   }
 
-  void setSettings(final PatchScriptSettings s) {
-    settings = s;
+  void setDiffPrefs(final AccountDiffPreference dp) {
+    diffPrefs = dp;
 
-    context = settings.getContext();
-    if (context == AccountGeneralPreferences.WHOLE_FILE_CONTEXT) {
+    context = diffPrefs.getContext();
+    if (context == AccountDiffPreference.WHOLE_FILE_CONTEXT) {
       context = MAX_CONTEXT;
     } else if (context > MAX_CONTEXT) {
       context = MAX_CONTEXT;
@@ -117,7 +116,7 @@
       //
       return new PatchScript(change.getKey(), content.getChangeType(), content
           .getOldName(), content.getNewName(), content.getHeaderLines(),
-          settings, a.dst, b.dst, Collections.<Edit> emptyList(),
+          diffPrefs, a.dst, b.dst, Collections.<Edit> emptyList(),
           a.displayMethod, b.displayMethod, comments, history, false, false);
     }
 
@@ -150,24 +149,24 @@
         // IF the file is really large, we disable things to avoid choking
         // the browser client.
         //
-        settings.setContext(Math.min(25, context));
-        settings.getPrettySettings().setSyntaxHighlighting(false);
-        context = settings.getContext();
+        diffPrefs.setContext((short) Math.min(25, context));
+        diffPrefs.setSyntaxHighlighting(false);
+        context = diffPrefs.getContext();
         hugeFile = true;
 
-      } else if (settings.getPrettySettings().isSyntaxHighlighting()) {
+      } else if (diffPrefs.isSyntaxHighlighting()) {
         // In order to syntax highlight the file properly we need to
         // give the client the complete file contents. So force our
         // context temporarily to the complete file size.
         //
         context = MAX_CONTEXT;
       }
-      packContent(settings.getWhitespace() != Whitespace.IGNORE_NONE);
+      packContent(diffPrefs.getIgnoreWhitespace() != Whitespace.IGNORE_NONE);
     }
 
     return new PatchScript(change.getKey(), content.getChangeType(), content
         .getOldName(), content.getNewName(), content.getHeaderLines(),
-        settings, a.dst, b.dst, edits, a.displayMethod, b.displayMethod,
+        diffPrefs, a.dst, b.dst, edits, a.displayMethod, b.displayMethod,
         comments, history, hugeFile, intralineDifference);
   }
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptFactory.java
index bfc2074..ee82418 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptFactory.java
@@ -16,16 +16,16 @@
 
 import com.google.gerrit.common.data.CommentDetail;
 import com.google.gerrit.common.data.PatchScript;
-import com.google.gerrit.common.data.PatchScriptSettings;
-import com.google.gerrit.common.data.PatchScriptSettings.Whitespace;
 import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.AccountDiffPreference;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.Patch;
 import com.google.gerrit.reviewdb.PatchLineComment;
 import com.google.gerrit.reviewdb.PatchSet;
 import com.google.gerrit.reviewdb.Project;
 import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.AccountDiffPreference.Whitespace;
 import com.google.gerrit.reviewdb.Patch.ChangeType;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
@@ -62,7 +62,7 @@
     PatchScriptFactory create(Patch.Key patchKey,
         @Assisted("patchSetA") PatchSet.Id patchSetA,
         @Assisted("patchSetB") PatchSet.Id patchSetB,
-        PatchScriptSettings settings);
+        AccountDiffPreference diffPrefs);
   }
 
   private static final Logger log =
@@ -79,7 +79,7 @@
   @Nullable
   private final PatchSet.Id psa;
   private final PatchSet.Id psb;
-  private final PatchScriptSettings settings;
+  private final AccountDiffPreference diffPrefs;
 
   private final PatchSet.Id patchSetId;
   private final Change.Id changeId;
@@ -102,7 +102,7 @@
       @Assisted final Patch.Key patchKey,
       @Assisted("patchSetA") @Nullable final PatchSet.Id patchSetA,
       @Assisted("patchSetB") final PatchSet.Id patchSetB,
-      @Assisted final PatchScriptSettings settings) {
+      @Assisted final AccountDiffPreference diffPrefs) {
     this.repoManager = grm;
     this.builderFactory = builderFactory;
     this.patchListCache = patchListCache;
@@ -113,7 +113,7 @@
     this.patchKey = patchKey;
     this.psa = patchSetA;
     this.psb = patchSetB;
-    this.settings = settings;
+    this.diffPrefs = diffPrefs;
 
     patchSetId = patchKey.getParentKey();
     changeId = patchSetId.getParentKey();
@@ -143,7 +143,7 @@
       throw new NoSuchChangeException(changeId, e);
     }
     try {
-      final PatchList list = listFor(keyFor(settings.getWhitespace()));
+      final PatchList list = listFor(keyFor(diffPrefs.getIgnoreWhitespace()));
       final boolean intraline = list.hasIntralineDifference();
       final PatchScriptBuilder b = newBuilder(list, git);
       final PatchListEntry content = list.get(patchKey.getFileName());
@@ -172,11 +172,11 @@
   }
 
   private PatchScriptBuilder newBuilder(final PatchList list, Repository git) {
-    final PatchScriptSettings s = new PatchScriptSettings(settings);
+    final AccountDiffPreference dp = new AccountDiffPreference(diffPrefs);
     final PatchScriptBuilder b = builderFactory.get();
     b.setRepository(git);
     b.setChange(change);
-    b.setSettings(s);
+    b.setDiffPrefs(dp);
     b.setTrees(list.getOldId(), list.getNewId());
     return b;
   }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddRefRight.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddRefRight.java
index 06f15c22..3c2c93f 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddRefRight.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddRefRight.java
@@ -144,17 +144,34 @@
     while (refPattern.startsWith("/")) {
       refPattern = refPattern.substring(1);
     }
-    if (!refPattern.startsWith(Constants.R_REFS)) {
-      refPattern = Constants.R_HEADS + refPattern;
-    }
-    if (refPattern.endsWith("/*")) {
-      final String prefix = refPattern.substring(0, refPattern.length() - 2);
-      if (!"refs".equals(prefix) && !Repository.isValidRefName(prefix)) {
+
+    if (refPattern.startsWith(RefRight.REGEX_PREFIX)) {
+      String example = RefControl.shortestExample(refPattern);
+
+      if (!example.startsWith(Constants.R_REFS)) {
+        refPattern = RefRight.REGEX_PREFIX + Constants.R_HEADS
+                + refPattern.substring(RefRight.REGEX_PREFIX.length());
+        example = RefControl.shortestExample(refPattern);
+      }
+
+      if (!Repository.isValidRefName(example)) {
         throw new InvalidNameException();
       }
+
     } else {
-      if (!Repository.isValidRefName(refPattern)) {
-        throw new InvalidNameException();
+      if (!refPattern.startsWith(Constants.R_REFS)) {
+        refPattern = Constants.R_HEADS + refPattern;
+      }
+
+      if (refPattern.endsWith("/*")) {
+        final String prefix = refPattern.substring(0, refPattern.length() - 2);
+        if (!"refs".equals(prefix) && !Repository.isValidRefName(prefix)) {
+          throw new InvalidNameException();
+        }
+      } else {
+        if (!Repository.isValidRefName(refPattern)) {
+          throw new InvalidNameException();
+        }
       }
     }
 
@@ -162,7 +179,7 @@
       refPattern = "-" + refPattern;
     }
 
-    if (!controlForRef(projectControl, refPattern).isOwner()) {
+    if (!projectControl.controlForRef(refPattern).isOwner()) {
       throw new NoSuchRefException(refPattern);
     }
 
@@ -187,11 +204,4 @@
     projectCache.evictAll();
     return projectDetailFactory.create(projectName).call();
   }
-
-  private RefControl controlForRef(ProjectControl p, String ref) {
-    if (ref.endsWith("/*")) {
-      ref = ref.substring(0, ref.length() - 1);
-    }
-    return p.controlForRef(ref);
-  }
 }
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 db179ed..92154c4 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
@@ -71,7 +71,7 @@
       if (!projectName.equals(k.getProjectNameKey())) {
         throw new IllegalArgumentException("All keys must be from same project");
       }
-      if (!controlForRef(projectControl, k.getRefPattern()).isOwner()) {
+      if (!projectControl.controlForRef(k.getRefPattern()).isOwner()) {
         throw new NoSuchRefException(k.getRefPattern());
       }
     }
@@ -85,11 +85,4 @@
     projectCache.evictAll();
     return projectDetailFactory.create(projectName).call();
   }
-
-  private RefControl controlForRef(ProjectControl p, String ref) {
-    if (ref.endsWith("/*")) {
-      ref = ref.substring(0, ref.length() - 1);
-    }
-    return p.controlForRef(ref);
-  }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java
index cdd535b..3ff3892f 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java
@@ -26,7 +26,6 @@
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.server.project.RefControl;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -77,7 +76,7 @@
 
     for (final RefRight r : projectState.getInheritedRights()) {
       InheritedRefRight refRight = new InheritedRefRight(
-          r, true, controlForRef(pc, r.getRefPattern()).isOwner());
+          r, true, pc.controlForRef(r.getRefPattern()).isOwner());
       if (!refRights.contains(refRight)) {
         refRights.add(refRight);
         wantGroup(r.getAccountGroupId());
@@ -86,7 +85,7 @@
 
     for (final RefRight r : projectState.getLocalRights()) {
       refRights.add(new InheritedRefRight(
-          r, false, controlForRef(pc, r.getRefPattern()).isOwner()));
+          r, false, pc.controlForRef(r.getRefPattern()).isOwner()));
       wantGroup(r.getAccountGroupId());
     }
 
@@ -144,11 +143,4 @@
       groups.put(groupId, groupCache.get(groupId));
     }
   }
-
-  private RefControl controlForRef(ProjectControl p, String ref) {
-    if (ref.endsWith("/*")) {
-      ref = ref.substring(0, ref.length() - 1);
-    }
-    return p.controlForRef(ref);
-  }
 }
diff --git a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/rpc/project/ListBranchesTest.java b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/rpc/project/ListBranchesTest.java
index 4c482d4..e05f811 100644
--- a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/rpc/project/ListBranchesTest.java
+++ b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/rpc/project/ListBranchesTest.java
@@ -14,12 +14,12 @@
 
 package com.google.gerrit.httpd.rpc.project;
 
+import static org.easymock.EasyMock.createStrictMock;
 import static org.easymock.EasyMock.eq;
 import static org.easymock.EasyMock.expect;
 import static org.easymock.EasyMock.expectLastCall;
-import static org.easymock.classextension.EasyMock.createStrictMock;
-import static org.easymock.classextension.EasyMock.replay;
-import static org.easymock.classextension.EasyMock.verify;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
 import static org.eclipse.jgit.lib.Constants.HEAD;
 import static org.eclipse.jgit.lib.Constants.R_HEADS;
 import static org.eclipse.jgit.lib.Ref.Storage.LOOSE;
diff --git a/gerrit-launcher/pom.xml b/gerrit-launcher/pom.xml
index 27199bd..1000777 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.3</version>
+    <version>2.1.4-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-launcher</artifactId>
diff --git a/gerrit-main/pom.xml b/gerrit-main/pom.xml
index 9167c37..10cb14a 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.3</version>
+    <version>2.1.4-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-main</artifactId>
diff --git a/gerrit-patch-commonsnet/pom.xml b/gerrit-patch-commonsnet/pom.xml
index a83d411..df64d13 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.3</version>
+    <version>2.1.4-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-patch-commonsnet</artifactId>
diff --git a/gerrit-patch-commonsnet/src/main/java/org/apache/commons/net/smtp/AuthSMTPClient.java b/gerrit-patch-commonsnet/src/main/java/org/apache/commons/net/smtp/AuthSMTPClient.java
index e380df4..4db7de6 100644
--- a/gerrit-patch-commonsnet/src/main/java/org/apache/commons/net/smtp/AuthSMTPClient.java
+++ b/gerrit-patch-commonsnet/src/main/java/org/apache/commons/net/smtp/AuthSMTPClient.java
@@ -17,8 +17,6 @@
 import com.google.gerrit.util.ssl.BlindSSLSocketFactory;
 
 import org.apache.commons.codec.binary.Base64;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
@@ -26,20 +24,16 @@
 import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
 import java.util.Arrays;
-import java.util.HashSet;
 import java.util.List;
-import java.util.Set;
 
 import javax.crypto.Mac;
 import javax.crypto.spec.SecretKeySpec;
 import javax.net.ssl.SSLSocketFactory;
 
 public class AuthSMTPClient extends SMTPClient {
-  private static final Logger log = LoggerFactory.getLogger(AuthSMTPClient.class);
   private static final String UTF_8 = "UTF-8";
 
   private String authTypes;
-  private Set<String> allowedRcptTo;
 
   public AuthSMTPClient(final String charset) {
     super(charset);
@@ -68,45 +62,6 @@
     }
   }
 
-  public void setAllowRcpt(final String[] allowed) {
-    if (allowed != null && allowed.length > 0) {
-      if (allowedRcptTo == null) {
-        allowedRcptTo = new HashSet<String>();
-      }
-      for (final String addr : allowed) {
-        allowedRcptTo.add(addr);
-      }
-    }
-  }
-
-  @Override
-  public int rcpt(final String forwardPath) throws IOException {
-    if (allowRcpt(forwardPath)) {
-      return super.rcpt(forwardPath);
-    } else {
-      log.warn("Not emailing " + forwardPath + " (prohibited by allowrcpt)");
-      return SMTPReply.ACTION_OK;
-    }
-  }
-
-  private boolean allowRcpt(String addr) {
-    if (allowedRcptTo == null) {
-      return true;
-    }
-    if (addr.startsWith("<") && addr.endsWith(">")) {
-      addr = addr.substring(1, addr.length() - 1);
-    }
-    if (allowedRcptTo.contains(addr)) {
-      return true;
-    }
-    final int at = addr.indexOf('@');
-    if (at > 0) {
-      return allowedRcptTo.contains(addr.substring(at))
-          || allowedRcptTo.contains(addr.substring(at + 1));
-    }
-    return false;
-  }
-
   @Override
   public String[] getReplyStrings() {
     return _replyLines.toArray(new String[_replyLines.size()]);
diff --git a/gerrit-patch-jgit/pom.xml b/gerrit-patch-jgit/pom.xml
index a58ef5c..68a08c3 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.3</version>
+    <version>2.1.4-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-patch-jgit</artifactId>
diff --git a/gerrit-pgm/pom.xml b/gerrit-pgm/pom.xml
index cf05fc6..66c5349 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.3</version>
+    <version>2.1.4-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-pgm</artifactId>
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java
index c5a545d..30176ef 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java
@@ -111,7 +111,7 @@
     WorkQueue.Executor executor = getExecutor();
 
     if (cont.isInitial()) {
-      TaskThunk task = new TaskThunk(cont, req);
+      TaskThunk task = new TaskThunk(executor, cont, req);
       if (maxWait > 0) {
         cont.setTimeout(maxWait);
       }
@@ -163,13 +163,16 @@
   private final class TaskThunk implements CancelableRunnable,
       ContinuationListener {
 
+    private final WorkQueue.Executor executor;
     private final Continuation cont;
     private final String name;
     private final Object lock = new Object();
     private boolean done;
     private Thread worker;
 
-    TaskThunk(final Continuation cont, final HttpServletRequest req) {
+    TaskThunk(final WorkQueue.Executor executor, final Continuation cont,
+        final HttpServletRequest req) {
+      this.executor = executor;
       this.cont = cont;
       this.name = generateName(req);
     }
@@ -219,7 +222,6 @@
 
     @Override
     public void onTimeout(Continuation self) {
-      WorkQueue.Executor executor = getExecutor();
       executor.remove(this);
     }
 
diff --git a/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/LibrariesTest.java b/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/LibrariesTest.java
index 72f752b..e723463 100644
--- a/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/LibrariesTest.java
+++ b/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/LibrariesTest.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.pgm.init;
 
-import static org.easymock.classextension.EasyMock.createStrictMock;
-import static org.easymock.classextension.EasyMock.replay;
-import static org.easymock.classextension.EasyMock.verify;
+import static org.easymock.EasyMock.createStrictMock;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
 
 import com.google.gerrit.pgm.util.ConsoleUI;
 import com.google.gerrit.server.config.SitePaths;
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 5dc0cd3..72b02d5 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
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.pgm.init;
 
+import static org.easymock.EasyMock.createStrictMock;
 import static org.easymock.EasyMock.eq;
 import static org.easymock.EasyMock.expect;
-import static org.easymock.classextension.EasyMock.createStrictMock;
-import static org.easymock.classextension.EasyMock.replay;
-import static org.easymock.classextension.EasyMock.verify;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
 
 import com.google.gerrit.pgm.util.ConsoleUI;
 import com.google.gerrit.server.config.SitePaths;
diff --git a/gerrit-prettify/pom.xml b/gerrit-prettify/pom.xml
index efcd6d0..99eece6 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.3</version>
+    <version>2.1.4-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-prettify</artifactId>
@@ -45,6 +45,12 @@
     </dependency>
 
     <dependency>
+      <groupId>com.google.gerrit</groupId>
+      <artifactId>gerrit-reviewdb</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
       <groupId>com.google.gwt</groupId>
       <artifactId>gwt-user</artifactId>
       <scope>provided</scope>
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/PrettyFormatter.gwt.xml b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/PrettyFormatter.gwt.xml
index 93ce272..48591f8 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/PrettyFormatter.gwt.xml
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/PrettyFormatter.gwt.xml
@@ -1,19 +1,27 @@
 <!--
  Copyright (C) 2008 The Android Open Source Project
 
- Licensed under the Apache License, Version 2.0 (the "License");
+ Licensed under the Apache License, Version 2.0 (the 'License');
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at
 
  http://www.apache.org/licenses/LICENSE-2.0
 
  Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
+ distributed under the License is distributed on an 'AS IS' BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
 -->
 <module>
+  <replace-with class='com.google.gerrit.prettify.client.PrivateScopeImplIE6'>
+    <when-type-is class='com.google.gerrit.prettify.client.PrivateScopeImpl'/>
+    <any>
+      <when-property-is name="user.agent" value="ie6" />
+      <when-property-is name="user.agent" value="ie8" />
+    </any>
+  </replace-with>
+
   <inherits name='com.google.gwt.resources.Resources'/>
   <inherits name='com.google.gwtexpui.safehtml.SafeHtml'/>
   <source path='common' />
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/ClientSideFormatter.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/ClientSideFormatter.java
index 2cfb6ae..4bce954 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/ClientSideFormatter.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/ClientSideFormatter.java
@@ -16,7 +16,9 @@
 
 import com.google.gerrit.prettify.common.PrettyFactory;
 import com.google.gerrit.prettify.common.PrettyFormatter;
-import com.google.gwt.resources.client.TextResource;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.user.client.ui.RootPanel;
 
 /** Evaluates prettify using the host browser's JavaScript engine. */
 public class ClientSideFormatter extends PrettyFormatter {
@@ -27,37 +29,36 @@
     }
   };
 
+  private static final PrivateScopeImpl prettify;
+
   static {
     Resources.I.prettify_css().ensureInjected();
     Resources.I.gerrit_css().ensureInjected();
 
-    compile(Resources.I.core());
-    compile(Resources.I.lang_css());
-    compile(Resources.I.lang_hs());
-    compile(Resources.I.lang_lisp());
-    compile(Resources.I.lang_lua());
-    compile(Resources.I.lang_ml());
-    compile(Resources.I.lang_proto());
-    compile(Resources.I.lang_sql());
-    compile(Resources.I.lang_vb());
-    compile(Resources.I.lang_wiki());
-  }
+    prettify = GWT.create(PrivateScopeImpl.class);
+    RootPanel.get().add(prettify);
 
-  private static void compile(TextResource core) {
-    eval(core.getText());
+    prettify.compile(Resources.I.core());
+    prettify.compile(Resources.I.lang_css());
+    prettify.compile(Resources.I.lang_hs());
+    prettify.compile(Resources.I.lang_lisp());
+    prettify.compile(Resources.I.lang_lua());
+    prettify.compile(Resources.I.lang_ml());
+    prettify.compile(Resources.I.lang_proto());
+    prettify.compile(Resources.I.lang_sql());
+    prettify.compile(Resources.I.lang_vb());
+    prettify.compile(Resources.I.lang_wiki());
   }
 
-  private static native void eval(String js)
-  /*-{ eval(js); }-*/;
-
   @Override
   protected String prettify(String html, String type) {
-    return go(html, type, settings.getTabSize());
+    return go(prettify.getContext(), html, type, diffPrefs.getTabSize());
   }
 
-  private static native String go(String srcText, String srcType, int tabSize)
+  private static native String go(JavaScriptObject ctx, String srcText,
+      String srcType, int tabSize)
   /*-{
-     window['PR_TAB_WIDTH'] = tabSize;
-     return window.prettyPrintOne(srcText, srcType);
+     ctx.PR_TAB_WIDTH = tabSize;
+     return ctx.prettyPrintOne(srcText, srcType);
   }-*/;
 }
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrivateScopeImpl.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrivateScopeImpl.java
new file mode 100644
index 0000000..65ee212
--- /dev/null
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrivateScopeImpl.java
@@ -0,0 +1,67 @@
+// 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.prettify.client;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.resources.client.TextResource;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.NamedFrame;
+
+/**
+ * Creates a private JavaScript environment, typically inside an IFrame.
+ * <p>
+ * Instances must be created through {@code GWT.create(PrivateScopeImpl.class)}.
+ * A scope must remain attached to the primary document for its entire life.
+ * Behavior is undefined if a scope is detached and attached again later. It is
+ * best to attach the scope with {@code RootPanel.get().add(scope)} as soon as
+ * it has been created.
+ */
+public class PrivateScopeImpl extends Composite {
+  private static int scopeId;
+
+  protected final String scopeName;
+
+  public PrivateScopeImpl() {
+    scopeName = nextScopeName();
+
+    NamedFrame frame = new NamedFrame(scopeName);
+    frame.setUrl("javascript:''");
+    initWidget(frame);
+
+    setVisible(false);
+  }
+
+  public void compile(TextResource js) {
+    eval(js.getText());
+  }
+
+  public void eval(String js) {
+    nativeEval(getContext(), js);
+  }
+
+  public JavaScriptObject getContext() {
+    return nativeGetContext(scopeName);
+  }
+
+  private static String nextScopeName() {
+    return "_PrivateScope" + (++scopeId);
+  }
+
+  private static native void nativeEval(JavaScriptObject ctx, String js)
+  /*-{ ctx.eval(js); }-*/;
+
+  private static native JavaScriptObject nativeGetContext(String scopeName)
+  /*-{ return $wnd[scopeName]; }-*/;
+}
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrivateScopeImplIE6.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrivateScopeImplIE6.java
new file mode 100644
index 0000000..abb4e15
--- /dev/null
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrivateScopeImplIE6.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.prettify.client;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+/** IE6 requires us to initialize the document before we can use it. */
+public class PrivateScopeImplIE6 extends PrivateScopeImpl {
+  private JavaScriptObject context;
+
+  @Override
+  protected void onAttach() {
+    super.onAttach();
+    context = nativeInitContext(scopeName);
+  }
+
+  @Override
+  public JavaScriptObject getContext() {
+    return context;
+  }
+
+  private static native JavaScriptObject nativeInitContext(String scopeName)
+  /*-{
+    var fe = $wnd[scopeName];
+    fe.document.write(
+        '<script>'
+      + 'parent._PrivateScopeNewChild = this;'
+      + '</' + 'script>'
+    );
+    var ctx = $wnd._PrivateScopeNewChild;
+    $wnd._PrivateScopeNewChild = undefined;
+    return ctx;
+  }-*/;
+}
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettyFormatter.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettyFormatter.java
index 4406477..5d1592d 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettyFormatter.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettyFormatter.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.prettify.common;
 
+import com.google.gerrit.reviewdb.AccountDiffPreference;
 import com.google.gwtexpui.safehtml.client.SafeHtml;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 
@@ -71,7 +72,8 @@
   protected SparseFileContent content;
   protected EditFilter side;
   protected List<Edit> edits;
-  protected PrettySettings settings;
+  protected AccountDiffPreference diffPrefs;
+  protected String fileName;
   protected Set<Integer> trailingEdits;
 
   private int col;
@@ -105,8 +107,12 @@
     edits = all;
   }
 
-  public void setPrettySettings(PrettySettings how) {
-    settings = how;
+  public void setDiffPrefs(AccountDiffPreference how) {
+    diffPrefs = how;
+  }
+
+  public void setFileName(String fileName) {
+    this.fileName = fileName;
   }
 
   /**
@@ -122,7 +128,7 @@
 
     String html = toHTML(src);
 
-    if (settings.isSyntaxHighlighting() && getFileType() != null
+    if (diffPrefs.isSyntaxHighlighting() && getFileType() != null
         && src.isWholeFile()) {
       // The prettify parsers don't like &#39; as an entity for the
       // single quote character. Replace them all out so we don't
@@ -205,7 +211,7 @@
       cleanText(txt, pos, start);
       pos = txt.indexOf(';', start + 1) + 1;
 
-      if (settings.getLineLength() <= col) {
+      if (diffPrefs.getLineLength() <= col) {
         buf.append("<br />");
         col = 0;
       }
@@ -219,14 +225,14 @@
 
   private void cleanText(String txt, int pos, int end) {
     while (pos < end) {
-      int free = settings.getLineLength() - col;
+      int free = diffPrefs.getLineLength() - col;
       if (free <= 0) {
         // The current line is full. Throw an explicit line break
         // onto the end, and we'll continue on the next line.
         //
         buf.append("<br />");
         col = 0;
-        free = settings.getLineLength();
+        free = diffPrefs.getLineLength();
       }
 
       int n = Math.min(end - pos, free);
@@ -305,7 +311,7 @@
   private String toHTML(SparseFileContent src) {
     SafeHtml html;
 
-    if (settings.isIntralineDifference()) {
+    if (diffPrefs.isIntralineDifference()) {
       html = colorLineEdits(src);
     } else {
       SafeHtmlBuilder b = new SafeHtmlBuilder();
@@ -321,7 +327,7 @@
       html = html.replaceAll("\r([^\n])", r);
     }
 
-    if (settings.isShowWhiteSpaceErrors()) {
+    if (diffPrefs.isShowWhitespaceErrors()) {
       // We need to do whitespace errors before showing tabs, because
       // these patterns rely on \t as a literal, before it expands.
       //
@@ -329,8 +335,8 @@
       html = showTrailingWhitespace(html);
     }
 
-    if (settings.isShowTabs()) {
-      String t = 1 < settings.getTabSize() ? "\t" : "";
+    if (diffPrefs.isShowTabs()) {
+      String t = 1 < diffPrefs.getTabSize() ? "\t" : "";
       html = html.replaceAll("\t", "<span class=\"vt\">\u00BB</span>" + t);
     }
 
@@ -496,17 +502,17 @@
   private String expandTabs(String html) {
     StringBuilder tmp = new StringBuilder();
     int i = 0;
-    if (settings.isShowTabs()) {
+    if (diffPrefs.isShowTabs()) {
       i = 1;
     }
-    for (; i < settings.getTabSize(); i++) {
+    for (; i < diffPrefs.getTabSize(); i++) {
       tmp.append("&nbsp;");
     }
     return html.replaceAll("\t", tmp.toString());
   }
 
   private String getFileType() {
-    String srcType = settings.getFilename();
+    String srcType = fileName;
     if (srcType == null) {
       return null;
     }
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettySettings.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettySettings.java
deleted file mode 100644
index 3ef17f5..0000000
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettySettings.java
+++ /dev/null
@@ -1,106 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.prettify.common;
-
-/** Settings to configure a {@link PrettyFormatter}. */
-public class PrettySettings {
-  protected String fileName;
-  protected boolean showWhiteSpaceErrors;
-  protected int lineLength;
-  protected int tabSize;
-  protected boolean showTabs;
-  protected boolean syntaxHighlighting;
-  protected boolean intralineDifference;
-
-  public PrettySettings() {
-    showWhiteSpaceErrors = true;
-    lineLength = 100;
-    tabSize = 8;
-    showTabs = true;
-    syntaxHighlighting = true;
-    intralineDifference = true;
-  }
-
-  public PrettySettings(PrettySettings pretty) {
-    fileName = pretty.fileName;
-    showWhiteSpaceErrors = pretty.showWhiteSpaceErrors;
-    lineLength = pretty.lineLength;
-    tabSize = pretty.tabSize;
-    showTabs = pretty.showTabs;
-    syntaxHighlighting = pretty.syntaxHighlighting;
-    intralineDifference = pretty.intralineDifference;
-  }
-
-  public String getFilename() {
-    return fileName;
-  }
-
-  public PrettySettings setFileName(final String name) {
-    fileName = name;
-    return this;
-  }
-
-  public boolean isShowWhiteSpaceErrors() {
-    return showWhiteSpaceErrors;
-  }
-
-  public PrettySettings setShowWhiteSpaceErrors(final boolean show) {
-    showWhiteSpaceErrors = show;
-    return this;
-  }
-
-  public int getLineLength() {
-    return lineLength;
-  }
-
-  public PrettySettings setLineLength(final int len) {
-    lineLength = len;
-    return this;
-  }
-
-  public int getTabSize() {
-    return tabSize;
-  }
-
-  public PrettySettings setTabSize(final int len) {
-    tabSize = len;
-    return this;
-  }
-
-  public boolean isShowTabs() {
-    return showTabs;
-  }
-
-  public PrettySettings setShowTabs(final boolean show) {
-    showTabs = show;
-    return this;
-  }
-
-  public boolean isSyntaxHighlighting() {
-    return syntaxHighlighting;
-  }
-
-  public void setSyntaxHighlighting(final boolean on) {
-    syntaxHighlighting = on;
-  }
-
-  public boolean isIntralineDifference() {
-    return intralineDifference;
-  }
-
-  public void setIntralineDifference(final boolean on) {
-    intralineDifference = on;
-  }
-}
diff --git a/gerrit-reviewdb/pom.xml b/gerrit-reviewdb/pom.xml
index 726098c..5626739 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.3</version>
+    <version>2.1.4-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 bc5495f..f428a22 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
@@ -52,6 +52,10 @@
  * <li>{@link StarredChange}: user has starred the change, tracking
  * notifications of updates on that change, or just book-marking it for faster
  * future reference. One record per starred change.</li>
+ *
+ * <li>{@link AccountDiffPreference}: user's preferences for rendering side-to-side
+ * and unified diff</li>
+ *
  * </ul>
  */
 public final class Account {
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
new file mode 100644
index 0000000..4a3dd18
--- /dev/null
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountDiffPreference.java
@@ -0,0 +1,188 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.reviewdb;
+
+import com.google.gwtorm.client.Column;
+
+/** Diff formatting preferences of an account */
+public class AccountDiffPreference {
+
+  /** Default number of lines of context. */
+  public static final short DEFAULT_CONTEXT = 10;
+
+  /** Context setting to display the entire file. */
+  public static final short WHOLE_FILE_CONTEXT = -1;
+
+  /** Typical valid choices for the default context setting. */
+  public static final short[] CONTEXT_CHOICES =
+      {3, 10, 25, 50, 75, 100, WHOLE_FILE_CONTEXT};
+
+  public static enum Whitespace implements CodedEnum {
+    IGNORE_NONE('N'), //
+    IGNORE_SPACE_AT_EOL('E'), //
+    IGNORE_SPACE_CHANGE('S'), //
+    IGNORE_ALL_SPACE('A');
+
+    private final char code;
+
+    private Whitespace(final char c) {
+      code = c;
+    }
+
+    public char getCode() {
+      return code;
+    }
+
+    public static Whitespace forCode(final char c) {
+      for (final Whitespace s : Whitespace.values()) {
+        if (s.code == c) {
+          return s;
+        }
+      }
+      return null;
+    }
+  }
+
+  public static AccountDiffPreference createDefault(Account.Id accountId) {
+    AccountDiffPreference p = new AccountDiffPreference(accountId);
+    p.setIgnoreWhitespace(Whitespace.IGNORE_NONE);
+    p.setTabSize(8);
+    p.setLineLength(100);
+    p.setSyntaxHighlighting(true);
+    p.setShowWhitespaceErrors(true);
+    p.setIntralineDifference(true);
+    p.setShowTabs(true);
+    p.setContext(DEFAULT_CONTEXT);
+    return p;
+  }
+
+  @Column(id = 1, name = Column.NONE)
+  protected Account.Id accountId;
+
+  @Column(id = 2)
+  protected char ignoreWhitespace;
+
+  @Column(id = 3)
+  protected int tabSize;
+
+  @Column(id = 4)
+  protected int lineLength;
+
+  @Column(id = 5)
+  protected boolean syntaxHighlighting;
+
+  @Column(id = 6)
+  protected boolean showWhitespaceErrors;
+
+  @Column(id = 7)
+  protected boolean intralineDifference;
+
+  @Column(id = 8)
+  protected boolean showTabs;
+
+  /** Number of lines of context when viewing a patch. */
+  @Column(id = 9)
+  protected short context;
+
+  protected AccountDiffPreference() {
+  }
+
+  public AccountDiffPreference(Account.Id accountId) {
+    this.accountId = accountId;
+  }
+
+  public AccountDiffPreference(AccountDiffPreference p) {
+    this.accountId = p.accountId;
+    this.ignoreWhitespace = p.ignoreWhitespace;
+    this.tabSize = p.tabSize;
+    this.lineLength = p.lineLength;
+    this.syntaxHighlighting = p.syntaxHighlighting;
+    this.showWhitespaceErrors = p.showWhitespaceErrors;
+    this.intralineDifference = p.intralineDifference;
+    this.showTabs = p.showTabs;
+    this.context = p.context;
+  }
+
+  public Account.Id getAccountId() {
+    return accountId;
+  }
+
+  public Whitespace getIgnoreWhitespace() {
+    return Whitespace.forCode(ignoreWhitespace);
+  }
+
+  public void setIgnoreWhitespace(Whitespace ignoreWhitespace) {
+    this.ignoreWhitespace = ignoreWhitespace.getCode();
+  }
+
+  public int getTabSize() {
+    return tabSize;
+  }
+
+  public void setTabSize(int tabSize) {
+    this.tabSize = tabSize;
+  }
+
+  public int getLineLength() {
+    return lineLength;
+  }
+
+  public void setLineLength(int lineLength) {
+    this.lineLength = lineLength;
+  }
+
+  public boolean isSyntaxHighlighting() {
+    return syntaxHighlighting;
+  }
+
+  public void setSyntaxHighlighting(boolean syntaxHighlighting) {
+    this.syntaxHighlighting = syntaxHighlighting;
+  }
+
+  public boolean isShowWhitespaceErrors() {
+    return showWhitespaceErrors;
+  }
+
+  public void setShowWhitespaceErrors(boolean showWhitespaceErrors) {
+    this.showWhitespaceErrors = showWhitespaceErrors;
+  }
+
+  public boolean isIntralineDifference() {
+    return intralineDifference;
+  }
+
+  public void setIntralineDifference(boolean intralineDifference) {
+    this.intralineDifference = intralineDifference;
+  }
+
+  public boolean isShowTabs() {
+    return showTabs;
+  }
+
+  public void setShowTabs(boolean showTabs) {
+    this.showTabs = showTabs;
+  }
+
+  /** Get the number of lines of context when viewing a patch. */
+  public short getContext() {
+    return context;
+  }
+
+  /** Set the number of lines of context when viewing a patch. */
+  public void setContext(final short context) {
+    assert 0 <= context || context == WHOLE_FILE_CONTEXT;
+    this.context = context;
+  }
+}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountDiffPreferenceAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountDiffPreferenceAccess.java
new file mode 100644
index 0000000..d1d134b
--- /dev/null
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountDiffPreferenceAccess.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.reviewdb;
+
+import com.google.gwtorm.client.Access;
+import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.client.PrimaryKey;
+
+public interface AccountDiffPreferenceAccess extends Access<AccountDiffPreference, Account.Id> {
+
+  @PrimaryKey("accountId")
+  AccountDiffPreference get(Account.Id key) throws OrmException;
+
+}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGeneralPreferences.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGeneralPreferences.java
index 64654c7..9b607b0 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
@@ -18,15 +18,6 @@
 
 /** Preferences about a single user. */
 public final class AccountGeneralPreferences {
-  /** Default number of lines of context. */
-  public static final short DEFAULT_CONTEXT = 10;
-
-  /** Context setting to display the entire file. */
-  public static final short WHOLE_FILE_CONTEXT = -1;
-
-  /** Typical valid choices for the default context setting. */
-  public static final short[] CONTEXT_CHOICES =
-      {3, 10, 25, 50, 75, 100, WHOLE_FILE_CONTEXT};
 
   /** Default number of items to display per page. */
   public static final short DEFAULT_PAGESIZE = 25;
@@ -44,10 +35,6 @@
     REPO_DOWNLOAD, PULL, CHECKOUT, CHERRY_PICK, FORMAT_PATCH;
   }
 
-  /** Default number of lines of context when viewing a patch. */
-  @Column(id = 1)
-  protected short defaultContext;
-
   /** Number of changes to show in a screen. */
   @Column(id = 2)
   protected short maximumPageSize;
@@ -68,19 +55,13 @@
   @Column(id = 6, length = 20, notNull = false)
   protected String downloadCommand;
 
+  /** If true we CC the user on their own changes. */
+  @Column(id = 7)
+  protected boolean copySelfOnEmail;
+
   public AccountGeneralPreferences() {
   }
 
-  /** Get the default number of lines of context when viewing a patch. */
-  public short getDefaultContext() {
-    return defaultContext;
-  }
-
-  /** Set the number of lines of context when viewing a patch. */
-  public void setDefaultContext(final short s) {
-    defaultContext = s;
-  }
-
   public short getMaximumPageSize() {
     return maximumPageSize;
   }
@@ -135,11 +116,19 @@
     }
   }
 
+  public boolean isCopySelfOnEmails() {
+    return copySelfOnEmail;
+  }
+
+  public void setCopySelfOnEmails(boolean includeSelfOnEmail) {
+    copySelfOnEmail = includeSelfOnEmail;
+  }
+
   public void resetToDefaults() {
-    defaultContext = DEFAULT_CONTEXT;
     maximumPageSize = DEFAULT_PAGESIZE;
     showSiteHeader = true;
     useFlashClipboard = true;
+    copySelfOnEmail = false;
     downloadUrl = null;
     downloadCommand = 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 5d1565b..52bef2b 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
@@ -16,9 +16,12 @@
 
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.CompoundKey;
+import com.google.gwtorm.client.StringKey;
 
 /** An {@link Account} interested in a {@link Project}. */
 public final class AccountProjectWatch {
+  public static final String FILTER_ALL = "*";
+
   public static class Key extends CompoundKey<Account.Id> {
     private static final long serialVersionUID = 1L;
 
@@ -28,14 +31,19 @@
     @Column(id = 2)
     protected Project.NameKey projectName;
 
+    @Column(id = 3)
+    protected Filter filter;
+
     protected Key() {
       accountId = new Account.Id();
       projectName = new Project.NameKey();
+      filter = new Filter();
     }
 
-    public Key(final Account.Id a, final Project.NameKey g) {
+    public Key(Account.Id a, Project.NameKey g, String f) {
       accountId = a;
       projectName = g;
+      filter = new Filter(f);
     }
 
     @Override
@@ -45,7 +53,31 @@
 
     @Override
     public com.google.gwtorm.client.Key<?>[] members() {
-      return new com.google.gwtorm.client.Key<?>[] {projectName};
+      return new com.google.gwtorm.client.Key<?>[] {projectName, filter};
+    }
+  }
+
+  public static class Filter extends StringKey<com.google.gwtorm.client.Key<?>> {
+    private static final long serialVersionUID = 1L;
+
+    @Column(id = 1)
+    protected String filter;
+
+    protected Filter() {
+    }
+
+    public Filter(String f) {
+      filter = f != null && !f.isEmpty() ? f : FILTER_ALL;
+    }
+
+    @Override
+    public String get() {
+      return filter;
+    }
+
+    @Override
+    protected void set(String newValue) {
+      filter = newValue;
     }
   }
 
@@ -83,6 +115,10 @@
     return key.projectName;
   }
 
+  public String getFilter() {
+    return FILTER_ALL.equals(key.filter.get()) ? null : key.filter.get();
+  }
+
   public boolean isNotifyNewChanges() {
     return notifyNewChanges;
   }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountProjectWatchAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountProjectWatchAccess.java
index ce44489..254aac9 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountProjectWatchAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountProjectWatchAccess.java
@@ -28,15 +28,6 @@
   @Query("WHERE key.accountId = ?")
   ResultSet<AccountProjectWatch> byAccount(Account.Id id) throws OrmException;
 
-  @Query("WHERE notifyNewChanges = true AND key.projectName = ?")
-  ResultSet<AccountProjectWatch> notifyNewChanges(Project.NameKey name)
-      throws OrmException;
-
-  @Query("WHERE notifyAllComments = true AND key.projectName = ?")
-  ResultSet<AccountProjectWatch> notifyAllComments(Project.NameKey name)
-      throws OrmException;
-
-  @Query("WHERE notifySubmittedChanges = true AND key.projectName = ?")
-  ResultSet<AccountProjectWatch> notifySubmittedChanges(Project.NameKey name)
-      throws OrmException;
+  @Query("WHERE key.projectName = ?")
+  ResultSet<AccountProjectWatch> byProject(Project.NameKey name) throws OrmException;
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Change.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Change.java
index 7fafb13..e4ae63d 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Change.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Change.java
@@ -351,6 +351,10 @@
   @Column(id = 13)
   protected String subject;
 
+  /** Topic name assigned by the user, if any. */
+  @Column(id = 14, notNull = false)
+  protected String topic;
+
   protected Change() {
   }
 
@@ -456,4 +460,12 @@
     open = newStatus.isOpen();
     status = newStatus.getCode();
   }
+
+  public String getTopic() {
+    return topic;
+  }
+
+  public void setTopic(String topic) {
+    this.topic = topic;
+  }
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchLineCommentAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchLineCommentAccess.java
index 6beacde..26785a8 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchLineCommentAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchLineCommentAccess.java
@@ -25,6 +25,9 @@
   @PrimaryKey("key")
   PatchLineComment get(PatchLineComment.Key id) throws OrmException;
 
+  @Query("WHERE key.patchKey.patchSetId.changeId = ?")
+  ResultSet<PatchLineComment> byChange(Change.Id id) throws OrmException;
+
   @Query("WHERE key.patchKey = ? AND status = '"
       + PatchLineComment.STATUS_PUBLISHED + "' ORDER BY lineNbr,writtenOn")
   ResultSet<PatchLineComment> published(Patch.Key patch) throws OrmException;
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 0db09e4..ec70051 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
@@ -25,6 +25,9 @@
   /** Pattern that matches all references in a project. */
   public static final String ALL = "refs/*";
 
+  /** Prefix that triggers a regular expression pattern. */
+  public static final String REGEX_PREFIX = "^";
+
   public static class RefPattern extends
       StringKey<com.google.gwtorm.client.Key<?>> {
     private static final long serialVersionUID = 1L;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ReviewDb.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ReviewDb.java
index c592cb13..f9b3cfa 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ReviewDb.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ReviewDb.java
@@ -76,6 +76,9 @@
   AccountGroupAgreementAccess accountGroupAgreements();
 
   @Relation
+  AccountDiffPreferenceAccess accountDiffPreferences();
+
+  @Relation
   StarredChangeAccess starredChanges();
 
   @Relation
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/TrackingId.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/TrackingId.java
index bd798a0..7df7619 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/TrackingId.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/TrackingId.java
@@ -127,6 +127,14 @@
     return key.changeId;
   }
 
+  public String getTrackingId() {
+    return key.trackingId.get();
+  }
+
+  public String getSystem() {
+    return key.trackingSystem.get();
+  }
+
   @Override
   public int hashCode() {
     return key.hashCode();
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_generic.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_generic.sql
index 5f6b375..0d41729 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_generic.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_generic.sql
@@ -47,17 +47,9 @@
 -- *********************************************************************
 -- AccountProjectWatchAccess
 --    @PrimaryKey covers: byAccount
---    covers:             notifyNewChanges
-CREATE INDEX account_project_watches_ntNew
-ON account_project_watches (notify_new_changes, project_name);
-
---    covers:             notifyAllComments
-CREATE INDEX account_project_watches_ntCmt
-ON account_project_watches (notify_all_comments, project_name);
-
---    covers:             notifySubmittedChanges
-CREATE INDEX account_project_watches_ntSub
-ON account_project_watches (notify_submitted_changes, project_name);
+--    covers:             byProject
+CREATE INDEX account_project_watches_byProject
+ON account_project_watches (project_name);
 
 
 -- *********************************************************************
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_postgres.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_postgres.sql
index 56acddb..b44351c 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_postgres.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_postgres.sql
@@ -86,20 +86,9 @@
 -- *********************************************************************
 -- AccountProjectWatchAccess
 --    @PrimaryKey covers: byAccount
---    covers:             notifyNewChanges
-CREATE INDEX account_project_watches_ntNew
-ON account_project_watches (project_name)
-WHERE notify_new_changes = 'Y';
-
---    covers:             notifyAllComments
-CREATE INDEX account_project_watches_ntCmt
-ON account_project_watches (project_name)
-WHERE notify_all_comments = 'Y';
-
---    covers:             notifySubmittedChanges
-CREATE INDEX account_project_watches_ntSub
-ON account_project_watches (project_name)
-WHERE notify_submitted_changes = 'Y';
+--    covers:             byProject
+CREATE INDEX account_project_watches_byProject
+ON account_project_watches (project_name);
 
 
 -- *********************************************************************
diff --git a/gerrit-server/.settings/org.eclipse.jdt.core.prefs b/gerrit-server/.settings/org.eclipse.jdt.core.prefs
index 04afc7f..2f45466 100644
--- a/gerrit-server/.settings/org.eclipse.jdt.core.prefs
+++ b/gerrit-server/.settings/org.eclipse.jdt.core.prefs
@@ -1,14 +1,8 @@
-#Tue May 12 17:44:13 PDT 2009
+#Fri Jul 16 23:39:13 PDT 2010
 eclipse.preferences.version=1
-org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
 org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
-org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
 org.eclipse.jdt.core.compiler.compliance=1.6
-org.eclipse.jdt.core.compiler.debug.lineNumber=generate
-org.eclipse.jdt.core.compiler.debug.localVariable=generate
-org.eclipse.jdt.core.compiler.debug.sourceFile=generate
-org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
-org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
 org.eclipse.jdt.core.compiler.source=1.6
 org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16
@@ -252,6 +246,8 @@
 org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert
 org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert
 org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.join_lines_in_comments=true
+org.eclipse.jdt.core.formatter.join_wrapped_lines=true
 org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false
 org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false
 org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=true
diff --git a/gerrit-server/pom.xml b/gerrit-server/pom.xml
index 8f3169a..db8093c 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.3</version>
+    <version>2.1.4-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-server</artifactId>
@@ -54,6 +54,11 @@
     </dependency>
 
     <dependency>
+      <groupId>commons-lang</groupId>
+      <artifactId>commons-lang</artifactId>
+    </dependency>
+
+    <dependency>
       <groupId>commons-net</groupId>
       <artifactId>commons-net</artifactId>
     </dependency>
@@ -133,6 +138,11 @@
       <groupId>com.google.gerrit</groupId>
       <artifactId>juniversalchardet</artifactId>
     </dependency>
+
+    <dependency>
+      <groupId>dk.brics.automaton</groupId>
+      <artifactId>automaton</artifactId>
+    </dependency>
   </dependencies>
 
   <build>
diff --git a/gerrit-server/src/main/antlr/com/google/gerrit/server/query/Query.g b/gerrit-server/src/main/antlr/com/google/gerrit/server/query/Query.g
index 7842c1b..74b7851 100644
--- a/gerrit-server/src/main/antlr/com/google/gerrit/server/query/Query.g
+++ b/gerrit-server/src/main/antlr/com/google/gerrit/server/query/Query.g
@@ -19,13 +19,10 @@
 }
 
 tokens {
-  FIELD_NAME;
-  DEFAULT_FIELD;
-  SINGLE_WORD;
-  EXACT_PHRASE;
   AND;
   OR;
   NOT;
+  DEFAULT_FIELD;
 }
 
 @header {
@@ -63,6 +60,8 @@
       final QueryLexer lexer = new QueryLexer(new ANTLRStringStream(value));
       lexer.mSINGLE_WORD();
       return lexer.nextToken().getType() == QueryParser.EOF;
+    } catch (QueryParseInternalException e) {
+      return false;
     } catch (RecognitionException e) {
       return false;
     }
@@ -81,6 +80,13 @@
 package com.google.gerrit.server.query;
 }
 @lexer::members {
+  @Override
+  public void displayRecognitionError(String[] tokenNames,
+                                      RecognitionException e) {
+      String hdr = getErrorHeader(e);
+      String msg = getErrorMessage(e, tokenNames);
+      throw new QueryParser.QueryParseInternalException(hdr + " " + msg);
+  }
 }
 
 query
@@ -110,10 +116,12 @@
 conditionNot
   : '-' conditionBase -> ^(NOT conditionBase)
   | NOT^ conditionBase
+  | VARIABLE_ASSIGN^ conditionOr ')'!
   | conditionBase
   ;
 conditionBase
-  : (FIELD_NAME ':') => FIELD_NAME^ ':'! fieldValue
+  : '('! conditionOr ')'!
+  | (FIELD_NAME ':') => FIELD_NAME^ ':'! fieldValue
   | fieldValue -> ^(DEFAULT_FIELD fieldValue)
   ;
 
@@ -121,7 +129,6 @@
   : n=FIELD_NAME   -> SINGLE_WORD[n]
   | SINGLE_WORD
   | EXACT_PHRASE
-  | '('! conditionOr ')'!
   ;
 
 AND: 'AND' ;
@@ -133,7 +140,14 @@
   ;
 
 FIELD_NAME
-  : ('a'..'z')+
+  : ('a'..'z' | '_')+
+  ;
+
+VARIABLE_ASSIGN
+  : ('A'..'Z') ('A'..'Z' | 'a'..'Z')* '=' '(' {
+      String s = $text;
+      setText(s.substring(0, s.length() - 2));
+    }
   ;
 
 EXACT_PHRASE
@@ -164,7 +178,9 @@
      // '/'  permit
      | ':'
      | ';'
-     | '<' | '=' | '>'
+     // '<' permit
+     // '=' permit
+     // '>' permit
      | '?'
      | '[' | ']'
      | '{' | '}'
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 259b0fb..f2d0ad0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
@@ -24,18 +24,22 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.events.ApprovalAttribute;
+import com.google.gerrit.server.events.ChangeAbandonedEvent;
+import com.google.gerrit.server.events.ChangeEvent;
+import com.google.gerrit.server.events.ChangeMergedEvent;
+import com.google.gerrit.server.events.CommentAddedEvent;
+import com.google.gerrit.server.events.EventFactory;
+import com.google.gerrit.server.events.PatchSetCreatedEvent;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
-import com.google.inject.internal.Nullable;
 
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Repository;
@@ -60,68 +64,6 @@
     /** A logger for this class. */
     private static final Logger log = LoggerFactory.getLogger(ChangeHookRunner.class);
 
-    public static abstract class ChangeEvent {
-    }
-
-    public static class ApprovalAttribute {
-        public String type;
-        public String description;
-        public String value;
-    }
-
-    public static class AuthorAttribute {
-        public String name;
-        public String email;
-    }
-
-    public static class ChangeAttribute {
-        public String project;
-        public String branch;
-        public String id;
-        public String number;
-        public String subject;
-        public AuthorAttribute owner;
-        public String url;
-    }
-
-    public static class PatchSetAttribute {
-        public String number;
-        public String revision;
-        public String ref;
-        public AuthorAttribute uploader;
-    }
-
-    public static class CommentAddedEvent extends ChangeEvent {
-        public final String type = "comment-added";
-        public ChangeAttribute change;
-        public PatchSetAttribute patchSet;
-        public AuthorAttribute author;
-        public ApprovalAttribute[] approvals;
-        public String comment;
-    }
-
-    public static class ChangeMergedEvent extends ChangeEvent {
-        public final String type = "change-merged";
-        public ChangeAttribute change;
-        public PatchSetAttribute patchSet;
-        public AuthorAttribute submitter;
-    }
-
-    public static class ChangeAbandonedEvent extends ChangeEvent {
-        public final String type = "change-abandoned";
-        public ChangeAttribute change;
-        public PatchSetAttribute patchSet;
-        public AuthorAttribute abandoner;
-        public String reason;
-    }
-
-    public static class PatchSetCreatedEvent extends ChangeEvent {
-        public final String type = "patchset-created";
-        public ChangeAttribute change;
-        public PatchSetAttribute patchSet;
-        public AuthorAttribute uploader;
-    }
-
     private static class ChangeListenerHolder {
         final ChangeListener listener;
         final IdentifiedUser user;
@@ -160,8 +102,7 @@
 
     private final ApprovalTypes approvalTypes;
 
-    private final Provider<String> urlProvider;
-
+    private final EventFactory eventFactory;
 
     /**
      * Create a new ChangeHookRunner.
@@ -179,13 +120,13 @@
       final ProjectCache projectCache,
       final AccountCache accountCache,
       final ApprovalTypes approvalTypes,
-      @CanonicalWebUrl @Nullable final Provider<String> cwu) {
+      final EventFactory eventFactory) {
         this.repoManager = repoManager;
         this.hookQueue = queue.createQueue(1, "hook");
         this.projectCache = projectCache;
         this.accountCache = accountCache;
         this.approvalTypes = approvalTypes;
-        this.urlProvider = cwu;
+        this.eventFactory = eventFactory;
 
         final File hooksPath = sitePath.resolve(getValue(config, "hooks", "path", sitePath.hooks_dir.getAbsolutePath()));
 
@@ -241,9 +182,9 @@
         final PatchSetCreatedEvent event = new PatchSetCreatedEvent();
         final AccountState uploader = accountCache.get(patchSet.getUploader());
 
-        event.change = getChangeAttribute(change);
-        event.patchSet = getPatchSetAttribute(patchSet);
-        event.uploader = getAccountAttribute(uploader.getAccount());
+        event.change = eventFactory.asChangeAttribute(change);
+        event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
+        event.uploader = eventFactory.asAccountAttribute(uploader.getAccount());
         fireEvent(change, event);
 
         final List<String> args = new ArrayList<String>();
@@ -279,9 +220,9 @@
     public void doCommentAddedHook(final Change change, final Account account, final PatchSet patchSet, final String comment, final Map<ApprovalCategory.Id, ApprovalCategoryValue.Id> approvals) {
         final CommentAddedEvent event = new CommentAddedEvent();
 
-        event.change = getChangeAttribute(change);
-        event.author =  getAccountAttribute(account);
-        event.patchSet = getPatchSetAttribute(patchSet);
+        event.change = eventFactory.asChangeAttribute(change);
+        event.author =  eventFactory.asAccountAttribute(account);
+        event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
         event.comment = comment;
 
         if (approvals.size() > 0) {
@@ -329,9 +270,9 @@
     public void doChangeMergedHook(final Change change, final Account account, final PatchSet patchSet) {
         final ChangeMergedEvent event = new ChangeMergedEvent();
 
-        event.change = getChangeAttribute(change);
-        event.submitter =  getAccountAttribute(account);
-        event.patchSet = getPatchSetAttribute(patchSet);
+        event.change = eventFactory.asChangeAttribute(change);
+        event.submitter = eventFactory.asAccountAttribute(account);
+        event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
         fireEvent(change, event);
 
         final List<String> args = new ArrayList<String>();
@@ -363,8 +304,8 @@
     public void doChangeAbandonedHook(final Change change, final Account account, final String reason) {
         final ChangeAbandonedEvent event = new ChangeAbandonedEvent();
 
-        event.change = getChangeAttribute(change);
-        event.abandoner = getAccountAttribute(account);
+        event.change = eventFactory.asChangeAttribute(change);
+        event.abandoner = eventFactory.asAccountAttribute(account);
         event.reason = reason;
         fireEvent(change, event);
 
@@ -404,22 +345,6 @@
         return pc.controlFor(change).isVisible();
     }
 
-    /** Get a link to the change; null if the server doesn't know its own address. */
-    private String getChangeUrl(final Change change) {
-        if (change != null && getGerritUrl() != null) {
-            final StringBuilder r = new StringBuilder();
-            r.append(getGerritUrl());
-            r.append(change.getChangeId());
-            return r.toString();
-        }
-        return null;
-    }
-
-    private String getGerritUrl() {
-        return urlProvider.get();
-    }
-
-
     /**
      * Create an ApprovalAttribute for the given approval suitable for serialization to JSON.
      * @param approval
@@ -436,54 +361,6 @@
     }
 
     /**
-     * Create an AuthorAttribute for the given account suitable for serialization to JSON.
-     *
-     * @param account
-     * @return object suitable for serialization to JSON
-     */
-    private AuthorAttribute getAccountAttribute(final Account account) {
-        AuthorAttribute author = new AuthorAttribute();
-        author.name = account.getFullName();
-        author.email = account.getPreferredEmail();
-        return author;
-    }
-
-    /**
-     * Create a ChangeAttribute for the given change suitable for serialization to JSON.
-     *
-     * @param change
-     * @return object suitable for serialization to JSON
-     */
-    private ChangeAttribute getChangeAttribute(final Change change) {
-        ChangeAttribute a = new ChangeAttribute();
-        a.project = change.getProject().get();
-        a.branch = change.getDest().getShortName();
-        a.id = change.getKey().get();
-        a.number = change.getId().toString();
-        a.subject = change.getSubject();
-        final AccountState owner = accountCache.get(change.getOwner());
-        a.owner = getAccountAttribute(owner.getAccount());
-        a.url = getChangeUrl(change);
-        return a;
-    }
-
-    /**
-     * Create an PatchSetAttribute for the given patchset suitable for serialization to JSON.
-     *
-     * @param patchSet
-     * @return object suitable for serialization to JSON
-     */
-    private PatchSetAttribute getPatchSetAttribute(final PatchSet patchSet) {
-        PatchSetAttribute p = new PatchSetAttribute();
-        p.revision = patchSet.getRevision().get();
-        p.number = Integer.toString(patchSet.getPatchSetId());
-        p.ref = patchSet.getRefName();
-        final AccountState uploader = accountCache.get(patchSet.getUploader());
-        p.uploader = getAccountAttribute(uploader.getAccount());
-        return p;
-    }
-
-    /**
      * Get the display name for the given account.
      *
      * @param account Account to get name for.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeListener.java b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeListener.java
index 7e8e853..65a9857 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeListener.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeListener.java
@@ -14,7 +14,8 @@
 
 package com.google.gerrit.common;
 
-import com.google.gerrit.common.ChangeHookRunner.ChangeEvent;
+import com.google.gerrit.server.events.ChangeEvent;
+
 
 public interface ChangeListener {
     public void onChangeEvent(ChangeEvent event);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/AnonymousUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/AnonymousUser.java
index a171497..1bd2066 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/AnonymousUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/AnonymousUser.java
@@ -15,11 +15,13 @@
 package com.google.gerrit.server;
 
 import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.AccountProjectWatch;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Set;
 
@@ -42,6 +44,11 @@
   }
 
   @Override
+  public Collection<AccountProjectWatch> getNotificationFilters() {
+    return Collections.emptySet();
+  }
+
+  @Override
   public String toString() {
     return "ANONYMOUS";
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
index 740ce9c..36fc2ff 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.reviewdb.TrackingId;
 import com.google.gerrit.server.config.TrackingFooter;
 import com.google.gerrit.server.config.TrackingFooters;
+import com.google.gerrit.server.git.MergeOp;
 import com.google.gerrit.server.git.MergeQueue;
 import com.google.gwtorm.client.AtomicUpdate;
 import com.google.gwtorm.client.OrmConcurrencyException;
@@ -135,8 +136,8 @@
     db.trackingIds().delete(toDelete);
   }
 
-  public static void submit(PatchSet.Id patchSetId, IdentifiedUser user, ReviewDb db, MergeQueue merger)
-      throws OrmException {
+  public static void submit(MergeOp.Factory opFactory, PatchSet.Id patchSetId,
+      IdentifiedUser user, ReviewDb db, MergeQueue merger) throws OrmException {
     final Change.Id changeId = patchSetId.getParentKey();
     final PatchSetApproval approval = createSubmitApproval(patchSetId, user, db);
 
@@ -154,7 +155,7 @@
     });
 
     if (change.getStatus() == Change.Status.SUBMITTED) {
-      merger.merge(change.getDest());
+      merger.merge(opFactory, change.getDest());
     }
   }
 
@@ -177,18 +178,22 @@
     return new PatchSetApproval(akey, (short) 1);
   }
 
-
-  public static void computeSortKey(final Change c) {
+  public static String sortKey(long lastUpdated, int id){
     // The encoding uses minutes since Wed Oct 1 00:00:00 2008 UTC.
     // We overrun approximately 4,085 years later, so ~6093.
     //
-    final long lastUpdatedOn =
-        (c.getLastUpdatedOn().getTime() / 1000L) - 1222819200L;
+    final long lastUpdatedOn = (lastUpdated / 1000L) - 1222819200L;
     final StringBuilder r = new StringBuilder(16);
     r.setLength(16);
     formatHexInt(r, 0, (int) (lastUpdatedOn / 60));
-    formatHexInt(r, 8, c.getId().get());
-    c.setSortKey(r.toString());
+    formatHexInt(r, 8, id);
+    return r.toString();
+  }
+
+  public static void computeSortKey(final Change c) {
+    long lastUpdated = c.getLastUpdatedOn().getTime();
+    int id = c.getId().get();
+    c.setSortKey(sortKey(lastUpdated, id));
   }
 
   private static final char[] hexchar =
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
index 17e69c7..751d215 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
@@ -15,10 +15,12 @@
 package com.google.gerrit.server;
 
 import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.AccountProjectWatch;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.inject.servlet.RequestScoped;
 
+import java.util.Collection;
 import java.util.Set;
 
 /**
@@ -59,6 +61,9 @@
   /** Set of changes starred by this user. */
   public abstract Set<Change.Id> getStarredChanges();
 
+  /** Filters selecting changes the user wants to monitor. */
+  public abstract Collection<AccountProjectWatch> getNotificationFilters();
+
   /** Is the user a non-interactive user? */
   public boolean isBatchUser() {
     return getEffectiveGroups().contains(authConfig.getBatchUsersGroup());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
index 6900538..78cbed3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
@@ -15,7 +15,9 @@
 package com.google.gerrit.server;
 
 import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.AccountDiffPreference;
 import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.AccountProjectWatch;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.reviewdb.StarredChange;
@@ -28,7 +30,6 @@
 import com.google.inject.Inject;
 import com.google.inject.OutOfScopeException;
 import com.google.inject.Provider;
-import com.google.inject.ProvisionException;
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.lib.PersonIdent;
@@ -41,9 +42,11 @@
 import java.net.MalformedURLException;
 import java.net.SocketAddress;
 import java.net.URL;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 import java.util.TimeZone;
 
@@ -73,6 +76,11 @@
       return create(AccessPath.UNKNOWN, null, id);
     }
 
+    public IdentifiedUser create(Provider<ReviewDb> db, Account.Id id) {
+      return new IdentifiedUser(AccessPath.UNKNOWN, authConfig, canonicalUrl,
+          realm, accountCache, null, db, id);
+    }
+
     public IdentifiedUser create(AccessPath accessPath,
         Provider<SocketAddress> remotePeerProvider, Account.Id id) {
       return new IdentifiedUser(accessPath, authConfig, canonicalUrl, realm,
@@ -138,6 +146,7 @@
   private Set<String> emailAddresses;
   private Set<AccountGroup.Id> effectiveGroups;
   private Set<Change.Id> starredChanges;
+  private Collection<AccountProjectWatch> notificationFilters;
 
   private IdentifiedUser(final AccessPath accessPath,
       final AuthConfig authConfig, final Provider<String> canonicalUrl,
@@ -174,6 +183,20 @@
     return state().getAccount();
   }
 
+  public AccountDiffPreference getAccountDiffPreference() {
+    AccountDiffPreference diffPref;
+    try {
+      diffPref = dbProvider.get().accountDiffPreferences().get(getAccountId());
+      if (diffPref == null) {
+        diffPref = AccountDiffPreference.createDefault(getAccountId());
+      }
+    } catch (OrmException e) {
+      log.warn("Cannot query account diff preferences", e);
+      diffPref = AccountDiffPreference.createDefault(getAccountId());
+    }
+    return diffPref;
+  }
+
   public Set<String> getEmailAddresses() {
     if (emailAddresses == null) {
       emailAddresses = state().getEmailAddresses();
@@ -206,8 +229,6 @@
             .byAccount(getAccountId())) {
           h.add(sc.getChangeId());
         }
-      } catch (ProvisionException e) {
-        log.warn("Cannot query starred by user changes", e);
       } catch (OrmException e) {
         log.warn("Cannot query starred by user changes", e);
       }
@@ -216,6 +237,25 @@
     return starredChanges;
   }
 
+  @Override
+  public Collection<AccountProjectWatch> getNotificationFilters() {
+    if (notificationFilters == null) {
+      if (dbProvider == null) {
+        throw new OutOfScopeException("Not in request scoped user");
+      }
+      List<AccountProjectWatch> r;
+      try {
+        r = dbProvider.get().accountProjectWatches() //
+            .byAccount(getAccountId()).toList();
+      } catch (OrmException e) {
+        log.warn("Cannot query notification filters of a user", e);
+        r = Collections.emptyList();
+      }
+      notificationFilters = Collections.unmodifiableList(r);
+    }
+    return notificationFilters;
+  }
+
   public PersonIdent newRefLogIdent() {
     return newRefLogIdent(new Date(), TimeZone.getDefault());
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java
index 26e510f..e422b19 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java
@@ -15,12 +15,15 @@
 package com.google.gerrit.server;
 
 import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.AccountProjectWatch;
 import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.Project.NameKey;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
 import java.net.SocketAddress;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
@@ -57,6 +60,11 @@
     return Collections.emptySet();
   }
 
+  @Override
+  public Collection<AccountProjectWatch> getNotificationFilters() {
+    return Collections.emptySet();
+  }
+
   public SocketAddress getRemoteAddress() {
     return peer;
   }
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 1b010a6e..3293324 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
@@ -15,11 +15,14 @@
 package com.google.gerrit.server;
 
 import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.AccountProjectWatch;
 import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.Project.NameKey;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
@@ -71,6 +74,11 @@
     return Collections.emptySet();
   }
 
+  @Override
+  public Collection<AccountProjectWatch> getNotificationFilters() {
+    return Collections.emptySet();
+  }
+
   public boolean isEverythingVisible() {
     return getEffectiveGroups() == EVERYTHING_VISIBLE;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
index d4f7a4d..64046fa 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
@@ -19,8 +19,7 @@
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.SelfPopulatingCache;
-import com.google.gwtorm.client.OrmException;
+import com.google.gerrit.server.cache.EntryCreator;
 import com.google.gwtorm.client.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Module;
@@ -43,69 +42,73 @@
       protected void configure() {
         final TypeLiteral<Cache<String, Set<Account.Id>>> type =
             new TypeLiteral<Cache<String, Set<Account.Id>>>() {};
-        core(type, CACHE_NAME);
+        core(type, CACHE_NAME).populateWith(Loader.class);
         bind(AccountByEmailCacheImpl.class);
         bind(AccountByEmailCache.class).to(AccountByEmailCacheImpl.class);
       }
     };
   }
 
-  private final SchemaFactory<ReviewDb> schema;
-  private final SelfPopulatingCache<String, Set<Account.Id>> self;
+  private final Cache<String, Set<Account.Id>> cache;
 
   @Inject
-  AccountByEmailCacheImpl(final SchemaFactory<ReviewDb> schema,
-      @Named(CACHE_NAME) final Cache<String, Set<Account.Id>> rawCache) {
-    this.schema = schema;
-    this.self = new SelfPopulatingCache<String, Set<Account.Id>>(rawCache) {
-      @Override
-      protected Set<Account.Id> createEntry(final String key) throws Exception {
-        return lookup(key);
-      }
-
-      @Override
-      protected Set<Account.Id> missing(final String key) {
-        return Collections.emptySet();
-      }
-    };
-  }
-
-  private Set<Account.Id> lookup(final String email) throws OrmException {
-    final ReviewDb db = schema.open();
-    try {
-      final HashSet<Account.Id> r = new HashSet<Account.Id>();
-      for (Account a : db.accounts().byPreferredEmail(email)) {
-        r.add(a.getId());
-      }
-      for (AccountExternalId a : db.accountExternalIds().byEmailAddress(email)) {
-        r.add(a.getAccountId());
-      }
-      return pack(r);
-    } finally {
-      db.close();
-    }
+  AccountByEmailCacheImpl(
+      @Named(CACHE_NAME) final Cache<String, Set<Account.Id>> cache) {
+    this.cache = cache;
   }
 
   public Set<Account.Id> get(final String email) {
-    return self.get(email);
+    return cache.get(email);
   }
 
   public void evict(final String email) {
-    self.remove(email);
+    cache.remove(email);
   }
 
-  private static Set<Account.Id> pack(final Set<Account.Id> c) {
-    switch (c.size()) {
-      case 0:
-        return Collections.emptySet();
-      case 1:
-        return one(c);
-      default:
-        return Collections.unmodifiableSet(new HashSet<Account.Id>(c));
+  static class Loader extends EntryCreator<String, Set<Account.Id>> {
+    private final SchemaFactory<ReviewDb> schema;
+
+    @Inject
+    Loader(final SchemaFactory<ReviewDb> schema) {
+      this.schema = schema;
     }
-  }
 
-  private static <T> Set<T> one(final Set<T> c) {
-    return Collections.singleton(c.iterator().next());
+    @Override
+    public Set<Account.Id> createEntry(final String email) throws Exception {
+      final ReviewDb db = schema.open();
+      try {
+        final HashSet<Account.Id> r = new HashSet<Account.Id>();
+        for (Account a : db.accounts().byPreferredEmail(email)) {
+          r.add(a.getId());
+        }
+        for (AccountExternalId a : db.accountExternalIds()
+            .byEmailAddress(email)) {
+          r.add(a.getAccountId());
+        }
+        return pack(r);
+      } finally {
+        db.close();
+      }
+    }
+
+    @Override
+    public Set<Account.Id> missing(final String key) {
+      return Collections.emptySet();
+    }
+
+    private static Set<Account.Id> pack(final Set<Account.Id> c) {
+      switch (c.size()) {
+        case 0:
+          return Collections.emptySet();
+        case 1:
+          return one(c);
+        default:
+          return Collections.unmodifiableSet(new HashSet<Account.Id>(c));
+      }
+    }
+
+    private static <T> Set<T> one(final Set<T> c) {
+      return Collections.singleton(c.iterator().next());
+    }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
index dcd0bda..52ccc66 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
@@ -21,7 +21,7 @@
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.SelfPopulatingCache;
+import com.google.gerrit.server.cache.EntryCreator;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gwtorm.client.OrmException;
 import com.google.gwtorm.client.SchemaFactory;
@@ -39,119 +39,35 @@
 /** Caches important (but small) account state to avoid database hits. */
 @Singleton
 public class AccountCacheImpl implements AccountCache {
-  private static final String CACHE_NAME = "accounts";
+  private static final String BYID_NAME = "accounts";
+  private static final String BYUSER_NAME = "accounts_byname";
 
   public static Module module() {
     return new CacheModule() {
       @Override
       protected void configure() {
-        final TypeLiteral<Cache<Object, AccountState>> type =
-            new TypeLiteral<Cache<Object, AccountState>>() {};
-        core(type, CACHE_NAME);
+        final TypeLiteral<Cache<Account.Id, AccountState>> byIdType =
+            new TypeLiteral<Cache<Account.Id, AccountState>>() {};
+        core(byIdType, BYID_NAME).populateWith(ByIdLoader.class);
+
+        final TypeLiteral<Cache<String, Account.Id>> byUsernameType =
+            new TypeLiteral<Cache<String, Account.Id>>() {};
+        core(byUsernameType, BYUSER_NAME).populateWith(ByNameLoader.class);
+
         bind(AccountCacheImpl.class);
         bind(AccountCache.class).to(AccountCacheImpl.class);
       }
     };
   }
 
-  private final SchemaFactory<ReviewDb> schema;
-  private final GroupCache groupCache;
-  private final SelfPopulatingCache<Account.Id, AccountState> byId;
-  private final SelfPopulatingCache<String, Account.Id> byName;
-
-  private final Set<AccountGroup.Id> registered;
-  private final Set<AccountGroup.Id> anonymous;
+  private final Cache<Account.Id, AccountState> byId;
+  private final Cache<String, Account.Id> byName;
 
   @Inject
-  AccountCacheImpl(final SchemaFactory<ReviewDb> sf, final AuthConfig auth,
-      final GroupCache groupCache,
-      @Named(CACHE_NAME) final Cache<Object, AccountState> rawCache) {
-    schema = sf;
-    registered = auth.getRegisteredGroups();
-    anonymous = auth.getAnonymousGroups();
-    this.groupCache = groupCache;
-
-    byId = new SelfPopulatingCache<Account.Id, AccountState>((Cache) rawCache) {
-      @Override
-      protected AccountState createEntry(Account.Id key) throws Exception {
-        return lookup(key);
-      }
-
-      @Override
-      protected AccountState missing(final Account.Id key) {
-        return missingAccount(key);
-      }
-    };
-
-    byName = new SelfPopulatingCache<String, Account.Id>((Cache) rawCache) {
-      @Override
-      protected Account.Id createEntry(String username) throws Exception {
-        return lookup(username);
-      }
-    };
-  }
-
-  private AccountState lookup(final Account.Id who) throws OrmException {
-    final ReviewDb db = schema.open();
-    try {
-      final AccountState state = load(db, who);
-      if (state.getUserName() != null) {
-        byName.put(state.getUserName(), state.getAccount().getId());
-      }
-      return state;
-    } finally {
-      db.close();
-    }
-  }
-
-  private Account.Id lookup(final String username) throws OrmException {
-    final ReviewDb db = schema.open();
-    try {
-      final AccountExternalId.Key key =
-          new AccountExternalId.Key(AccountExternalId.SCHEME_USERNAME, username);
-      final AccountExternalId id = db.accountExternalIds().get(key);
-      return id != null ? id.getAccountId() : null;
-    } finally {
-      db.close();
-    }
-  }
-
-  private AccountState load(final ReviewDb db, final Account.Id who)
-      throws OrmException {
-    final Account account = db.accounts().get(who);
-    if (account == null) {
-      // Account no longer exists? They are anonymous.
-      //
-      return missingAccount(who);
-    }
-
-    final Collection<AccountExternalId> externalIds =
-        Collections.unmodifiableCollection(db.accountExternalIds().byAccount(
-            who).toList());
-
-    Set<AccountGroup.Id> internalGroups = new HashSet<AccountGroup.Id>();
-    for (AccountGroupMember g : db.accountGroupMembers().byAccount(who)) {
-      final AccountGroup.Id groupId = g.getAccountGroupId();
-      final AccountGroup group = groupCache.get(groupId);
-      if (group != null && group.getType() == AccountGroup.Type.INTERNAL) {
-        internalGroups.add(groupId);
-      }
-    }
-
-    if (internalGroups.isEmpty()) {
-      internalGroups = registered;
-    } else {
-      internalGroups.addAll(registered);
-      internalGroups = Collections.unmodifiableSet(internalGroups);
-    }
-
-    return new AccountState(account, internalGroups, externalIds);
-  }
-
-  private AccountState missingAccount(final Account.Id accountId) {
-    final Account account = new Account(accountId);
-    final Collection<AccountExternalId> ids = Collections.emptySet();
-    return new AccountState(account, anonymous, ids);
+  AccountCacheImpl(@Named(BYID_NAME) Cache<Account.Id, AccountState> byId,
+      @Named(BYUSER_NAME) Cache<String, Account.Id> byUsername) {
+    this.byId = byId;
+    this.byName = byUsername;
   }
 
   public AccountState get(final Account.Id accountId) {
@@ -161,7 +77,7 @@
   @Override
   public AccountState getByUsername(String username) {
     Account.Id id = byName.get(username);
-    return id != null ? get(id) : null;
+    return id != null ? byId.get(id) : null;
   }
 
   public void evict(final Account.Id accountId) {
@@ -171,4 +87,99 @@
   public void evictByUsername(String username) {
     byName.remove(username);
   }
+
+  static class ByIdLoader extends EntryCreator<Account.Id, AccountState> {
+    private final SchemaFactory<ReviewDb> schema;
+    private final Set<AccountGroup.Id> registered;
+    private final Set<AccountGroup.Id> anonymous;
+    private final GroupCache groupCache;
+    private final Cache<String, Account.Id> byName;
+
+    @Inject
+    ByIdLoader(SchemaFactory<ReviewDb> sf, AuthConfig auth,
+        GroupCache groupCache,
+        @Named(BYUSER_NAME) Cache<String, Account.Id> byUsername) {
+      this.schema = sf;
+      this.registered = auth.getRegisteredGroups();
+      this.anonymous = auth.getAnonymousGroups();
+      this.groupCache = groupCache;
+      this.byName = byUsername;
+    }
+
+    @Override
+    public AccountState createEntry(final Account.Id key) throws Exception {
+      final ReviewDb db = schema.open();
+      try {
+        final AccountState state = load(db, key);
+        if (state.getUserName() != null) {
+          byName.put(state.getUserName(), state.getAccount().getId());
+        }
+        return state;
+      } finally {
+        db.close();
+      }
+    }
+
+    private AccountState load(final ReviewDb db, final Account.Id who)
+        throws OrmException {
+      final Account account = db.accounts().get(who);
+      if (account == null) {
+        // Account no longer exists? They are anonymous.
+        //
+        return missing(who);
+      }
+
+      final Collection<AccountExternalId> externalIds =
+          Collections.unmodifiableCollection(db.accountExternalIds().byAccount(
+              who).toList());
+
+      Set<AccountGroup.Id> internalGroups = new HashSet<AccountGroup.Id>();
+      for (AccountGroupMember g : db.accountGroupMembers().byAccount(who)) {
+        final AccountGroup.Id groupId = g.getAccountGroupId();
+        final AccountGroup group = groupCache.get(groupId);
+        if (group != null && group.getType() == AccountGroup.Type.INTERNAL) {
+          internalGroups.add(groupId);
+        }
+      }
+
+      if (internalGroups.isEmpty()) {
+        internalGroups = registered;
+      } else {
+        internalGroups.addAll(registered);
+        internalGroups = Collections.unmodifiableSet(internalGroups);
+      }
+
+      return new AccountState(account, internalGroups, externalIds);
+    }
+
+    @Override
+    public AccountState missing(final Account.Id accountId) {
+      final Account account = new Account(accountId);
+      final Collection<AccountExternalId> ids = Collections.emptySet();
+      return new AccountState(account, anonymous, ids);
+    }
+  }
+
+  static class ByNameLoader extends EntryCreator<String, Account.Id> {
+    private final SchemaFactory<ReviewDb> schema;
+
+    @Inject
+    ByNameLoader(final SchemaFactory<ReviewDb> sf) {
+      this.schema = sf;
+    }
+
+    @Override
+    public Account.Id createEntry(final String username) throws Exception {
+      final ReviewDb db = schema.open();
+      try {
+        final AccountExternalId.Key key = new AccountExternalId.Key( //
+            AccountExternalId.SCHEME_USERNAME, //
+            username);
+        final AccountExternalId id = db.accountExternalIds().get(key);
+        return id != null ? id.getAccountId() : null;
+      } finally {
+        db.close();
+      }
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java
index 1509299..e875a19 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java
@@ -91,6 +91,9 @@
   public VoidResult call() throws OrmException, NameAlreadyUsedException,
       InvalidUserNameException {
     final Collection<AccountExternalId> old = old();
+    if (!old.isEmpty()) {
+      throw new IllegalStateException("Username cannot be changed.");
+    }
 
     if (newUsername != null && !newUsername.isEmpty()) {
       if (!USER_NAME_PATTERN.matcher(newUsername).matches()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/ClearPassword.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/ClearPassword.java
new file mode 100644
index 0000000..1fc87fe
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/ClearPassword.java
@@ -0,0 +1,63 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.gerrit.common.errors.NoSuchEntityException;
+import com.google.gerrit.reviewdb.AccountExternalId;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gwtorm.client.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import java.util.Collections;
+import java.util.concurrent.Callable;
+
+/** Operation to clear a password for an account. */
+public class ClearPassword implements Callable<AccountExternalId> {
+  public interface Factory {
+    ClearPassword create(AccountExternalId.Key forUser);
+  }
+
+  private final AccountCache accountCache;
+  private final ReviewDb db;
+  private final IdentifiedUser user;
+
+  private final AccountExternalId.Key forUser;
+
+  @Inject
+  ClearPassword(final AccountCache accountCache, final ReviewDb db,
+      final IdentifiedUser user,
+
+      @Assisted AccountExternalId.Key forUser) {
+    this.accountCache = accountCache;
+    this.db = db;
+    this.user = user;
+
+    this.forUser = forUser;
+  }
+
+  public AccountExternalId call() throws OrmException, NoSuchEntityException {
+    AccountExternalId id = db.accountExternalIds().get(forUser);
+    if (id == null || !user.getAccountId().equals(id.getAccountId())) {
+      throw new NoSuchEntityException();
+    }
+
+    id.setPassword(null);
+    db.accountExternalIds().update(Collections.singleton(id));
+    accountCache.evict(user.getAccountId());
+    return id;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
index 9fedd80..d948aef 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
@@ -19,9 +19,8 @@
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.SelfPopulatingCache;
+import com.google.gerrit.server.cache.EntryCreator;
 import com.google.gerrit.server.config.AuthConfig;
-import com.google.gwtorm.client.OrmException;
 import com.google.gwtorm.client.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Module;
@@ -34,114 +33,45 @@
 /** Tracks group objects in memory for efficient access. */
 @Singleton
 public class GroupCacheImpl implements GroupCache {
-  private static final String CACHE_NAME = "groups";
+  private static final String BYID_NAME = "groups";
+  private static final String BYNAME_NAME = "groups_byname";
+  private static final String BYEXT_NAME = "groups_byext";
 
   public static Module module() {
     return new CacheModule() {
       @Override
       protected void configure() {
-        final TypeLiteral<Cache<com.google.gwtorm.client.Key<?>, AccountGroup>> byId =
-            new TypeLiteral<Cache<com.google.gwtorm.client.Key<?>, AccountGroup>>() {};
-        core(byId, CACHE_NAME);
+        final TypeLiteral<Cache<AccountGroup.Id, AccountGroup>> byId =
+            new TypeLiteral<Cache<AccountGroup.Id, AccountGroup>>() {};
+        core(byId, BYID_NAME).populateWith(ByIdLoader.class);
+
+        final TypeLiteral<Cache<AccountGroup.NameKey, AccountGroup>> byName =
+            new TypeLiteral<Cache<AccountGroup.NameKey, AccountGroup>>() {};
+        core(byName, BYNAME_NAME).populateWith(ByNameLoader.class);
+
+        final TypeLiteral<Cache<AccountGroup.ExternalNameKey, Collection<AccountGroup>>> byExternalName =
+            new TypeLiteral<Cache<AccountGroup.ExternalNameKey, Collection<AccountGroup>>>() {};
+        core(byExternalName, BYEXT_NAME) //
+            .populateWith(ByExternalNameLoader.class);
+
         bind(GroupCacheImpl.class);
         bind(GroupCache.class).to(GroupCacheImpl.class);
       }
     };
   }
 
-  private final SchemaFactory<ReviewDb> schema;
-  private final AccountGroup.Id administrators;
-  private final SelfPopulatingCache<AccountGroup.Id, AccountGroup> byId;
-  private final SelfPopulatingCache<AccountGroup.NameKey, AccountGroup> byName;
-  private final SelfPopulatingCache<AccountGroup.ExternalNameKey, Collection<AccountGroup>> byExternalName;
+  private final Cache<AccountGroup.Id, AccountGroup> byId;
+  private final Cache<AccountGroup.NameKey, AccountGroup> byName;
+  private final Cache<AccountGroup.ExternalNameKey, Collection<AccountGroup>> byExternalName;
 
   @Inject
   GroupCacheImpl(
-      final SchemaFactory<ReviewDb> sf,
-      final AuthConfig authConfig,
-      @Named(CACHE_NAME) final Cache<com.google.gwtorm.client.Key<?>, AccountGroup> rawAny) {
-    schema = sf;
-    administrators = authConfig.getAdministratorsGroup();
-
-    byId =
-        new SelfPopulatingCache<AccountGroup.Id, AccountGroup>((Cache) rawAny) {
-          @Override
-          public AccountGroup createEntry(final AccountGroup.Id key)
-              throws Exception {
-            return lookup(key);
-          }
-
-          @Override
-          protected AccountGroup missing(final AccountGroup.Id key) {
-            return missingGroup(key);
-          }
-        };
-
-    byName =
-        new SelfPopulatingCache<AccountGroup.NameKey, AccountGroup>(
-            (Cache) rawAny) {
-          @Override
-          public AccountGroup createEntry(final AccountGroup.NameKey key)
-              throws Exception {
-            return lookup(key);
-          }
-        };
-
-    byExternalName =
-        new SelfPopulatingCache<AccountGroup.ExternalNameKey, Collection<AccountGroup>>(
-            (Cache) rawAny) {
-          @Override
-          public Collection<AccountGroup> createEntry(
-              final AccountGroup.ExternalNameKey key) throws Exception {
-            return lookup(key);
-          }
-        };
-  }
-
-  private AccountGroup lookup(final AccountGroup.Id groupId)
-      throws OrmException {
-    final ReviewDb db = schema.open();
-    try {
-      final AccountGroup group = db.accountGroups().get(groupId);
-      if (group != null) {
-        return group;
-      } else {
-        return missingGroup(groupId);
-      }
-    } finally {
-      db.close();
-    }
-  }
-
-  private AccountGroup missingGroup(final AccountGroup.Id groupId) {
-    final AccountGroup.NameKey name =
-        new AccountGroup.NameKey("Deleted Group" + groupId.toString());
-    final AccountGroup g = new AccountGroup(name, groupId);
-    g.setType(AccountGroup.Type.SYSTEM);
-    g.setOwnerGroupId(administrators);
-    return g;
-  }
-
-  private AccountGroup lookup(final AccountGroup.NameKey name)
-      throws OrmException {
-    final AccountGroupName r;
-    final ReviewDb db = schema.open();
-    try {
-      r = db.accountGroupNames().get(name);
-    } finally {
-      db.close();
-    }
-    return r != null ? get(r.getId()) : null;
-  }
-
-  private Collection<AccountGroup> lookup(
-      final AccountGroup.ExternalNameKey name) throws OrmException {
-    final ReviewDb db = schema.open();
-    try {
-      return db.accountGroups().byExternalName(name).toList();
-    } finally {
-      db.close();
-    }
+      @Named(BYID_NAME) Cache<AccountGroup.Id, AccountGroup> byId,
+      @Named(BYNAME_NAME) Cache<AccountGroup.NameKey, AccountGroup> byName,
+      @Named(BYEXT_NAME) Cache<AccountGroup.ExternalNameKey, Collection<AccountGroup>> byExternalName) {
+    this.byId = byId;
+    this.byName = byName;
+    this.byExternalName = byExternalName;
   }
 
   public AccountGroup get(final AccountGroup.Id groupId) {
@@ -166,4 +96,88 @@
       final AccountGroup.ExternalNameKey externalName) {
     return byExternalName.get(externalName);
   }
+
+  static class ByIdLoader extends EntryCreator<AccountGroup.Id, AccountGroup> {
+    private final SchemaFactory<ReviewDb> schema;
+    private final AccountGroup.Id administrators;
+
+    @Inject
+    ByIdLoader(final SchemaFactory<ReviewDb> sf, final AuthConfig authConfig) {
+      schema = sf;
+      administrators = authConfig.getAdministratorsGroup();
+    }
+
+    @Override
+    public AccountGroup createEntry(final AccountGroup.Id key) throws Exception {
+      final ReviewDb db = schema.open();
+      try {
+        final AccountGroup group = db.accountGroups().get(key);
+        if (group != null) {
+          return group;
+        } else {
+          return missing(key);
+        }
+      } finally {
+        db.close();
+      }
+    }
+
+    @Override
+    public AccountGroup missing(final AccountGroup.Id key) {
+      final AccountGroup.NameKey name =
+          new AccountGroup.NameKey("Deleted Group" + key.toString());
+      final AccountGroup g = new AccountGroup(name, key);
+      g.setType(AccountGroup.Type.SYSTEM);
+      g.setOwnerGroupId(administrators);
+      return g;
+    }
+  }
+
+  static class ByNameLoader extends
+      EntryCreator<AccountGroup.NameKey, AccountGroup> {
+    private final SchemaFactory<ReviewDb> schema;
+
+    @Inject
+    ByNameLoader(final SchemaFactory<ReviewDb> sf) {
+      schema = sf;
+    }
+
+    @Override
+    public AccountGroup createEntry(final AccountGroup.NameKey key)
+        throws Exception {
+      final AccountGroupName r;
+      final ReviewDb db = schema.open();
+      try {
+        r = db.accountGroupNames().get(key);
+        if (r != null) {
+          return db.accountGroups().get(r.getId());
+        } else {
+          return null;
+        }
+      } finally {
+        db.close();
+      }
+    }
+  }
+
+  static class ByExternalNameLoader extends
+      EntryCreator<AccountGroup.ExternalNameKey, Collection<AccountGroup>> {
+    private final SchemaFactory<ReviewDb> schema;
+
+    @Inject
+    ByExternalNameLoader(final SchemaFactory<ReviewDb> sf) {
+      schema = sf;
+    }
+
+    @Override
+    public Collection<AccountGroup> createEntry(
+        final AccountGroup.ExternalNameKey key) throws Exception {
+      final ReviewDb db = schema.open();
+      try {
+        return db.accountGroups().byExternalName(key).toList();
+      } finally {
+        db.close();
+      }
+    }
+  }
 }
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
new file mode 100644
index 0000000..6f6a4d4
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java
@@ -0,0 +1,316 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.auth.ldap;
+
+import com.google.gerrit.common.data.ParamertizedString;
+import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.server.account.AccountException;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.util.ssl.BlindSSLSocketFactory;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Properties;
+import java.util.Set;
+
+import javax.naming.Context;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.DirContext;
+import javax.naming.directory.InitialDirContext;
+import javax.net.ssl.SSLSocketFactory;
+
+@Singleton class Helper {
+  private final GroupCache groupCache;
+  private final Config config;
+  private final String server;
+  private final String username;
+  private final String password;
+  private final String referral;
+  private final boolean sslVerify;
+  private volatile LdapSchema ldapSchema;
+
+  @Inject
+  Helper(@GerritServerConfig final Config config, final GroupCache groupCache) {
+    this.groupCache = groupCache;
+    this.config = config;
+    this.server = LdapRealm.required(config, "server");
+    this.username = LdapRealm.optional(config, "username");
+    this.password = LdapRealm.optional(config, "password");
+    this.referral = LdapRealm.optional(config, "referral");
+    this.sslVerify = config.getBoolean("ldap", "sslverify", true);
+  }
+
+  private Properties createContextProperties() {
+    final Properties env = new Properties();
+    env.put(Context.INITIAL_CONTEXT_FACTORY, LdapRealm.LDAP);
+    env.put(Context.PROVIDER_URL, server);
+    if (server.startsWith("ldaps:") && !sslVerify) {
+      Class<? extends SSLSocketFactory> factory = BlindSSLSocketFactory.class;
+      env.put("java.naming.ldap.factory.socket", factory.getName());
+    }
+    return env;
+  }
+
+  DirContext open() throws NamingException {
+    final Properties env = createContextProperties();
+    if (username != null) {
+      env.put(Context.SECURITY_AUTHENTICATION, "simple");
+      env.put(Context.SECURITY_PRINCIPAL, username);
+      env.put(Context.SECURITY_CREDENTIALS, password != null ? password : "");
+      env.put(Context.REFERRAL, referral != null ? referral : "ignore");
+    }
+    return new InitialDirContext(env);
+  }
+
+  DirContext authenticate(String dn, String password) throws AccountException {
+    final Properties env = createContextProperties();
+    env.put(Context.SECURITY_AUTHENTICATION, "simple");
+    env.put(Context.SECURITY_PRINCIPAL, dn);
+    env.put(Context.SECURITY_CREDENTIALS, password != null ? password : "");
+    env.put(Context.REFERRAL, referral != null ? referral : "ignore");
+    try {
+      return new InitialDirContext(env);
+    } catch (NamingException e) {
+      throw new AccountException("Incorrect username or password", e);
+    }
+  }
+
+  LdapSchema getSchema(DirContext ctx) {
+    if (ldapSchema == null) {
+      synchronized (this) {
+        if (ldapSchema == null) {
+          ldapSchema = new LdapSchema(ctx);
+        }
+      }
+    }
+    return ldapSchema;
+  }
+
+  LdapQuery.Result findAccount(final Helper.LdapSchema schema,
+      final DirContext ctx, final String username) throws NamingException,
+      AccountException {
+    final HashMap<String, String> params = new HashMap<String, String>();
+    params.put(LdapRealm.USERNAME, username);
+
+    final List<LdapQuery.Result> res = new ArrayList<LdapQuery.Result>();
+    for (LdapQuery accountQuery : schema.accountQueryList) {
+      res.addAll(accountQuery.query(ctx, params));
+    }
+
+    switch (res.size()) {
+      case 0:
+        throw new AccountException("No such user:" + username);
+
+      case 1:
+        return res.get(0);
+
+      default:
+        throw new AccountException("Duplicate users: " + username);
+    }
+  }
+
+  Set<AccountGroup.Id> queryForGroups(final DirContext ctx,
+      final String username, LdapQuery.Result account)
+      throws NamingException, AccountException {
+    final LdapSchema schema = getSchema(ctx);
+    final Set<String> groupDNs = new HashSet<String>();
+
+    if (!schema.groupMemberQueryList.isEmpty()) {
+      final HashMap<String, String> params = new HashMap<String, String>();
+
+      if (schema.groupNeedsAccount) {
+        if (account == null) {
+          account = findAccount(schema, ctx, username);
+        }
+        for (String name : schema.groupMemberQueryList.get(0).getParameters()) {
+          params.put(name, account.get(name));
+        }
+      }
+
+      params.put(LdapRealm.USERNAME, username);
+
+      for (LdapQuery groupMemberQuery : schema.groupMemberQueryList) {
+        for (LdapQuery.Result r : groupMemberQuery.query(ctx, params)) {
+          recursivelyExpandGroups(groupDNs, schema, ctx, r.getDN());
+        }
+      }
+    }
+
+    if (schema.accountMemberField != null) {
+      if (account == null) {
+        account = findAccount(schema, ctx, username);
+      }
+
+      final Attribute groupAtt = account.getAll(schema.accountMemberField);
+      if (groupAtt != null) {
+        final NamingEnumeration<?> groups = groupAtt.getAll();
+        while (groups.hasMore()) {
+          final String nextDN = (String) groups.next();
+          recursivelyExpandGroups(groupDNs, schema, ctx, nextDN);
+        }
+      }
+    }
+
+    final Set<AccountGroup.Id> actual = new HashSet<AccountGroup.Id>();
+    for (String dn : groupDNs) {
+      for (AccountGroup group : groupCache
+          .get(new AccountGroup.ExternalNameKey(dn))) {
+        if (group.getType() == AccountGroup.Type.LDAP) {
+          actual.add(group.getId());
+        }
+      }
+    }
+
+    if (actual.isEmpty()) {
+      return Collections.emptySet();
+    } else {
+      return Collections.unmodifiableSet(actual);
+    }
+  }
+
+  private void recursivelyExpandGroups(final Set<String> groupDNs,
+      final LdapSchema schema, final DirContext ctx, final String groupDN) {
+    if (groupDNs.add(groupDN) && schema.accountMemberField != null) {
+      // Recursively identify the groups it is a member of.
+      //
+      try {
+        final Attribute in =
+            ctx.getAttributes(groupDN).get(schema.accountMemberField);
+        if (in != null) {
+          final NamingEnumeration<?> groups = in.getAll();
+          while (groups.hasMore()) {
+            final String nextDN = (String) groups.next();
+            recursivelyExpandGroups(groupDNs, schema, ctx, nextDN);
+          }
+        }
+      } catch (NamingException e) {
+        LdapRealm.log.warn("Could not find group " + groupDN, e);
+      }
+    }
+  }
+
+  class LdapSchema {
+    final LdapType type;
+
+    final ParamertizedString accountFullName;
+    final ParamertizedString accountEmailAddress;
+    final ParamertizedString accountSshUserName;
+    final String accountMemberField;
+    final List<LdapQuery> accountQueryList;
+
+    boolean groupNeedsAccount;
+    final List<String> groupBases;
+    final SearchScope groupScope;
+    final ParamertizedString groupPattern;
+    final List<LdapQuery> groupMemberQueryList;
+
+    LdapSchema(final DirContext ctx) {
+      type = discoverLdapType(ctx);
+      groupMemberQueryList = new ArrayList<LdapQuery>();
+      accountQueryList = new ArrayList<LdapQuery>();
+
+      final Set<String> accountAtts = new HashSet<String>();
+
+      // Group query
+      //
+
+      groupBases = LdapRealm.optionalList(config, "groupBase");
+      groupScope = LdapRealm.scope(config, "groupScope");
+      groupPattern = LdapRealm.paramString(config, "groupPattern", type.groupPattern());
+      final String groupMemberPattern =
+          LdapRealm.optdef(config, "groupMemberPattern", type.groupMemberPattern());
+
+      for (String groupBase : groupBases) {
+        if (groupMemberPattern != null) {
+          final LdapQuery groupMemberQuery =
+              new LdapQuery(groupBase, groupScope, new ParamertizedString(
+                  groupMemberPattern), Collections.<String> emptySet());
+          if (groupMemberQuery.getParameters().isEmpty()) {
+            throw new IllegalArgumentException(
+                "No variables in ldap.groupMemberPattern");
+          }
+
+          for (final String name : groupMemberQuery.getParameters()) {
+            if (!LdapRealm.USERNAME.equals(name)) {
+              groupNeedsAccount = true;
+              accountAtts.add(name);
+            }
+          }
+
+          groupMemberQueryList.add(groupMemberQuery);
+        }
+      }
+
+      // Account query
+      //
+      accountFullName =
+          LdapRealm.paramString(config, "accountFullName", type.accountFullName());
+      if (accountFullName != null) {
+        accountAtts.addAll(accountFullName.getParameterNames());
+      }
+      accountEmailAddress =
+          LdapRealm.paramString(config, "accountEmailAddress", type
+              .accountEmailAddress());
+      if (accountEmailAddress != null) {
+        accountAtts.addAll(accountEmailAddress.getParameterNames());
+      }
+      accountSshUserName =
+          LdapRealm.paramString(config, "accountSshUserName", type.accountSshUserName());
+      if (accountSshUserName != null) {
+        accountAtts.addAll(accountSshUserName.getParameterNames());
+      }
+      accountMemberField =
+          LdapRealm.optdef(config, "accountMemberField", type.accountMemberField());
+      if (accountMemberField != null) {
+        accountAtts.add(accountMemberField);
+      }
+
+      final SearchScope accountScope = LdapRealm.scope(config, "accountScope");
+      final String accountPattern =
+          LdapRealm.reqdef(config, "accountPattern", type.accountPattern());
+
+      for (String accountBase : LdapRealm.requiredList(config, "accountBase")) {
+        final LdapQuery accountQuery =
+            new LdapQuery(accountBase, accountScope, new ParamertizedString(
+                accountPattern), accountAtts);
+        if (accountQuery.getParameters().isEmpty()) {
+          throw new IllegalArgumentException(
+              "No variables in ldap.accountPattern");
+        }
+        accountQueryList.add(accountQuery);
+      }
+    }
+
+    LdapType discoverLdapType(DirContext ctx) {
+      try {
+        return LdapType.guessType(ctx);
+      } catch (NamingException e) {
+        LdapRealm.log.warn("Cannot discover type of LDAP server at " + server
+            + ", assuming the server is RFC 2307 compliant.", e);
+        return LdapType.RFC_2307;
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java
index 352e466..810df28 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java
@@ -34,11 +34,15 @@
   protected void configure() {
     final TypeLiteral<Cache<String, Set<AccountGroup.Id>>> groups =
         new TypeLiteral<Cache<String, Set<AccountGroup.Id>>>() {};
+    core(groups, GROUP_CACHE).maxAge(1, HOURS) //
+        .populateWith(LdapRealm.MemberLoader.class);
+
     final TypeLiteral<Cache<String, Account.Id>> usernames =
         new TypeLiteral<Cache<String, Account.Id>>() {};
+    core(usernames, USERNAME_CACHE) //
+        .populateWith(LdapRealm.UserLoader.class);
 
-    core(groups, GROUP_CACHE).maxAge(1, HOURS);
-    core(usernames, USERNAME_CACHE);
     bind(Realm.class).to(LdapRealm.class).in(Scopes.SINGLETON);
+    bind(Helper.class);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
index bad97e8..de33b44 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
@@ -26,14 +26,13 @@
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.EmailExpander;
-import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.account.Realm;
+import com.google.gerrit.server.auth.ldap.Helper.LdapSchema;
 import com.google.gerrit.server.cache.Cache;
-import com.google.gerrit.server.cache.SelfPopulatingCache;
+import com.google.gerrit.server.cache.EntryCreator;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.util.ssl.BlindSSLSocketFactory;
 import com.google.gwtorm.client.OrmException;
 import com.google.gwtorm.client.SchemaFactory;
 import com.google.inject.Inject;
@@ -44,7 +43,6 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
@@ -52,62 +50,40 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Properties;
 import java.util.Set;
 
-import javax.naming.Context;
-import javax.naming.NamingEnumeration;
 import javax.naming.NamingException;
-import javax.naming.directory.Attribute;
 import javax.naming.directory.DirContext;
-import javax.naming.directory.InitialDirContext;
-import javax.net.ssl.SSLSocketFactory;
 
 @Singleton
 class LdapRealm implements Realm {
-  private static final Logger log = LoggerFactory.getLogger(LdapRealm.class);
-  private static final String LDAP = "com.sun.jndi.ldap.LdapCtxFactory";
-  private static final String USERNAME = "username";
+  static final Logger log = LoggerFactory.getLogger(LdapRealm.class);
+  static final String LDAP = "com.sun.jndi.ldap.LdapCtxFactory";
+  static final String USERNAME = "username";
   private static final String GROUPNAME = "groupname";
 
-  private final Config config;
-  private final String server;
-  private final String username;
-  private final String password;
-  private final String referral;
-  private final boolean sslVerify;
-
+  private final Helper helper;
   private final AuthConfig authConfig;
-  private final SchemaFactory<ReviewDb> schema;
   private final EmailExpander emailExpander;
-  private final SelfPopulatingCache<String, Account.Id> usernameCache;
+  private final Cache<String, Account.Id> usernameCache;
   private final Set<Account.FieldName> readOnlyAccountFields;
 
-  private final GroupCache groupCache;
-  private final SelfPopulatingCache<String, Set<AccountGroup.Id>> membershipCache;
-
-  private volatile LdapSchema ldapSchema;
+  private final Cache<String, Set<AccountGroup.Id>> membershipCache;
 
   @Inject
   LdapRealm(
+      final Helper helper,
       final AuthConfig authConfig,
-      final GroupCache groupCache,
       final EmailExpander emailExpander,
-      final SchemaFactory<ReviewDb> schema,
-      @Named(LdapModule.GROUP_CACHE) final Cache<String, Set<AccountGroup.Id>> rawGroup,
-      @Named(LdapModule.USERNAME_CACHE) final Cache<String, Account.Id> rawUsername,
+      @Named(LdapModule.GROUP_CACHE) final Cache<String, Set<AccountGroup.Id>> membershipCache,
+      @Named(LdapModule.USERNAME_CACHE) final Cache<String, Account.Id> usernameCache,
       @GerritServerConfig final Config config) {
-    this.config = config;
+    this.helper = helper;
     this.authConfig = authConfig;
-    this.groupCache = groupCache;
     this.emailExpander = emailExpander;
-    this.schema = schema;
+    this.usernameCache = usernameCache;
+    this.membershipCache = membershipCache;
 
-    this.server = required(config, "server");
-    this.username = optional(config, "username");
-    this.password = optional(config, "password");
-    this.referral = optional(config, "referral");
-    this.sslVerify = config.getBoolean("ldap", "sslverify", true);
     this.readOnlyAccountFields = new HashSet<Account.FieldName>();
 
     if (optdef(config, "accountFullName", "DEFAULT") != null) {
@@ -116,38 +92,17 @@
     if (optdef(config, "accountSshUserName", "DEFAULT") != null) {
       readOnlyAccountFields.add(Account.FieldName.USER_NAME);
     }
-
-    membershipCache =
-        new SelfPopulatingCache<String, Set<AccountGroup.Id>>(rawGroup) {
-          @Override
-          public Set<AccountGroup.Id> createEntry(final String username)
-              throws Exception {
-            return queryForGroups(username);
-          }
-
-          @Override
-          protected Set<AccountGroup.Id> missing(final String key) {
-            return Collections.emptySet();
-          }
-        };
-
-    usernameCache = new SelfPopulatingCache<String, Account.Id>(rawUsername) {
-      @Override
-      public Account.Id createEntry(final String username) throws Exception {
-        return queryForUsername(username);
-      }
-    };
   }
 
-  private static SearchScope scope(final Config c, final String setting) {
+  static SearchScope scope(final Config c, final String setting) {
     return ConfigUtil.getEnum(c, "ldap", null, setting, SearchScope.SUBTREE);
   }
 
-  private static String optional(final Config config, final String name) {
+  static String optional(final Config config, final String name) {
     return config.getString("ldap", null, name);
   }
 
-  private static String required(final Config config, final String name) {
+  static String required(final Config config, final String name) {
     final String v = optional(config, name);
     if (v == null || "".equals(v)) {
       throw new IllegalArgumentException("No ldap." + name + " configured");
@@ -155,13 +110,13 @@
     return v;
   }
 
-  private static List<String> optionalList(final Config config,
+  static List<String> optionalList(final Config config,
       final String name) {
     String s[] = config.getStringList("ldap", null, name);
     return Arrays.asList(s);
   }
 
-  private static List<String> requiredList(final Config config,
+  static List<String> requiredList(final Config config,
       final String name) {
     List<String> vlist = optionalList(config, name);
 
@@ -172,7 +127,7 @@
     return vlist;
   }
 
-  private static String optdef(final Config c, final String n, final String d) {
+  static String optdef(final Config c, final String n, final String d) {
     final String[] v = c.getStringList("ldap", null, n);
     if (v == null || v.length == 0) {
       return d;
@@ -185,7 +140,7 @@
     }
   }
 
-  private static String reqdef(final Config c, final String n, final String d) {
+  static String reqdef(final Config c, final String n, final String d) {
     final String v = optdef(c, n, d);
     if (v == null) {
       throw new IllegalArgumentException("No ldap." + n + " configured");
@@ -193,7 +148,7 @@
     return v;
   }
 
-  private static ParamertizedString paramString(Config c, String n, String d) {
+  static ParamertizedString paramString(Config c, String n, String d) {
     String expression = optdef(c, n, d);
     if (expression == null) {
       return null;
@@ -230,19 +185,19 @@
     try {
       final DirContext ctx;
       if (authConfig.getAuthType() == AuthType.LDAP_BIND) {
-        ctx = authenticate(username, who.getPassword());
+        ctx = helper.authenticate(username, who.getPassword());
       } else {
-        ctx = open();
+        ctx = helper.open();
       }
       try {
-        final LdapSchema schema = getSchema(ctx);
-        final LdapQuery.Result m = findAccount(schema, ctx, username);
+        final Helper.LdapSchema schema = helper.getSchema(ctx);
+        final LdapQuery.Result m = helper.findAccount(schema, ctx, username);
 
         if (authConfig.getAuthType() == AuthType.LDAP) {
           // We found the user account, but we need to verify
           // the password matches it before we can continue.
           //
-          authenticate(m.getDN(), who.getPassword());
+          helper.authenticate(m.getDN(), who.getPassword());
         }
 
         who.setDisplayName(apply(schema.accountFullName, m));
@@ -265,7 +220,7 @@
         // in the middle of authenticating the user, its likely we will
         // need to know what access rights they have soon.
         //
-        membershipCache.put(username, queryForGroups(ctx, username, m));
+        membershipCache.put(username, helper.queryForGroups(ctx, username, m));
         return who;
       } finally {
         try {
@@ -293,99 +248,6 @@
     return r;
   }
 
-  private Set<AccountGroup.Id> queryForGroups(final String username)
-      throws NamingException, AccountException {
-    final DirContext ctx = open();
-    try {
-      return queryForGroups(ctx, username, null);
-    } finally {
-      try {
-        ctx.close();
-      } catch (NamingException e) {
-        log.warn("Cannot close LDAP query handle", e);
-      }
-    }
-  }
-
-  private Set<AccountGroup.Id> queryForGroups(final DirContext ctx,
-      final String username, LdapQuery.Result account) throws NamingException,
-      AccountException {
-    final LdapSchema schema = getSchema(ctx);
-    final Set<String> groupDNs = new HashSet<String>();
-
-    if (!schema.groupMemberQueryList.isEmpty()) {
-      final HashMap<String, String> params = new HashMap<String, String>();
-
-      if (schema.groupNeedsAccount) {
-        if (account == null) {
-          account = findAccount(schema, ctx, username);
-        }
-        for (String name : schema.groupMemberQueryList.get(0).getParameters()) {
-          params.put(name, account.get(name));
-        }
-      }
-
-      params.put(USERNAME, username);
-
-      for (LdapQuery groupMemberQuery : schema.groupMemberQueryList) {
-        for (LdapQuery.Result r : groupMemberQuery.query(ctx, params)) {
-          recursivelyExpandGroups(groupDNs, schema, ctx, r.getDN());
-        }
-      }
-    }
-
-    if (schema.accountMemberField != null) {
-      if (account == null) {
-        account = findAccount(schema, ctx, username);
-      }
-
-      final Attribute groupAtt = account.getAll(schema.accountMemberField);
-      if (groupAtt != null) {
-        final NamingEnumeration<?> groups = groupAtt.getAll();
-        while (groups.hasMore()) {
-          final String nextDN = (String) groups.next();
-          recursivelyExpandGroups(groupDNs, schema, ctx, nextDN);
-        }
-      }
-    }
-
-    final Set<AccountGroup.Id> actual = new HashSet<AccountGroup.Id>();
-    for (String dn : groupDNs) {
-      for (AccountGroup group : groupCache
-          .get(new AccountGroup.ExternalNameKey(dn))) {
-        if (group.getType() == AccountGroup.Type.LDAP) {
-          actual.add(group.getId());
-        }
-      }
-    }
-
-    if (actual.isEmpty()) {
-      return Collections.emptySet();
-    } else {
-      return Collections.unmodifiableSet(actual);
-    }
-  }
-
-  private void recursivelyExpandGroups(final Set<String> groupDNs,
-      final LdapSchema schema, final DirContext ctx, final String groupDN) {
-    if (groupDNs.add(groupDN) && schema.accountMemberField != null) {
-      // Recursively identify the groups it is a member of.
-      //
-      try {
-        final Attribute in =
-            ctx.getAttributes(groupDN).get(schema.accountMemberField);
-        if (in != null) {
-          final NamingEnumeration<?> groups = in.getAll();
-          while (groups.hasMore()) {
-            final String nextDN = (String) groups.next();
-            recursivelyExpandGroups(groupDNs, schema, ctx, nextDN);
-          }
-        }
-      } catch (NamingException e) {
-        log.warn("Could not find group " + groupDN, e);
-      }
-    }
-  }
 
   private static String findId(final Collection<AccountExternalId> ids) {
     for (final AccountExternalId i : ids) {
@@ -408,9 +270,9 @@
 
     out = new HashSet<AccountGroup.ExternalNameKey>();
     try {
-      final DirContext ctx = open();
+      final DirContext ctx = helper.open();
       try {
-        final LdapSchema schema = getSchema(ctx);
+        final LdapSchema schema = helper.getSchema(ctx);
         final ParamertizedString filter =
             ParamertizedString.asis(schema.groupPattern
                 .replace(GROUPNAME, name).toString());
@@ -435,192 +297,59 @@
     return out;
   }
 
-  private Account.Id queryForUsername(final String username) {
-    try {
-      final ReviewDb db = schema.open();
+  static class UserLoader extends EntryCreator<String, Account.Id> {
+    private final SchemaFactory<ReviewDb> schema;
+
+    @Inject
+    UserLoader(SchemaFactory<ReviewDb> schema) {
+      this.schema = schema;
+    }
+
+    @Override
+    public Account.Id createEntry(final String username) throws Exception {
       try {
-        final AccountExternalId extId =
-            db.accountExternalIds().get(
-                new AccountExternalId.Key(SCHEME_GERRIT, username));
-        return extId != null ? extId.getAccountId() : null;
+        final ReviewDb db = schema.open();
+        try {
+          final AccountExternalId extId =
+              db.accountExternalIds().get(
+                  new AccountExternalId.Key(SCHEME_GERRIT, username));
+          return extId != null ? extId.getAccountId() : null;
+        } finally {
+          db.close();
+        }
+      } catch (OrmException e) {
+        log.warn("Cannot query for username in database", e);
+        return null;
+      }
+    }
+  }
+
+  static class MemberLoader extends EntryCreator<String, Set<AccountGroup.Id>> {
+    private final Helper helper;
+
+    @Inject
+    MemberLoader(final Helper helper) {
+      this.helper = helper;
+    }
+
+    @Override
+    public Set<AccountGroup.Id> createEntry(final String username)
+        throws Exception {
+      final DirContext ctx = helper.open();
+      try {
+        return helper.queryForGroups(ctx, username, null);
       } finally {
-        db.close();
-      }
-    } catch (OrmException e) {
-      log.warn("Cannot query for username in database", e);
-      return null;
-    }
-  }
-
-  private Properties createContextProperties() {
-    final Properties env = new Properties();
-    env.put(Context.INITIAL_CONTEXT_FACTORY, LDAP);
-    env.put(Context.PROVIDER_URL, server);
-    if (server.startsWith("ldaps:") && !sslVerify) {
-      Class<? extends SSLSocketFactory> factory = BlindSSLSocketFactory.class;
-      env.put("java.naming.ldap.factory.socket", factory.getName());
-    }
-    return env;
-  }
-
-  private DirContext open() throws NamingException {
-    final Properties env = createContextProperties();
-    if (username != null) {
-      env.put(Context.SECURITY_AUTHENTICATION, "simple");
-      env.put(Context.SECURITY_PRINCIPAL, username);
-      env.put(Context.SECURITY_CREDENTIALS, password != null ? password : "");
-      env.put(Context.REFERRAL, referral != null ? referral : "ignore");
-    }
-    return new InitialDirContext(env);
-  }
-
-  private DirContext authenticate(String dn, String password)
-      throws AccountException {
-    final Properties env = createContextProperties();
-    env.put(Context.SECURITY_AUTHENTICATION, "simple");
-    env.put(Context.SECURITY_PRINCIPAL, dn);
-    env.put(Context.SECURITY_CREDENTIALS, password != null ? password : "");
-    env.put(Context.REFERRAL, referral != null ? referral : "ignore");
-    try {
-      return new InitialDirContext(env);
-    } catch (NamingException e) {
-      throw new AccountException("Incorrect username or password", e);
-    }
-  }
-
-  private LdapQuery.Result findAccount(final LdapSchema schema,
-      final DirContext ctx, final String username) throws NamingException,
-      AccountException {
-    final HashMap<String, String> params = new HashMap<String, String>();
-    params.put(USERNAME, username);
-
-    final List<LdapQuery.Result> res = new ArrayList<LdapQuery.Result>();
-    for (LdapQuery accountQuery : schema.accountQueryList) {
-      res.addAll(accountQuery.query(ctx, params));
-    }
-
-    switch (res.size()) {
-      case 0:
-        throw new AccountException("No such user:" + username);
-
-      case 1:
-        return res.get(0);
-
-      default:
-        throw new AccountException("Duplicate users: " + username);
-    }
-  }
-
-  private LdapSchema getSchema(DirContext ctx) {
-    if (ldapSchema == null) {
-      synchronized (this) {
-        if (ldapSchema == null) {
-          ldapSchema = new LdapSchema(ctx);
+        try {
+          ctx.close();
+        } catch (NamingException e) {
+          log.warn("Cannot close LDAP query handle", e);
         }
       }
     }
-    return ldapSchema;
-  }
 
-  private class LdapSchema {
-    final LdapType type;
-
-    final ParamertizedString accountFullName;
-    final ParamertizedString accountEmailAddress;
-    final ParamertizedString accountSshUserName;
-    final String accountMemberField;
-    final List<LdapQuery> accountQueryList;
-
-    boolean groupNeedsAccount;
-    final List<String> groupBases;
-    final SearchScope groupScope;
-    final ParamertizedString groupPattern;
-    final List<LdapQuery> groupMemberQueryList;
-
-    LdapSchema(final DirContext ctx) {
-      type = discoverLdapType(ctx);
-      groupMemberQueryList = new ArrayList<LdapQuery>();
-      accountQueryList = new ArrayList<LdapQuery>();
-
-      final Set<String> accountAtts = new HashSet<String>();
-
-      // Group query
-      //
-
-      groupBases = optionalList(config, "groupBase");
-      groupScope = scope(config, "groupScope");
-      groupPattern = paramString(config, "groupPattern", type.groupPattern());
-      final String groupMemberPattern =
-          optdef(config, "groupMemberPattern", type.groupMemberPattern());
-
-      for (String groupBase : groupBases) {
-        if (groupMemberPattern != null) {
-          final LdapQuery groupMemberQuery =
-              new LdapQuery(groupBase, groupScope, new ParamertizedString(
-                  groupMemberPattern), Collections.<String> emptySet());
-          if (groupMemberQuery.getParameters().isEmpty()) {
-            throw new IllegalArgumentException(
-                "No variables in ldap.groupMemberPattern");
-          }
-
-          for (final String name : groupMemberQuery.getParameters()) {
-            if (!USERNAME.equals(name)) {
-              groupNeedsAccount = true;
-              accountAtts.add(name);
-            }
-          }
-
-          groupMemberQueryList.add(groupMemberQuery);
-        }
-      }
-
-      // Account query
-      //
-      accountFullName =
-          paramString(config, "accountFullName", type.accountFullName());
-      if (accountFullName != null) {
-        accountAtts.addAll(accountFullName.getParameterNames());
-      }
-      accountEmailAddress =
-          paramString(config, "accountEmailAddress", type.accountEmailAddress());
-      if (accountEmailAddress != null) {
-        accountAtts.addAll(accountEmailAddress.getParameterNames());
-      }
-      accountSshUserName =
-          paramString(config, "accountSshUserName", type.accountSshUserName());
-      if (accountSshUserName != null) {
-        accountAtts.addAll(accountSshUserName.getParameterNames());
-      }
-      accountMemberField =
-          optdef(config, "accountMemberField", type.accountMemberField());
-      if (accountMemberField != null) {
-        accountAtts.add(accountMemberField);
-      }
-
-      final SearchScope accountScope = scope(config, "accountScope");
-      final String accountPattern =
-          reqdef(config, "accountPattern", type.accountPattern());
-
-      for (String accountBase : requiredList(config, "accountBase")) {
-        final LdapQuery accountQuery =
-            new LdapQuery(accountBase, accountScope, new ParamertizedString(
-                accountPattern), accountAtts);
-        if (accountQuery.getParameters().isEmpty()) {
-          throw new IllegalArgumentException(
-              "No variables in ldap.accountPattern");
-        }
-        accountQueryList.add(accountQuery);
-      }
-    }
-
-    LdapType discoverLdapType(DirContext ctx) {
-      try {
-        return LdapType.guessType(ctx);
-      } catch (NamingException e) {
-        log.warn("Cannot discover type of LDAP server at " + server
-            + ", assuming the server is RFC 2307 compliant.", e);
-        return LdapType.RFC_2307;
-      }
+    @Override
+    public Set<AccountGroup.Id> missing(final String key) {
+      return Collections.emptySet();
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/Cache.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/Cache.java
index 86979b8..7159501 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/Cache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/Cache.java
@@ -33,6 +33,9 @@
   /** Remove any existing value from the cache, no-op if not present. */
   public void remove(K key);
 
+  /** Remove all cached items. */
+  public void removeAll();
+
   /**
    * Get the time an element will survive in the cache.
    *
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheModule.java
index 23c094e..7fb3b3b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheModule.java
@@ -16,7 +16,10 @@
 
 import com.google.inject.AbstractModule;
 import com.google.inject.Key;
+import com.google.inject.Provider;
+import com.google.inject.Scopes;
 import com.google.inject.TypeLiteral;
+import com.google.inject.internal.UniqueAnnotations;
 import com.google.inject.name.Names;
 
 import java.io.Serializable;
@@ -35,7 +38,8 @@
    * @return binding to describe the cache. Caller must set at least the name on
    *         the returned binding.
    */
-  protected <K, V> UnnamedCacheBinding core(final TypeLiteral<Cache<K, V>> type) {
+  protected <K, V> UnnamedCacheBinding<K, V> core(
+      final TypeLiteral<Cache<K, V>> type) {
     return core(Key.get(type));
   }
 
@@ -49,15 +53,15 @@
    *        and with {@code @Named} annotations.
    * @return binding to describe the cache.
    */
-  protected <K, V> NamedCacheBinding core(final TypeLiteral<Cache<K, V>> type,
-      final String name) {
+  protected <K, V> NamedCacheBinding<K, V> core(
+      final TypeLiteral<Cache<K, V>> type, final String name) {
     return core(Key.get(type, Names.named(name))).name(name);
   }
 
-  private <K, V> UnnamedCacheBinding core(final Key<Cache<K, V>> key) {
+  private <K, V> UnnamedCacheBinding<K, V> core(final Key<Cache<K, V>> key) {
     final boolean disk = false;
-    final CacheProvider<K, V> b = new CacheProvider<K, V>(disk);
-    bind(key).toProvider(b);
+    final CacheProvider<K, V> b = new CacheProvider<K, V>(disk, this);
+    bind(key).toProvider(b).in(Scopes.SINGLETON);
     return b;
   }
 
@@ -72,7 +76,7 @@
    * @return binding to describe the cache. Caller must set at least the name on
    *         the returned binding.
    */
-  protected <K extends Serializable, V extends Serializable> UnnamedCacheBinding disk(
+  protected <K extends Serializable, V extends Serializable> UnnamedCacheBinding<K, V> disk(
       final TypeLiteral<Cache<K, V>> type) {
     return disk(Key.get(type));
   }
@@ -87,15 +91,31 @@
    *        and with {@code @Named} annotations.
    * @return binding to describe the cache.
    */
-  protected <K extends Serializable, V extends Serializable> NamedCacheBinding disk(
+  protected <K extends Serializable, V extends Serializable> NamedCacheBinding<K, V> disk(
       final TypeLiteral<Cache<K, V>> type, final String name) {
     return disk(Key.get(type, Names.named(name))).name(name);
   }
 
-  private <K, V> UnnamedCacheBinding disk(final Key<Cache<K, V>> key) {
+  private <K, V> UnnamedCacheBinding<K, V> disk(final Key<Cache<K, V>> key) {
     final boolean disk = true;
-    final CacheProvider<K, V> b = new CacheProvider<K, V>(disk);
-    bind(key).toProvider(b);
+    final CacheProvider<K, V> b = new CacheProvider<K, V>(disk, this);
+    bind(key).toProvider(b).in(Scopes.SINGLETON);
     return b;
   }
+
+  <K, V> Provider<EntryCreator<K, V>> getEntryCreator(CacheProvider<K, V> cp,
+      Class<? extends EntryCreator<K, V>> type) {
+    Key<EntryCreator<K, V>> key = newKey();
+    bind(key).to(type).in(Scopes.SINGLETON);
+    return getProvider(key);
+  }
+
+  @SuppressWarnings("unchecked")
+  private static <K, V> Key<EntryCreator<K, V>> newKey() {
+    return (Key<EntryCreator<K, V>>) newKeyImpl();
+  }
+
+  private static Key<?> newKeyImpl() {
+    return Key.get(EntryCreator.class, UniqueAnnotations.create());
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheProvider.java
index 6a7293e..0ba424f2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheProvider.java
@@ -27,7 +27,8 @@
 import java.util.concurrent.TimeUnit;
 
 final class CacheProvider<K, V> implements Provider<Cache<K, V>>,
-    NamedCacheBinding, UnnamedCacheBinding {
+    NamedCacheBinding<K, V>, UnnamedCacheBinding<K, V> {
+  private final CacheModule module;
   private final boolean disk;
   private int memoryLimit;
   private int diskLimit;
@@ -35,9 +36,11 @@
   private EvictionPolicy evictionPolicy;
   private String cacheName;
   private ProxyEhcache cache;
+  private Provider<EntryCreator<K, V>> entryCreator;
 
-  CacheProvider(final boolean disk) {
+  CacheProvider(final boolean disk, CacheModule module) {
     this.disk = disk;
+    this.module = module;
 
     memoryLimit(1024);
     maxAge(90, DAYS);
@@ -84,7 +87,7 @@
     return evictionPolicy;
   }
 
-  public NamedCacheBinding name(final String name) {
+  public NamedCacheBinding<K, V> name(final String name) {
     if (cacheName != null) {
       throw new IllegalStateException("Cache name already set");
     }
@@ -92,12 +95,12 @@
     return this;
   }
 
-  public NamedCacheBinding memoryLimit(final int objects) {
+  public NamedCacheBinding<K, V> memoryLimit(final int objects) {
     memoryLimit = objects;
     return this;
   }
 
-  public NamedCacheBinding diskLimit(final int objects) {
+  public NamedCacheBinding<K, V> diskLimit(final int objects) {
     if (!disk) {
       // TODO This should really be a compile time type error, but I'm
       // too lazy to create the mess of permutations required to setup
@@ -109,21 +112,30 @@
     return this;
   }
 
-  public NamedCacheBinding maxAge(final long duration, final TimeUnit unit) {
+  public NamedCacheBinding<K, V> maxAge(final long duration, final TimeUnit unit) {
     maxAge = SECONDS.convert(duration, unit);
     return this;
   }
 
   @Override
-  public NamedCacheBinding evictionPolicy(final EvictionPolicy policy) {
+  public NamedCacheBinding<K, V> evictionPolicy(final EvictionPolicy policy) {
     evictionPolicy = policy;
     return this;
   }
 
+  public NamedCacheBinding<K, V> populateWith(
+      Class<? extends EntryCreator<K, V>> creator) {
+    entryCreator = module.getEntryCreator(this, creator);
+    return this;
+  }
+
   public Cache<K, V> get() {
     if (cache == null) {
       throw new ProvisionException("Cache \"" + cacheName + "\" not available");
     }
+    if (entryCreator != null) {
+      return new PopulatingCache<K, V>(cache, entryCreator.get());
+    }
     return new SimpleCache<K, V>(cache);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/EntryCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/EntryCreator.java
new file mode 100644
index 0000000..af07e08
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/EntryCreator.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.cache;
+
+/**
+ * Creates a cache entry on demand when its not found.
+ *
+ * @param <K> type of the cache's key.
+ * @param <V> type of the cache's value element.
+ */
+public abstract class EntryCreator<K, V> {
+  /**
+   * Invoked on a cache miss, to compute the cache entry.
+   *
+   * @param key entry whose content needs to be obtained.
+   * @return new cache content. The caller will automatically put this object
+   *         into the cache.
+   * @throws Exception the cache content cannot be computed. No entry will be
+   *         stored in the cache, and {@link #missing(Object)} will be invoked
+   *         instead. Future requests for the same key will retry this method.
+   */
+  public abstract V createEntry(K key) throws Exception;
+
+  /** Invoked when {@link #createEntry(Object)} fails, by default return null. */
+  public V missing(K key) {
+    return null;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/NamedCacheBinding.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/NamedCacheBinding.java
index d486425..3394c71 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/NamedCacheBinding.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/NamedCacheBinding.java
@@ -17,16 +17,19 @@
 import java.util.concurrent.TimeUnit;
 
 /** Configure a cache declared within a {@link CacheModule} instance. */
-public interface NamedCacheBinding {
+public interface NamedCacheBinding<K, V> {
   /** Set the number of objects to cache in memory. */
-  public NamedCacheBinding memoryLimit(int objects);
+  public NamedCacheBinding<K, V> memoryLimit(int objects);
 
   /** Set the number of objects to cache in memory. */
-  public NamedCacheBinding diskLimit(int objects);
+  public NamedCacheBinding<K, V> diskLimit(int objects);
 
   /** Set the time an element lives before being expired. */
-  public NamedCacheBinding maxAge(long duration, TimeUnit durationUnits);
+  public NamedCacheBinding<K, V> maxAge(long duration, TimeUnit durationUnits);
 
   /** Set the eviction policy for elements when the cache is full. */
-  public NamedCacheBinding evictionPolicy(EvictionPolicy policy);
+  public NamedCacheBinding<K, V> evictionPolicy(EvictionPolicy policy);
+
+  /** Populate the cache with items from the EntryCreator. */
+  public NamedCacheBinding<K, V> populateWith(Class<? extends EntryCreator<K, V>> creator);
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/SelfPopulatingCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/PopulatingCache.java
similarity index 61%
rename from gerrit-server/src/main/java/com/google/gerrit/server/cache/SelfPopulatingCache.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/cache/PopulatingCache.java
index 7067ef4..0822cc0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/SelfPopulatingCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/PopulatingCache.java
@@ -29,61 +29,39 @@
 /**
  * A decorator for {@link Cache} which automatically constructs missing entries.
  * <p>
- * On a cache miss {@link #createEntry(Object)} is invoked, allowing the
- * application specific subclass to compute the entry and return it for caching.
- * During a miss the cache takes a lock related to the missing key, ensuring
- * that at most one thread performs the creation work, and other threads wait
- * for the result. Concurrent creations are possible if two different keys miss
- * and hash to different locks in the internal lock table.
+ * On a cache miss {@link EntryCreator#createEntry(Object)} is invoked, allowing
+ * the application specific subclass to compute the entry and return it for
+ * caching. During a miss the cache takes a lock related to the missing key,
+ * ensuring that at most one thread performs the creation work, and other
+ * threads wait for the result. Concurrent creations are possible if two
+ * different keys miss and hash to different locks in the internal lock table.
  *
  * @param <K> type of key used to name cache entries.
  * @param <V> type of value stored within a cache entry.
  */
-public abstract class SelfPopulatingCache<K, V> implements Cache<K, V> {
+class PopulatingCache<K, V> implements Cache<K, V> {
   private static final Logger log =
-      LoggerFactory.getLogger(SelfPopulatingCache.class);
+      LoggerFactory.getLogger(PopulatingCache.class);
 
   private final net.sf.ehcache.constructs.blocking.SelfPopulatingCache self;
+  private final EntryCreator<K, V> creator;
 
-  /**
-   * Create a new cache which uses another cache to store entries.
-   *
-   * @param backingStore cache which will store the entries for this cache.
-   */
-  @SuppressWarnings("unchecked")
-  public SelfPopulatingCache(final Cache<K, V> backingStore) {
-    final Ehcache s = ((SimpleCache) backingStore).getEhcache();
+  PopulatingCache(Ehcache s, EntryCreator<K, V> entryCreator) {
+    creator = entryCreator;
     final CacheEntryFactory f = new CacheEntryFactory() {
       @SuppressWarnings("unchecked")
       @Override
       public Object createEntry(Object key) throws Exception {
-        return SelfPopulatingCache.this.createEntry((K) key);
+        return creator.createEntry((K) key);
       }
     };
     self = new net.sf.ehcache.constructs.blocking.SelfPopulatingCache(s, f);
   }
 
   /**
-   * Invoked on a cache miss, to compute the cache entry.
-   *
-   * @param key entry whose content needs to be obtained.
-   * @return new cache content. The caller will automatically put this object
-   *         into the cache.
-   * @throws Exception the cache content cannot be computed. No entry will be
-   *         stored in the cache, and {@link #missing(Object)} will be invoked
-   *         instead. Future requests for the same key will retry this method.
-   */
-  protected abstract V createEntry(K key) throws Exception;
-
-  /** Invoked when {@link #createEntry(Object)} fails, by default return null. */
-  protected V missing(K key) {
-    return null;
-  }
-
-  /**
-   * Get the element from the cache, or {@link #missing(Object)} if not found.
+   * Get the element from the cache, or {@link EntryCreator#missing(Object)} if not found.
    * <p>
-   * The {@link #missing(Object)} method is only invoked if:
+   * The {@link EntryCreator#missing(Object)} method is only invoked if:
    * <ul>
    * <li>{@code key == null}, in which case the application should return a
    * suitable return value that callers can accept, or throw a RuntimeException.
@@ -99,7 +77,7 @@
   @SuppressWarnings("unchecked")
   public V get(final K key) {
     if (key == null) {
-      return missing(key);
+      return creator.missing(key);
     }
 
     final Element m;
@@ -107,12 +85,12 @@
       m = self.get(key);
     } catch (IllegalStateException err) {
       log.error("Cannot lookup " + key + " in \"" + self.getName() + "\"", err);
-      return missing(key);
+      return creator.missing(key);
     } catch (CacheException err) {
       log.error("Cannot lookup " + key + " in \"" + self.getName() + "\"", err);
-      return missing(key);
+      return creator.missing(key);
     }
-    return m != null ? (V) m.getObjectValue() : missing(key);
+    return m != null ? (V) m.getObjectValue() : creator.missing(key);
   }
 
   public void remove(final K key) {
@@ -122,7 +100,7 @@
   }
 
   /** Remove all cached items, forcing them to be created again on demand. */
-  public void removeAll(){
+  public void removeAll() {
     self.removeAll();
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/SimpleCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/SimpleCache.java
index d776412..2283f96 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/SimpleCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/SimpleCache.java
@@ -72,6 +72,10 @@
     }
   }
 
+  public void removeAll() {
+    self.removeAll();
+  }
+
   @Override
   public long getTimeToLive(final TimeUnit unit) {
     final long maxAge = self.getCacheConfiguration().getTimeToLiveSeconds();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/UnnamedCacheBinding.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/UnnamedCacheBinding.java
index 328ebd8..43039e1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/UnnamedCacheBinding.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/UnnamedCacheBinding.java
@@ -16,7 +16,7 @@
 
 
 /** Configure a cache declared within a {@link CacheModule} instance. */
-public interface UnnamedCacheBinding {
+public interface UnnamedCacheBinding<K, V> {
   /** Set the name of the cache. */
-  public NamedCacheBinding name(String cacheName);
+  public NamedCacheBinding<K, V> name(String cacheName);
 }
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 e722782..f889a2b 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
@@ -140,6 +140,27 @@
       return defaultValue;
     }
 
+    try {
+      return getTimeUnit(s, defaultValue, wantUnit);
+    } catch (IllegalArgumentException notTime) {
+      throw notTimeUnit(section, subsection, setting, valueString);
+    }
+  }
+
+  /**
+   * Parse a numerical time unit, such as "1 minute", from a string.
+   *
+   * @param s 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
+   *        well as the units to assume if the value does not contain an
+   *        indication of the units.
+   * @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) {
@@ -198,13 +219,13 @@
       inputMul = 365;
 
     } else {
-      throw notTimeUnit(section, subsection, setting, valueString);
+      throw notTimeUnit(valueString);
     }
 
     try {
       return wantUnit.convert(Long.parseLong(s) * inputMul, inputUnit);
     } catch (NumberFormatException nfe) {
-      throw notTimeUnit(section, subsection, setting, valueString);
+      throw notTimeUnit(valueString);
     }
   }
 
@@ -278,6 +299,10 @@
         + valueString);
   }
 
+  private static IllegalArgumentException notTimeUnit(final String val) {
+    return new IllegalArgumentException("Invalid time unit value: " + val);
+  }
+
   private ConfigUtil() {
   }
 }
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 5f2c6f9..fa2aaad 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
@@ -37,35 +37,32 @@
 import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.auth.ldap.LdapModule;
 import com.google.gerrit.server.cache.CachePool;
+import com.google.gerrit.server.events.EventFactory;
 import com.google.gerrit.server.git.ChangeMergeQueue;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.LocalDiskRepositoryManager;
-import com.google.gerrit.server.git.MergeOp;
 import com.google.gerrit.server.git.MergeQueue;
 import com.google.gerrit.server.git.PushAllProjectsOp;
 import com.google.gerrit.server.git.PushReplication;
 import com.google.gerrit.server.git.ReloadSubmitQueueOp;
 import com.google.gerrit.server.git.ReplicationQueue;
 import com.google.gerrit.server.git.WorkQueue;
-import com.google.gerrit.server.mail.AbandonedSender;
-import com.google.gerrit.server.mail.CommentSender;
 import com.google.gerrit.server.mail.EmailSender;
 import com.google.gerrit.server.mail.FromAddressGenerator;
 import com.google.gerrit.server.mail.FromAddressGeneratorProvider;
-import com.google.gerrit.server.mail.MergeFailSender;
-import com.google.gerrit.server.mail.MergedSender;
-import com.google.gerrit.server.mail.RegisterNewEmailSender;
 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.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.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.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.PersonIdent;
 
@@ -125,12 +122,12 @@
     bind(FileTypeRegistry.class).to(MimeUtilFileTypeRegistry.class);
     bind(WorkQueue.class);
     bind(ToolsCatalog.class);
+    bind(EventFactory.class);
 
     bind(ReplicationQueue.class).to(PushReplication.class).in(SINGLETON);
     factory(PushAllProjectsOp.Factory.class);
 
     bind(MergeQueue.class).to(ChangeMergeQueue.class).in(SINGLETON);
-    factory(MergeOp.Factory.class);
     factory(ReloadSubmitQueueOp.Factory.class);
 
     bind(FromAddressGenerator.class).toProvider(
@@ -139,13 +136,9 @@
 
     bind(PatchSetInfoFactory.class);
     bind(IdentifiedUser.GenericFactory.class).in(SINGLETON);
+    bind(ChangeControl.GenericFactory.class);
+    bind(ProjectControl.GenericFactory.class);
     factory(FunctionState.Factory.class);
-
-    factory(AbandonedSender.Factory.class);
-    factory(CommentSender.Factory.class);
-    factory(MergedSender.Factory.class);
-    factory(MergeFailSender.Factory.class);
-    factory(RegisterNewEmailSender.Factory.class);
     factory(ReplicationUser.Factory.class);
 
     install(new LifecycleModule() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
index 9221ad9..5a75995 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
@@ -21,13 +21,21 @@
 import com.google.gerrit.server.RequestCleanup;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.account.GroupControl;
+import com.google.gerrit.server.git.MergeOp;
 import com.google.gerrit.server.git.ReceiveCommits;
+import com.google.gerrit.server.mail.AbandonedSender;
 import com.google.gerrit.server.mail.AddReviewerSender;
+import com.google.gerrit.server.mail.CommentSender;
 import com.google.gerrit.server.mail.CreateChangeSender;
+import com.google.gerrit.server.mail.MergeFailSender;
+import com.google.gerrit.server.mail.MergedSender;
+import com.google.gerrit.server.mail.RegisterNewEmailSender;
 import com.google.gerrit.server.mail.ReplacePatchSetSender;
 import com.google.gerrit.server.patch.PublishComments;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.ChangeQueryRewriter;
 import com.google.inject.servlet.RequestScoped;
 
 /** Bindings for {@link RequestScoped} entities. */
@@ -39,12 +47,15 @@
         RequestScoped.class);
     bind(IdentifiedUser.RequestFactory.class).in(SINGLETON);
     bind(AccountResolver.class);
+    bind(ChangeQueryRewriter.class);
 
     bind(ChangeControl.Factory.class).in(SINGLETON);
     bind(GroupControl.Factory.class).in(SINGLETON);
     bind(ProjectControl.Factory.class).in(SINGLETON);
 
+    factory(ChangeQueryBuilder.Factory.class);
     factory(ReceiveCommits.Factory.class);
+    factory(MergeOp.Factory.class);
 
     // Not really per-request, but dammit, I don't know where else to
     // easily park this stuff.
@@ -53,5 +64,10 @@
     factory(CreateChangeSender.Factory.class);
     factory(PublishComments.Factory.class);
     factory(ReplacePatchSetSender.Factory.class);
+    factory(AbandonedSender.Factory.class);
+    factory(CommentSender.Factory.class);
+    factory(MergedSender.Factory.class);
+    factory(MergeFailSender.Factory.class);
+    factory(RegisterNewEmailSender.Factory.class);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/AccountAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/AccountAttribute.java
new file mode 100644
index 0000000..2ad7ffe
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/AccountAttribute.java
@@ -0,0 +1,20 @@
+// 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 AccountAttribute {
+    public String name;
+    public String email;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ApprovalAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/ApprovalAttribute.java
new file mode 100644
index 0000000..baa660c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/ApprovalAttribute.java
@@ -0,0 +1,24 @@
+// 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 ApprovalAttribute {
+    public String type;
+    public String description;
+    public String value;
+
+    public Long grantedOn;
+    public AccountAttribute by;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeAbandonedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeAbandonedEvent.java
new file mode 100644
index 0000000..baaf30c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeAbandonedEvent.java
@@ -0,0 +1,23 @@
+// 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 ChangeAbandonedEvent extends ChangeEvent {
+    public final String type = "change-abandoned";
+    public ChangeAttribute change;
+    public PatchSetAttribute patchSet;
+    public AccountAttribute abandoner;
+    public String reason;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeAttribute.java
new file mode 100644
index 0000000..79a7e5b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeAttribute.java
@@ -0,0 +1,39 @@
+// 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;
+
+import com.google.gerrit.reviewdb.Change;
+
+import java.util.List;
+
+public class ChangeAttribute {
+    public String project;
+    public String branch;
+    public String topic;
+    public String id;
+    public String number;
+    public String subject;
+    public AccountAttribute owner;
+    public String url;
+
+    public Long lastUpdated;
+    public String sortKey;
+    public Boolean open;
+    public Change.Status status;
+
+    public List<TrackingIdAttribute> trackingIds;
+    public PatchSetAttribute currentPatchSet;
+    public List<PatchSetAttribute> patchSets;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeEvent.java
new file mode 100644
index 0000000..904a0a0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeEvent.java
@@ -0,0 +1,18 @@
+// 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 abstract class ChangeEvent {
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeMergedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeMergedEvent.java
new file mode 100644
index 0000000..0d5fc31
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeMergedEvent.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 ChangeMergedEvent extends ChangeEvent {
+    public final String type = "change-merged";
+    public ChangeAttribute change;
+    public PatchSetAttribute patchSet;
+    public AccountAttribute submitter;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/CommentAddedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/CommentAddedEvent.java
new file mode 100644
index 0000000..f00caaf
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/CommentAddedEvent.java
@@ -0,0 +1,24 @@
+// 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 CommentAddedEvent extends ChangeEvent {
+    public final String type = "comment-added";
+    public ChangeAttribute change;
+    public PatchSetAttribute patchSet;
+    public AccountAttribute author;
+    public ApprovalAttribute[] approvals;
+    public String comment;
+}
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
new file mode 100644
index 0000000..3ad397e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
@@ -0,0 +1,194 @@
+// 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;
+
+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.Change;
+import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.reviewdb.PatchSetApproval;
+import com.google.gerrit.reviewdb.TrackingId;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import com.google.inject.internal.Nullable;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+@Singleton
+public class EventFactory {
+  private final AccountCache accountCache;
+  private final Provider<String> urlProvider;
+  private final ApprovalTypes approvalTypes;
+
+  @Inject
+  EventFactory(AccountCache accountCache,
+      @CanonicalWebUrl @Nullable Provider<String> urlProvider,
+      ApprovalTypes approvalTypes) {
+    this.accountCache = accountCache;
+    this.urlProvider = urlProvider;
+    this.approvalTypes = approvalTypes;
+  }
+
+  /**
+   * Create a ChangeAttribute for the given change suitable for serialization to
+   * JSON.
+   *
+   * @param change
+   * @return object suitable for serialization to JSON
+   */
+  public ChangeAttribute asChangeAttribute(final Change change) {
+    ChangeAttribute a = new ChangeAttribute();
+    a.project = change.getProject().get();
+    a.branch = change.getDest().getShortName();
+    a.topic = change.getTopic();
+    a.id = change.getKey().get();
+    a.number = change.getId().toString();
+    a.subject = change.getSubject();
+    a.url = getChangeUrl(change);
+    a.owner = asAccountAttribute(change.getOwner());
+    return a;
+  }
+
+  /**
+   * Extend the existing ChangeAttribute with additional fields.
+   *
+   * @param a
+   * @param change
+   */
+  public void extend(ChangeAttribute a, Change change) {
+    a.lastUpdated = change.getLastUpdatedOn().getTime() / 1000L;
+    a.sortKey = change.getSortKey();
+    a.open = change.getStatus().isOpen();
+    a.status = change.getStatus();
+  }
+
+  public void addTrackingIds(ChangeAttribute a, Collection<TrackingId> ids) {
+    if (!ids.isEmpty()) {
+      a.trackingIds = new ArrayList<TrackingIdAttribute>(ids.size());
+      for (TrackingId t : ids) {
+        a.trackingIds.add(asTrackingIdAttribute(t));
+      }
+    }
+  }
+
+  public void addPatchSets(ChangeAttribute a, Collection<PatchSet> ps) {
+    if (!ps.isEmpty()) {
+      a.patchSets = new ArrayList<PatchSetAttribute>(ps.size());
+      for (PatchSet p : ps) {
+        a.patchSets.add(asPatchSetAttribute(p));
+      }
+    }
+  }
+
+  public TrackingIdAttribute asTrackingIdAttribute(TrackingId id) {
+    TrackingIdAttribute a = new TrackingIdAttribute();
+    a.system = id.getSystem();
+    a.id = id.getTrackingId();
+    return a;
+  }
+
+  /**
+   * Create a PatchSetAttribute for the given patchset suitable for
+   * serialization to JSON.
+   *
+   * @param patchSet
+   * @return object suitable for serialization to JSON
+   */
+  public PatchSetAttribute asPatchSetAttribute(final PatchSet patchSet) {
+    PatchSetAttribute p = new PatchSetAttribute();
+    p.revision = patchSet.getRevision().get();
+    p.number = Integer.toString(patchSet.getPatchSetId());
+    p.ref = patchSet.getRefName();
+    p.uploader = asAccountAttribute(patchSet.getUploader());
+    return p;
+  }
+
+  public void addApprovals(PatchSetAttribute p,
+      Collection<PatchSetApproval> list) {
+    if (!list.isEmpty()) {
+      p.approvals = new ArrayList<ApprovalAttribute>(list.size());
+      for (PatchSetApproval a : list) {
+        if (a.getValue() != 0) {
+          p.approvals.add(asApprovalAttribute(a));
+        }
+      }
+      if (p.approvals.isEmpty()) {
+        p.approvals = null;
+      }
+    }
+  }
+
+  /**
+   * Create an AuthorAttribute for the given account suitable for serialization
+   * to JSON.
+   *
+   * @param id
+   * @return object suitable for serialization to JSON
+   */
+  public AccountAttribute asAccountAttribute(Account.Id id) {
+    return asAccountAttribute(accountCache.get(id).getAccount());
+  }
+
+  /**
+   * Create an AuthorAttribute for the given account suitable for serialization
+   * to JSON.
+   *
+   * @param account
+   * @return object suitable for serialization to JSON
+   */
+  public AccountAttribute asAccountAttribute(final Account account) {
+    AccountAttribute who = new AccountAttribute();
+    who.name = account.getFullName();
+    who.email = account.getPreferredEmail();
+    return who;
+  }
+
+  /**
+   * Create an ApprovalAttribute for the given approval suitable for
+   * serialization to JSON.
+   *
+   * @param approval
+   * @return object suitable for serialization to JSON
+   */
+  public ApprovalAttribute asApprovalAttribute(PatchSetApproval approval) {
+    ApprovalAttribute a = new ApprovalAttribute();
+    a.type = approval.getCategoryId().get();
+    a.value = Short.toString(approval.getValue());
+    a.by = asAccountAttribute(approval.getAccountId());
+    a.grantedOn = approval.getGranted().getTime() / 1000L;
+
+    ApprovalType at = approvalTypes.getApprovalType(approval.getCategoryId());
+    if (at != null) {
+      a.description = at.getCategory().getName();
+    }
+    return a;
+  }
+
+  /** Get a link to the change; null if the server doesn't know its own address. */
+  private String getChangeUrl(final Change change) {
+    if (change != null && urlProvider.get() != null) {
+      final StringBuilder r = new StringBuilder();
+      r.append(urlProvider.get());
+      r.append(change.getChangeId());
+      return r.toString();
+    }
+    return null;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetAttribute.java
new file mode 100644
index 0000000..5de4d6f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetAttribute.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.server.events;
+
+import java.util.List;
+
+public class PatchSetAttribute {
+    public String number;
+    public String revision;
+    public String ref;
+    public AccountAttribute uploader;
+
+    public List<ApprovalAttribute> approvals;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetCreatedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetCreatedEvent.java
new file mode 100644
index 0000000..15e3978
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetCreatedEvent.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 PatchSetCreatedEvent extends ChangeEvent {
+    public final String type = "patchset-created";
+    public ChangeAttribute change;
+    public PatchSetAttribute patchSet;
+    public AccountAttribute uploader;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/QueryStats.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/QueryStats.java
new file mode 100644
index 0000000..1c5e7d7
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/QueryStats.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 QueryStats {
+  public final String type = "stats";
+  public int rowCount;
+  public long runTimeMilliseconds;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/TrackingIdAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/TrackingIdAttribute.java
new file mode 100644
index 0000000..7d55dd2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/TrackingIdAttribute.java
@@ -0,0 +1,20 @@
+// 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 TrackingIdAttribute {
+  public String system;
+  public String id;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMergeQueue.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMergeQueue.java
index d6c8f4a..a7a969f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMergeQueue.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMergeQueue.java
@@ -19,12 +19,30 @@
 
 import com.google.gerrit.reviewdb.Branch;
 import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.RemotePeer;
+import com.google.gerrit.server.RequestCleanup;
+import com.google.gerrit.server.config.GerritRequestModule;
+import com.google.gerrit.server.ssh.SshInfo;
+import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.OutOfScopeException;
+import com.google.inject.Provider;
+import com.google.inject.Scope;
+import com.google.inject.servlet.RequestScoped;
+
+import com.jcraft.jsch.HostKey;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.net.SocketAddress;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 
@@ -38,18 +56,47 @@
       new HashMap<Branch.NameKey, RecheckJob>();
 
   private final WorkQueue workQueue;
-  private final MergeOp.Factory opFactory;
+  private final Provider<MergeOp.Factory> bgFactory;
 
   @Inject
-  ChangeMergeQueue(final WorkQueue wq, final MergeOp.Factory of) {
+  ChangeMergeQueue(final WorkQueue wq, Injector parent) {
     workQueue = wq;
-    opFactory = of;
+
+    Injector child = parent.createChildInjector(new AbstractModule() {
+      @Override
+      protected void configure() {
+        bindScope(RequestScoped.class, MyScope.REQUEST);
+        install(new GerritRequestModule());
+
+        bind(CurrentUser.class).to(IdentifiedUser.class);
+        bind(IdentifiedUser.class).toProvider(new Provider<IdentifiedUser>() {
+          @Override
+          public IdentifiedUser get() {
+            throw new OutOfScopeException("No user on merge thread");
+          }
+        });
+        bind(SocketAddress.class).annotatedWith(RemotePeer.class).toProvider(
+            new Provider<SocketAddress>() {
+              @Override
+              public SocketAddress get() {
+                throw new OutOfScopeException("No remote peer on merge thread");
+              }
+            });
+        bind(SshInfo.class).toInstance(new SshInfo() {
+          @Override
+          public List<HostKey> getHostKeys() {
+            return Collections.emptyList();
+          }
+        });
+      }
+    });
+    bgFactory = child.getProvider(MergeOp.Factory.class);
   }
 
   @Override
-  public void merge(final Branch.NameKey branch) {
+  public void merge(MergeOp.Factory mof, Branch.NameKey branch) {
     if (start(branch)) {
-      mergeImpl(branch);
+      mergeImpl(mof, branch);
     }
   }
 
@@ -127,7 +174,7 @@
     e.needMerge = false;
   }
 
-  private void mergeImpl(final Branch.NameKey branch) {
+  private void mergeImpl(MergeOp.Factory opFactory, Branch.NameKey branch) {
     try {
       opFactory.create(branch).merge();
     } catch (Throwable e) {
@@ -137,6 +184,26 @@
     }
   }
 
+  private void mergeImpl(Branch.NameKey branch) {
+    try {
+      MyScope ctx = new MyScope();
+      MyScope old = MyScope.set(ctx);
+      try {
+        try {
+          bgFactory.get().create(branch).merge();
+        } finally {
+          ctx.cleanup.run();
+        }
+      } finally {
+        MyScope.set(old);
+      }
+    } catch (Throwable e) {
+      log.error("Merge attempt for " + branch + " failed", e);
+    } finally {
+      finish(branch);
+    }
+  }
+
   private synchronized void recheck(final RecheckJob e) {
     final long remainingDelay = e.recheckAt - System.currentTimeMillis();
     if (MILLISECONDS.convert(10, SECONDS) < remainingDelay) {
@@ -194,4 +261,65 @@
       return "recheck " + project.get() + " " + dest.getShortName();
     }
   }
+
+  private static class MyScope {
+    private static final ThreadLocal<MyScope> current =
+        new ThreadLocal<MyScope>();
+
+    private static MyScope getContext() {
+      final MyScope ctx = current.get();
+      if (ctx == null) {
+        throw new OutOfScopeException("Not in command/request");
+      }
+      return ctx;
+    }
+
+    static MyScope set(MyScope ctx) {
+      MyScope old = current.get();
+      current.set(ctx);
+      return old;
+    }
+
+    static final Scope REQUEST = new Scope() {
+      public <T> Provider<T> scope(final Key<T> key, final Provider<T> creator) {
+        return new Provider<T>() {
+          public T get() {
+            return getContext().get(key, creator);
+          }
+
+          @Override
+          public String toString() {
+            return String.format("%s[%s]", creator, REQUEST);
+          }
+        };
+      }
+
+      @Override
+      public String toString() {
+        return "MergeQueue.REQUEST";
+      }
+    };
+
+    private static final Key<RequestCleanup> RC_KEY =
+        Key.get(RequestCleanup.class);
+
+    private final RequestCleanup cleanup;
+    private final Map<Key<?>, Object> map;
+
+    MyScope() {
+      cleanup = new RequestCleanup();
+      map = new HashMap<Key<?>, Object>();
+      map.put(RC_KEY, cleanup);
+    }
+
+    synchronized <T> T get(Key<T> key, Provider<T> creator) {
+      @SuppressWarnings("unchecked")
+      T t = (T) map.get(key);
+      if (t == null) {
+        t = creator.get();
+        map.put(key, t);
+      }
+      return t;
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitProjectImporter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitProjectImporter.java
index f87f518..676e4b6 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
@@ -22,6 +22,7 @@
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.lib.RepositoryCache.FileKey;
+import org.eclipse.jgit.util.FS;
 
 import java.io.File;
 import java.io.IOException;
@@ -76,7 +77,7 @@
         continue;
       }
 
-      if (FileKey.isGitRepository(f)) {
+      if (FileKey.isGitRepository(f, FS.DETECTED)) {
         if (name.equals(".git")) {
           name = prefix.substring(0, prefix.length() - 1);
 
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 f0e6366..e8df230 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
@@ -29,6 +29,7 @@
 import org.eclipse.jgit.lib.WindowCache;
 import org.eclipse.jgit.lib.WindowCacheConfig;
 import org.eclipse.jgit.lib.RepositoryCache.FileKey;
+import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.IO;
 import org.eclipse.jgit.util.RawParseUtils;
 import org.slf4j.Logger;
@@ -90,7 +91,7 @@
     }
 
     try {
-      final FileKey loc = FileKey.lenient(new File(basePath, name));
+      final FileKey loc = FileKey.lenient(new File(basePath, name), FS.DETECTED);
       return RepositoryCache.open(loc);
     } catch (IOException e1) {
       final RepositoryNotFoundException e2;
@@ -107,12 +108,12 @@
     }
 
     try {
-      File dir = FileKey.resolve(new File(basePath, name));
+      File dir = FileKey.resolve(new File(basePath, name), FS.DETECTED);
       FileKey loc;
       if (dir != null) {
         // Already exists on disk, use the repository we found.
         //
-        loc = FileKey.exact(dir);
+        loc = FileKey.exact(dir, FS.DETECTED);
       } else {
         // It doesn't exist under any of the standard permutations
         // of the repository name, so prefer the standard bare name.
@@ -120,7 +121,7 @@
         if (!name.endsWith(".git")) {
           name = name + ".git";
         }
-        loc = FileKey.exact(new File(basePath, name));
+        loc = FileKey.exact(new File(basePath, name), FS.DETECTED);
       }
       return RepositoryCache.open(loc, false);
     } catch (IOException e1) {
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 5280e0c..66e8d61 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
@@ -1179,7 +1179,6 @@
       if (submitter != null) {
         cm.setFrom(submitter.getAccountId());
       }
-      cm.setReviewDb(schema);
       cm.setPatchSet(schema.patchSets().get(c.currentPatchSetId()));
       cm.send();
     } catch (OrmException e) {
@@ -1241,7 +1240,6 @@
           cm.setFrom(submitter.getAccountId());
         }
       }
-      cm.setReviewDb(schema);
       cm.setPatchSet(schema.patchSets().get(c.currentPatchSetId()));
       cm.setChangeMessage(msg);
       cm.send();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeQueue.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeQueue.java
index abb0ab8..39017eb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeQueue.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeQueue.java
@@ -19,7 +19,7 @@
 import java.util.concurrent.TimeUnit;
 
 public interface MergeQueue {
-  void merge(Branch.NameKey branch);
+  void merge(MergeOp.Factory mof, Branch.NameKey branch);
 
   void schedule(Branch.NameKey branch);
 
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 9d56b08..76b8bf0 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
@@ -44,6 +44,7 @@
 import org.eclipse.jgit.transport.SshConfigSessionFactory;
 import org.eclipse.jgit.transport.SshSessionFactory;
 import org.eclipse.jgit.transport.URIish;
+import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.QuotedString;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -231,7 +232,7 @@
     try {
       sshSession =
           sshFactory.getSession(replicateURI.getUser(), replicateURI.getPass(),
-              replicateURI.getHost(), replicateURI.getPort());
+              replicateURI.getHost(), replicateURI.getPort(), FS.DETECTED);
       sshSession.connect();
 
       Channel channel = sshSession.openChannel("exec");
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 63ff58a..fbcdc09 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,7 +55,6 @@
 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.Constants;
 import org.eclipse.jgit.lib.ObjectId;
@@ -159,6 +158,8 @@
 
   private Map<ObjectId, Ref> refsById;
 
+  private String destTopicName;
+
   @Inject
   ReceiveCommits(final ReviewDb db, final ApprovalTypes approvalTypes,
       final AccountResolver accountResolver,
@@ -579,38 +580,55 @@
       destBranchName = Constants.R_HEADS + destBranchName;
     }
 
-    if (rp.getAdvertisedRefs().containsKey(destBranchName)) {
-      // We advertised the branch to the client so we know
-      // the branch exists. Target this branch for the upload.
-      //
-      destBranch = new Branch.NameKey(project.getNameKey(), destBranchName);
-
-    } else {
-      // We didn't advertise the branch, because it doesn't exist yet.
-      // Allow it anyway if HEAD is a symbolic reference to the name.
-      //
-      final String head;
-      try {
-        head = repo.getFullBranch();
-      } catch (IOException e) {
-        log.error("Cannot read HEAD symref", e);
-        reject(cmd, "internal error");
-        return;
-      }
-
-      if (head.equals(destBranchName)) {
-        destBranch = new Branch.NameKey(project.getNameKey(), destBranchName);
-      }
-    }
-
-    if (destBranch == null) {
-      String n = destBranchName;
-      if (n.startsWith(Constants.R_HEADS))
-        n = n.substring(Constants.R_HEADS.length());
-      reject(cmd, "branch " + n + " not found");
+    final String head;
+    try {
+      head = repo.getFullBranch();
+    } catch (IOException e) {
+      log.error("Cannot read HEAD symref", e);
+      reject(cmd, "internal error");
       return;
     }
 
+    // Split the destination branch by branch and topic.  The topic
+    // suffix is entirely optional, so it might not even exist.
+    //
+    int split = destBranchName.length();
+    for (;;) {
+      String name = destBranchName.substring(0, split);
+
+      if (rp.getAdvertisedRefs().containsKey(name)) {
+        // We advertised the branch to the client so we know
+        // the branch exists. Target this branch for the upload.
+        //
+        break;
+      } else if (head.equals(name)) {
+        // We didn't advertise the branch, because it doesn't exist yet.
+        // Allow it anyway as HEAD is a symbolic reference to the name.
+        //
+        break;
+      }
+
+      split = name.lastIndexOf('/', split - 1);
+      if (split <= Constants.R_REFS.length()) {
+        String n = destBranchName;
+        if (n.startsWith(Constants.R_HEADS))
+          n = n.substring(Constants.R_HEADS.length());
+        reject(cmd, "branch " + n + " not found");
+        return;
+      }
+    }
+
+    if (split < destBranchName.length()) {
+      destTopicName = destBranchName.substring(split + 1);
+    } else {
+      // We use empty string here to denote the topic wasn't
+      // supplied, but the caller used the syntax that allows
+      // for a topic to be given.
+      //
+      destTopicName = "";
+    }
+    destBranch = new Branch.NameKey(project.getNameKey(), //
+        destBranchName.substring(0, split));
     destBranchCtl = projectControl.controlForRef(destBranch);
     if (!destBranchCtl.canUpload()) {
       reject(cmd);
@@ -858,6 +876,7 @@
 
     final Change change =
         new Change(changeKey, new Change.Id(db.nextChangeId()), me, destBranch);
+    change.setTopic(destTopicName.isEmpty() ? null : destTopicName);
     change.nextPatchSetId();
 
     final PatchSet ps = new PatchSet(change.currPatchSetId());
@@ -912,7 +931,6 @@
       cm = createChangeSenderFactory.create(change);
       cm.setFrom(me);
       cm.setPatchSet(ps, info);
-      cm.setReviewDb(db);
       cm.addReviewers(reviewers);
       cm.addExtraCC(cc);
       cm.send();
@@ -1158,6 +1176,11 @@
             @Override
             public Change update(Change change) {
               if (change.getStatus().isOpen()) {
+                if (destTopicName != null) {
+                  change.setTopic(destTopicName.isEmpty() //
+                      ? null //
+                      : destTopicName);
+                }
                 change.setStatus(Change.Status.NEW);
                 change.setCurrentPatchSet(result.info);
                 ChangeUtil.updated(change);
@@ -1210,7 +1233,6 @@
       cm.setFrom(me);
       cm.setPatchSet(ps, result.info);
       cm.setChangeMessage(result.msg);
-      cm.setReviewDb(db);
       cm.addReviewers(reviewers);
       cm.addExtraCC(cc);
       cm.addReviewers(oldReviewers);
@@ -1559,7 +1581,6 @@
       try {
         final MergedSender cm = mergedSenderFactory.create(result.change);
         cm.setFrom(currentUser.getAccountId());
-        cm.setReviewDb(db);
         cm.setPatchSet(result.patchSet, result.info);
         cm.setDest(new Branch.NameKey(project.getNameKey(),
             result.mergedIntoRef));
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 bba69c8..d2b5c29 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
@@ -25,8 +25,8 @@
   }
 
   @Inject
-  public AbandonedSender(@Assisted Change c) {
-    super(c, "abandon");
+  public AbandonedSender(EmailArguments ea, @Assisted Change c) {
+    super(ea, c, "abandon");
   }
 
   @Override
@@ -39,7 +39,7 @@
   }
 
   @Override
-  protected void format() {
+  protected void formatChange() {
     appendText(getNameFor(fromId));
     appendText(" has abandoned change " + change.getKey().abbreviate() + ":\n");
     appendText("\n");
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 c9a6d98..be62ba0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddReviewerSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddReviewerSender.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.mail;
 
 import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.server.ssh.SshInfo;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -25,8 +26,9 @@
   }
 
   @Inject
-  public AddReviewerSender(@Assisted Change c) {
-    super(c);
+  public AddReviewerSender(EmailArguments ea, SshInfo sshInfo,
+      @Assisted Change c) {
+    super(ea, sshInfo, c);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
new file mode 100644
index 0000000..e7c463c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
@@ -0,0 +1,445 @@
+// 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.mail;
+
+import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.AccountProjectWatch;
+import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.ChangeMessage;
+import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.reviewdb.PatchSetApproval;
+import com.google.gerrit.reviewdb.PatchSetInfo;
+import com.google.gerrit.reviewdb.StarredChange;
+import com.google.gerrit.reviewdb.UserIdentity;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.mail.EmailHeader.AddressList;
+import com.google.gerrit.server.patch.PatchList;
+import com.google.gerrit.server.patch.PatchListEntry;
+import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gwtorm.client.OrmException;
+
+import org.eclipse.jgit.util.SystemReader;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.Set;
+import java.util.TreeSet;
+
+/** Sends an email to one or more interested parties. */
+public abstract class ChangeEmail extends OutgoingEmail {
+  protected final Change change;
+  protected String projectName;
+  protected PatchSet patchSet;
+  protected PatchSetInfo patchSetInfo;
+  protected ChangeMessage changeMessage;
+
+  private ProjectState projectState;
+  protected ChangeData changeData;
+  private boolean inFooter;
+
+  protected ChangeEmail(EmailArguments ea, final Change c, final String mc) {
+    super(ea, mc);
+    change = c;
+    changeData = change != null ? new ChangeData(change) : null;
+  }
+
+  public void setPatchSet(final PatchSet ps) {
+    patchSet = ps;
+  }
+
+  public void setPatchSet(final PatchSet ps, final PatchSetInfo psi) {
+    patchSet = ps;
+    patchSetInfo = psi;
+  }
+
+  public void setChangeMessage(final ChangeMessage cm) {
+    changeMessage = cm;
+  }
+
+  /** Format the message body by calling {@link #appendText(String)}. */
+  protected void format() {
+    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");
+
+    try {
+      HashSet<Account.Id> reviewers = new HashSet<Account.Id>();
+      for (PatchSetApproval p : args.db.get().patchSetApprovals().byChange(
+          change.getId())) {
+        reviewers.add(p.getAccountId());
+      }
+
+      TreeSet<String> names = new TreeSet<String>();
+      for (Account.Id who : reviewers) {
+        names.add(getNameEmailFor(who));
+      }
+
+      for (String name : names) {
+        appendText("Gerrit-Reviewer: " + name + "\n");
+      }
+    } catch (OrmException e) {
+    }
+  }
+
+  /** Format the message body by calling {@link #appendText(String)}. */
+  protected abstract void formatChange();
+
+  /** Setup the message headers and envelope (TO, CC, BCC). */
+  protected void init() {
+    super.init();
+    if (args.projectCache != null) {
+      projectState = args.projectCache.get(change.getProject());
+      projectName =
+          projectState != null ? projectState.getProject().getName() : null;
+    } else {
+      projectState = null;
+      projectName = null;
+    }
+
+    if (patchSet == null) {
+      try {
+        patchSet = args.db.get().patchSets().get(change.currentPatchSetId());
+      } catch (OrmException err) {
+        patchSet = null;
+      }
+    }
+
+    if (patchSet != null && patchSetInfo == null) {
+      try {
+        patchSetInfo = args.patchSetInfoFactory.get(patchSet.getId());
+      } catch (PatchSetInfoNotAvailableException err) {
+        patchSetInfo = null;
+      }
+    }
+
+    if (changeMessage != null && changeMessage.getWrittenOn() != null) {
+      setHeader("Date", new Date(changeMessage.getWrittenOn().getTime()));
+    }
+    setChangeSubjectHeader();
+    setHeader("X-Gerrit-Change-Id", "" + change.getKey().get());
+    setListIdHeader();
+    setChangeUrlHeader();
+    setCommitIdHeader();
+
+    inFooter = false;
+  }
+
+  private void setListIdHeader() {
+    // 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('@', '.') + ">");
+    if (getSettingsUrl() != null) {
+      setHeader("List-Unsubscribe", "<" + getSettingsUrl() + ">");
+    }
+  }
+
+  private void setChangeUrlHeader() {
+    final String u = getChangeUrl();
+    if (u != null) {
+      setHeader("X-Gerrit-ChangeURL", "<" + u + ">");
+    }
+  }
+
+  private void setCommitIdHeader() {
+    if (patchSet != null && patchSet.getRevision() != null
+        && patchSet.getRevision().get() != null
+        && patchSet.getRevision().get().length() > 0) {
+      setHeader("X-Gerrit-Commit", patchSet.getRevision().get());
+    }
+  }
+
+  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());
+  }
+
+  /** Get a link to the change; null if the server doesn't know its own address. */
+  protected String getChangeUrl() {
+    if (change != null && getGerritUrl() != null) {
+      final StringBuilder r = new StringBuilder();
+      r.append(getGerritUrl());
+      r.append(change.getChangeId());
+      return r.toString();
+    }
+    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");
+    }
+  }
+
+  /** Format the sender's "cover letter", {@link #getCoverLetter()}. */
+  protected void formatCoverLetter() {
+    final String cover = getCoverLetter();
+    if (!"".equals(cover)) {
+      appendText(cover);
+      appendText("\n\n");
+    }
+  }
+
+  /** Get the text of the "cover letter", from {@link ChangeMessage}. */
+  protected String getCoverLetter() {
+    if (changeMessage != null) {
+      final String txt = changeMessage.getMessage();
+      if (txt != null) {
+        return txt.trim();
+      }
+    }
+    return "";
+  }
+
+  /** Format the change message and the affected file list. */
+  protected void formatChangeDetail() {
+    if (patchSetInfo != null) {
+      appendText(patchSetInfo.getMessage().trim());
+      appendText("\n");
+    } else {
+      appendText(change.getSubject().trim());
+      appendText("\n");
+    }
+
+    if (patchSet != null) {
+      appendText("---\n");
+      for (PatchListEntry p : getPatchList().getPatches()) {
+        appendText(p.getChangeType().getCode() + " " + p.getNewName() + "\n");
+      }
+      appendText("\n");
+    }
+  }
+
+  /** Get the patch list corresponding to this patch set. */
+  protected PatchList getPatchList() {
+    if (patchSet != null) {
+      return args.patchListCache.get(change, patchSet);
+    }
+    return null;
+  }
+
+  /** Get the project entity the change is in; null if its been deleted. */
+  protected ProjectState getProjectState() {
+    return projectState;
+  }
+
+  /** Get the groups which own the project. */
+  protected Set<AccountGroup.Id> getProjectOwners() {
+    final ProjectState r;
+
+    r = args.projectCache.get(change.getProject());
+    return r != null ? r.getOwners() : Collections.<AccountGroup.Id> emptySet();
+  }
+
+  /** TO or CC all vested parties (change owner, patch set uploader, author). */
+  protected void rcptToAuthors(final RecipientType rt) {
+    add(rt, change.getOwner());
+    if (patchSet != null) {
+      add(rt, patchSet.getUploader());
+    }
+    if (patchSetInfo != null) {
+      add(rt, patchSetInfo.getAuthor());
+      add(rt, patchSetInfo.getCommitter());
+    }
+  }
+
+  /** BCC any user who has starred this change. */
+  protected void bccStarredBy() {
+    try {
+      // BCC anyone who has starred this change.
+      //
+      for (StarredChange w : args.db.get().starredChanges().byChange(
+          change.getId())) {
+        add(RecipientType.BCC, w.getAccountId());
+      }
+    } catch (OrmException err) {
+      // Just don't BCC everyone. Better to send a partial message to those
+      // we already have queued up then to fail deliver entirely to people
+      // who have a lower interest in the change.
+    }
+  }
+
+  /** BCC any user who has set "notify all comments" on this project. */
+  protected void bccWatchesNotifyAllComments() {
+    try {
+      // BCC anyone else who has interest in this project's changes
+      //
+      for (final AccountProjectWatch w : getWatches()) {
+        if (w.isNotifyAllComments()) {
+          add(RecipientType.BCC, w.getAccountId());
+        }
+      }
+    } catch (OrmException err) {
+      // Just don't CC everyone. Better to send a partial message to those
+      // we already have queued up then to fail deliver entirely to people
+      // who have a lower interest in the change.
+    }
+  }
+
+  /** Returns all watches that are relevant */
+  protected final List<AccountProjectWatch> getWatches() throws OrmException {
+    if (changeData == null) {
+      return Collections.emptyList();
+    }
+
+    List<AccountProjectWatch> matching = new ArrayList<AccountProjectWatch>();
+    Set<Account.Id> projectWatchers = new HashSet<Account.Id>();
+
+    for (AccountProjectWatch w : args.db.get().accountProjectWatches()
+        .byProject(change.getProject())) {
+      projectWatchers.add(w.getAccountId());
+      add(matching, w);
+    }
+
+    for (AccountProjectWatch w : args.db.get().accountProjectWatches()
+        .byProject(args.wildProject)) {
+      if (!projectWatchers.contains(w.getAccountId())) {
+        add(matching, w);
+      }
+    }
+
+    return Collections.unmodifiableList(matching);
+  }
+
+  @SuppressWarnings("unchecked")
+  private void add(List<AccountProjectWatch> matching, AccountProjectWatch w)
+      throws OrmException {
+    IdentifiedUser user =
+        args.identifiedUserFactory.create(args.db, w.getAccountId());
+    ChangeQueryBuilder qb = args.queryBuilder.create(user);
+    Predicate<ChangeData> p = qb.is_visible();
+    if (w.getFilter() != null) {
+      try {
+        qb.setAllowFile(true);
+        p = Predicate.and(qb.parse(w.getFilter()), p);
+        p = args.queryRewriter.get().rewrite(p);
+        if (p.match(changeData)) {
+          matching.add(w);
+        }
+      } catch (QueryParseException e) {
+        // Ignore broken filter expressions.
+      }
+    } else if (p.match(changeData)) {
+      matching.add(w);
+    }
+  }
+
+  /** Any user who has published comments on this change. */
+  protected void ccAllApprovals() {
+    ccApprovals(true);
+  }
+
+  /** Users who have non-zero approval codes on the change. */
+  protected void ccExistingReviewers() {
+    ccApprovals(false);
+  }
+
+  private void ccApprovals(final boolean includeZero) {
+    try {
+      // CC anyone else who has posted an approval mark on this change
+      //
+      for (PatchSetApproval ap : args.db.get().patchSetApprovals().byChange(
+          change.getId())) {
+        if (!includeZero && ap.getValue() == 0) {
+          continue;
+        }
+        add(RecipientType.CC, ap.getAccountId());
+      }
+    } catch (OrmException err) {
+    }
+  }
+
+  protected boolean isVisibleTo(final Account.Id to) {
+    return projectState == null
+        || change == null
+        || projectState.controlFor(args.identifiedUserFactory.create(to))
+            .controlFor(change).isVisible();
+  }
+}
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 a010ac1..be6360f 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
@@ -27,7 +27,9 @@
 
 import java.io.IOException;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 /** Send comments, after the author of them hit used Publish Comments in the UI. */
 public class CommentSender extends ReplyToChangeSender {
@@ -38,12 +40,19 @@
   private List<PatchLineComment> inlineComments = Collections.emptyList();
 
   @Inject
-  public CommentSender(@Assisted Change c) {
-    super(c, "comment");
+  public CommentSender(EmailArguments ea, @Assisted Change c) {
+    super(ea, c, "comment");
   }
 
   public void setPatchLineComments(final List<PatchLineComment> plc) {
     inlineComments = plc;
+
+    Set<String> paths = new HashSet<String>();
+    for (PatchLineComment c : plc) {
+      Patch.Key p = c.getKey().getParentKey();
+      paths.add(p.getFileName());
+    }
+    changeData.setCurrentFilePaths(paths);
   }
 
   @Override
@@ -56,7 +65,7 @@
   }
 
   @Override
-  protected void format() {
+  protected void formatChange() {
     if (!"".equals(getCoverLetter()) || !inlineComments.isEmpty()) {
       appendText("Comments on Patch Set " + patchSet.getPatchSetId() + ":\n");
       appendText("\n");
@@ -124,7 +133,7 @@
 
   private Repository getRepository() {
     try {
-      return server.openRepository(projectName);
+      return args.server.openRepository(projectName);
     } catch (RepositoryNotFoundException e) {
       return null;
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
index d25d584..ea57cde 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
@@ -19,7 +19,7 @@
 import com.google.gerrit.reviewdb.AccountGroupMember;
 import com.google.gerrit.reviewdb.AccountProjectWatch;
 import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -34,8 +34,9 @@
   }
 
   @Inject
-  public CreateChangeSender(@Assisted Change c) {
-    super(c);
+  public CreateChangeSender(EmailArguments ea, SshInfo sshInfo,
+      @Assisted Change c) {
+    super(ea, sshInfo, c);
   }
 
   @Override
@@ -46,37 +47,32 @@
   }
 
   private void bccWatchers() {
-    if (db != null) {
-      try {
-        // BCC anyone else who has interest in this project's changes
-        //
-        final ProjectState ps = getProjectState();
-        if (ps != null) {
-          // Try to mark interested owners with a TO and not a BCC line.
-          //
-          final Set<Account.Id> owners = new HashSet<Account.Id>();
-          for (AccountGroup.Id g : getProjectOwners()) {
-            for (AccountGroupMember m : db.accountGroupMembers().byGroup(g)) {
-              owners.add(m.getAccountId());
-            }
-          }
+    try {
+      // Try to mark interested owners with a TO and not a BCC line.
+      //
+      final Set<Account.Id> owners = new HashSet<Account.Id>();
+      for (AccountGroup.Id g : getProjectOwners()) {
+        for (AccountGroupMember m : args.db.get().accountGroupMembers()
+            .byGroup(g)) {
+          owners.add(m.getAccountId());
+        }
+      }
 
-          // BCC anyone who has interest in this project's changes
-          //
-          for (AccountProjectWatch w : db.accountProjectWatches()
-              .notifyNewChanges(ps.getProject().getNameKey())) {
-            if (owners.contains(w.getAccountId())) {
-              add(RecipientType.TO, w.getAccountId());
-            } else {
-              add(RecipientType.BCC, w.getAccountId());
-            }
+      // BCC anyone who has interest in this project's changes
+      //
+      for (final AccountProjectWatch w : getWatches()) {
+        if (w.isNotifyNewChanges()) {
+          if (owners.contains(w.getAccountId())) {
+            add(RecipientType.TO, w.getAccountId());
+          } else {
+            add(RecipientType.BCC, w.getAccountId());
           }
         }
-      } catch (OrmException err) {
-        // Just don't CC everyone. Better to send a partial message to those
-        // we already have queued up then to fail deliver entirely to people
-        // who have a lower interest in the change.
       }
+    } catch (OrmException err) {
+      // Just don't CC everyone. Better to send a partial message to those
+      // we already have queued up then to fail deliver entirely to people
+      // who have a lower interest in the change.
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
new file mode 100644
index 0000000..8a7ffd6
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
@@ -0,0 +1,75 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.mail;
+
+import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.IdentifiedUser.GenericFactory;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.WildProjectName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.ChangeQueryRewriter;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import javax.annotation.Nullable;
+
+class EmailArguments {
+  final GitRepositoryManager server;
+  final ProjectCache projectCache;
+  final AccountCache accountCache;
+  final PatchListCache patchListCache;
+  final FromAddressGenerator fromAddressGenerator;
+  final EmailSender emailSender;
+  final PatchSetInfoFactory patchSetInfoFactory;
+  final IdentifiedUser.GenericFactory identifiedUserFactory;
+  final Provider<String> urlProvider;
+  final Project.NameKey wildProject;
+
+  final ChangeQueryBuilder.Factory queryBuilder;
+  final Provider<ChangeQueryRewriter> queryRewriter;
+  final Provider<ReviewDb> db;
+
+  @Inject
+  EmailArguments(GitRepositoryManager server, ProjectCache projectCache,
+      AccountCache accountCache, PatchListCache patchListCache,
+      FromAddressGenerator fromAddressGenerator, EmailSender emailSender,
+      PatchSetInfoFactory patchSetInfoFactory,
+      GenericFactory identifiedUserFactory,
+      @CanonicalWebUrl @Nullable Provider<String> urlProvider,
+      @WildProjectName Project.NameKey wildProject,
+      ChangeQueryBuilder.Factory queryBuilder,
+      Provider<ChangeQueryRewriter> queryRewriter, Provider<ReviewDb> db) {
+    this.server = server;
+    this.projectCache = projectCache;
+    this.accountCache = accountCache;
+    this.patchListCache = patchListCache;
+    this.fromAddressGenerator = fromAddressGenerator;
+    this.emailSender = emailSender;
+    this.patchSetInfoFactory = patchSetInfoFactory;
+    this.identifiedUserFactory = identifiedUserFactory;
+    this.urlProvider = urlProvider;
+    this.wildProject = wildProject;
+    this.queryBuilder = queryBuilder;
+    this.queryRewriter = queryRewriter;
+    this.db = db;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailHeader.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailHeader.java
index f2e89ac..10f6510 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailHeader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailHeader.java
@@ -19,6 +19,7 @@
 import java.io.Writer;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
 
@@ -118,6 +119,14 @@
       list.add(addr);
     }
 
+    void remove(java.lang.String email) {
+      for (Iterator<Address> i = list.iterator(); i.hasNext();) {
+        if (i.next().email.equals(email)) {
+          i.remove();
+        }
+      }
+    }
+
     @Override
     boolean isEmpty() {
       return list.isEmpty();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSender.java
index 411e6ca..9c4f4c4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSender.java
@@ -22,6 +22,14 @@
   boolean isEnabled();
 
   /**
+   * Can the address receive messages from us?
+   *
+   * @param address the address to consider.
+   * @return true if this sender will deliver to the address.
+   */
+  boolean canEmail(String address);
+
+  /**
    * Sends an email message.
    *
    * @param from who the message is from.
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 11a541b..00750ef 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
@@ -25,8 +25,8 @@
   }
 
   @Inject
-  public MergeFailSender(@Assisted Change c) {
-    super(c, "comment");
+  public MergeFailSender(EmailArguments ea, @Assisted Change c) {
+    super(ea, c, "comment");
   }
 
   @Override
@@ -37,7 +37,7 @@
   }
 
   @Override
-  protected void format() {
+  protected void formatChange() {
     appendText("Change " + change.getKey().abbreviate());
     if (patchSetInfo != null && patchSetInfo.getAuthor() != null
         && patchSetInfo.getAuthor().getName() != null) {
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 a1f7d9e..caf19e4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.reviewdb.Branch;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.PatchSetApproval;
-import com.google.gerrit.server.project.ProjectState;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -37,15 +36,14 @@
     public MergedSender create(Change change);
   }
 
+  private final ApprovalTypes approvalTypes;
   private Branch.NameKey dest;
 
   @Inject
-  private ApprovalTypes approvalTypes;
-
-  @Inject
-  public MergedSender(@Assisted Change c) {
-    super(c, "merged");
+  public MergedSender(EmailArguments ea, ApprovalTypes at, @Assisted Change c) {
+    super(ea, c, "merged");
     dest = c.getDest();
+    approvalTypes = at;
   }
 
   public void setDest(final Branch.NameKey key) {
@@ -63,7 +61,7 @@
   }
 
   @Override
-  protected void format() {
+  protected void formatChange() {
     appendText("Change " + change.getKey().abbreviate());
     if (patchSetInfo != null && patchSetInfo.getAuthor() != null
         && patchSetInfo.getAuthor().getName() != null) {
@@ -78,7 +76,7 @@
   }
 
   private void formatApprovals() {
-    if (db != null && patchSet != null) {
+    if (patchSet != null) {
       try {
         final Map<Account.Id, Map<ApprovalCategory.Id, PatchSetApproval>> pos =
             new HashMap<Account.Id, Map<ApprovalCategory.Id, PatchSetApproval>>();
@@ -86,8 +84,8 @@
         final Map<Account.Id, Map<ApprovalCategory.Id, PatchSetApproval>> neg =
             new HashMap<Account.Id, Map<ApprovalCategory.Id, PatchSetApproval>>();
 
-        for (PatchSetApproval ca : db.patchSetApprovals().byPatchSet(
-            patchSet.getId())) {
+        for (PatchSetApproval ca : args.db.get().patchSetApprovals()
+            .byPatchSet(patchSet.getId())) {
           if (ca.getValue() > 0) {
             insert(pos, ca);
           } else if (ca.getValue() < 0) {
@@ -157,22 +155,18 @@
   }
 
   private void bccWatchesNotifySubmittedChanges() {
-    if (db != null) {
-      try {
-        // BCC anyone else who has interest in this project's changes
-        //
-        final ProjectState ps = getProjectState();
-        if (ps != null) {
-          for (AccountProjectWatch w : db.accountProjectWatches()
-              .notifySubmittedChanges(ps.getProject().getNameKey())) {
-            add(RecipientType.BCC, w.getAccountId());
-          }
+    try {
+      // BCC anyone else who has interest in this project's changes
+      //
+      for (final AccountProjectWatch w : getWatches()) {
+        if (w.isNotifySubmittedChanges()) {
+          add(RecipientType.BCC, w.getAccountId());
         }
-      } catch (OrmException err) {
-        // Just don't CC everyone. Better to send a partial message to those
-        // we already have queued up then to fail deliver entirely to people
-        // who have a lower interest in the change.
       }
+    } catch (OrmException err) {
+      // Just don't CC everyone. Better to send a partial message to those
+      // we already have queued up then to fail deliver entirely to people
+      // who have a lower interest in the change.
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java
index 6c35f0e..dc8c2c2 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
@@ -17,7 +17,6 @@
 import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.server.ssh.SshInfo;
-import com.google.inject.Inject;
 
 import com.jcraft.jsch.HostKey;
 
@@ -28,15 +27,14 @@
 import java.util.Set;
 
 /** Sends an email alerting a user to a new change for them to review. */
-public abstract class NewChangeSender extends OutgoingEmail {
-  @Inject
-  private SshInfo sshInfo;
-
+public abstract class NewChangeSender extends ChangeEmail {
+  private final SshInfo sshInfo;
   private final Set<Account.Id> reviewers = new HashSet<Account.Id>();
   private final Set<Account.Id> extraCC = new HashSet<Account.Id>();
 
-  protected NewChangeSender(Change c) {
-    super(c, "newchange");
+  protected NewChangeSender(EmailArguments ea, SshInfo sshInfo, Change c) {
+    super(ea, c, "newchange");
+    this.sshInfo = sshInfo;
   }
 
   public void addReviewers(final Collection<Account.Id> cc) {
@@ -59,7 +57,7 @@
   }
 
   @Override
-  protected void format() {
+  protected void formatChange() {
     formatSalutation();
     formatChangeDetail();
 
@@ -114,24 +112,31 @@
   }
 
   private String getPullUrl() {
-    final List<HostKey> hostKeys = sshInfo.getHostKeys();
-    if (hostKeys.isEmpty()) {
+    final String host = getSshHost();
+    if (host == null) {
       return "";
     }
 
-    final String host = hostKeys.get(0).getHost();
     final StringBuilder r = new StringBuilder();
     r.append("git pull ssh://");
-    if (host.startsWith("*:")) {
-      r.append(getGerritHost());
-      r.append(host.substring(1));
-    } else {
-      r.append(host);
-    }
+    r.append(host);
     r.append("/");
     r.append(projectName);
     r.append(" ");
     r.append(patchSet.getRefName());
     return r.toString();
   }
+
+  public String getSshHost() {
+    final List<HostKey> hostKeys = sshInfo.getHostKeys();
+    if (hostKeys.isEmpty()) {
+      return null;
+    }
+
+    final String host = hostKeys.get(0).getHost();
+    if (host.startsWith("*:")) {
+      return getGerritHost() + host.substring(1);
+    }
+    return host;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
index 41f532c..fe66636 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
@@ -22,25 +22,24 @@
 import com.google.gerrit.reviewdb.PatchSet;
 import com.google.gerrit.reviewdb.PatchSetApproval;
 import com.google.gerrit.reviewdb.PatchSetInfo;
-import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.reviewdb.StarredChange;
 import com.google.gerrit.reviewdb.UserIdentity;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.config.CanonicalWebUrl;
-import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.mail.EmailHeader.AddressList;
 import com.google.gerrit.server.patch.PatchList;
-import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchListEntry;
-import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
-import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import com.google.gwtorm.client.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
 
 import org.eclipse.jgit.util.SystemReader;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.net.MalformedURLException;
 import java.net.URL;
@@ -49,105 +48,47 @@
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Random;
 import java.util.Set;
-
-import javax.annotation.Nullable;
+import java.util.TreeSet;
 
 /** Sends an email to one or more interested parties. */
 public abstract class OutgoingEmail {
+  private static final Logger log = LoggerFactory.getLogger(OutgoingEmail.class);
+
   private static final String HDR_TO = "To";
   private static final String HDR_CC = "CC";
 
-  private static final Random RNG = new Random();
-  private final String messageClass;
-  protected final Change change;
-  protected String projectName;
+  protected String messageClass;
   private final HashSet<Account.Id> rcptTo = new HashSet<Account.Id>();
   private final Map<String, EmailHeader> headers;
   private final List<Address> smtpRcptTo = new ArrayList<Address>();
   private Address smtpFromAddress;
   private StringBuilder body;
-  private boolean inFooter;
 
+  protected final EmailArguments args;
   protected Account.Id fromId;
-  protected PatchSet patchSet;
-  protected PatchSetInfo patchSetInfo;
-  protected ChangeMessage changeMessage;
-  protected ReviewDb db;
 
-  @Inject
-  protected GitRepositoryManager server;
-
-  @Inject
-  private ProjectCache projectCache;
-
-  @Inject
-  private AccountCache accountCache;
-
-  @Inject
-  private PatchListCache patchListCache;
-
-  @Inject
-  private FromAddressGenerator fromAddressGenerator;
-
-  @Inject
-  private EmailSender emailSender;
-
-  @Inject
-  private PatchSetInfoFactory patchSetInfoFactory;
-
-  @Inject
-  private IdentifiedUser.GenericFactory identifiedUserFactory;
-
-  @Inject
-  @CanonicalWebUrl
-  @Nullable
-  private Provider<String> urlProvider;
-
-  private ProjectState projectState;
-
-  protected OutgoingEmail(final Change c, final String mc) {
-    change = c;
+  protected OutgoingEmail(EmailArguments ea, final String mc) {
+    args = ea;
     messageClass = mc;
     headers = new LinkedHashMap<String, EmailHeader>();
   }
 
-  protected OutgoingEmail(final String mc) {
-    this(null, mc);
-  }
-
   public void setFrom(final Account.Id id) {
     fromId = id;
   }
 
-  public void setPatchSet(final PatchSet ps) {
-    patchSet = ps;
-  }
-
-  public void setPatchSet(final PatchSet ps, final PatchSetInfo psi) {
-    patchSet = ps;
-    patchSetInfo = psi;
-  }
-
-  public void setChangeMessage(final ChangeMessage cm) {
-    changeMessage = cm;
-  }
-
-  public void setReviewDb(final ReviewDb d) {
-    db = d;
-  }
-
   /**
    * Format and enqueue the message for delivery.
    *
    * @throws EmailException
    */
   public void send() throws EmailException {
-    if (!emailSender.isEnabled()) {
+    if (!args.emailSender.isEnabled()) {
       // Server has explicitly disabled email sending.
       //
       return;
@@ -157,49 +98,38 @@
     format();
     if (shouldSendMessage()) {
       if (fromId != null) {
-        // If we are impersonating a user, make sure they receive a CC of
-        // this message so they can always review and audit what we sent
-        // on their behalf to others.
-        //
-        add(RecipientType.CC, fromId);
-      }
-      if (change != null) {
-        if (getChangeUrl() != null) {
-          openFooter();
-          appendText("To view visit ");
-          appendText(getChangeUrl());
-          appendText("\n");
-        }
-        if (getSettingsUrl() != null) {
-          openFooter();
-          appendText("To unsubscribe, visit ");
-          appendText(getSettingsUrl());
-          appendText("\n");
-        }
+        final Account fromUser = args.accountCache.get(fromId).getAccount();
 
-        if (inFooter) {
-          appendText("\n");
-        } else {
-          openFooter();
+        if (fromUser.getGeneralPreferences().isCopySelfOnEmails()) {
+          // If we are impersonating a user, make sure they receive a CC of
+          // this message so they can always review and audit what we sent
+          // on their behalf to others.
+          //
+          add(RecipientType.CC, fromId);
+
+        } else if (rcptTo.remove(fromId)) {
+          // If they don't want a copy, but we queued one up anyway,
+          // drop them from the recipient lists.
+          //
+          final String fromEmail = fromUser.getPreferredEmail();
+          for (Iterator<Address> i = smtpRcptTo.iterator(); i.hasNext();) {
+            if (i.next().email.equals(fromEmail)) {
+              i.remove();
+            }
+          }
+          for (EmailHeader hdr : headers.values()) {
+            if (hdr instanceof AddressList) {
+              ((AddressList) hdr).remove(fromEmail);
+            }
+          }
+
+          if (smtpRcptTo.isEmpty()) {
+            return;
+          }
         }
-        appendText("Gerrit-MessageType: " + messageClass + "\n");
-        appendText("Gerrit-Project: " + projectName + "\n");
-        appendText("Gerrit-Branch: " + change.getDest().getShortName() + "\n");
       }
 
-      if (headers.get("Message-ID").isEmpty()) {
-        final StringBuilder rndid = new StringBuilder();
-        rndid.append("<");
-        rndid.append(System.currentTimeMillis());
-        rndid.append("-");
-        rndid.append(Integer.toString(RNG.nextInt(999999), 36));
-        rndid.append("@");
-        rndid.append(SystemReader.getInstance().getHostname());
-        rndid.append(">");
-        setHeader("Message-ID", rndid.toString());
-      }
-
-      emailSender.send(smtpFromAddress, smtpRcptTo, headers, body.toString());
+      args.emailSender.send(smtpFromAddress, smtpRcptTo, headers, body.toString());
     }
   }
 
@@ -208,27 +138,11 @@
 
   /** Setup the message headers and envelope (TO, CC, BCC). */
   protected void init() {
-    if (change != null && projectCache != null) {
-      projectState = projectCache.get(change.getProject());
-      projectName =
-          projectState != null ? projectState.getProject().getName() : null;
-    } else {
-      projectState = null;
-      projectName = null;
-    }
-
-    smtpFromAddress = fromAddressGenerator.from(fromId);
-    if (changeMessage != null && changeMessage.getWrittenOn() != null) {
-      setHeader("Date", new Date(changeMessage.getWrittenOn().getTime()));
-    } else {
-      setHeader("Date", new Date());
-    }
+    smtpFromAddress = args.fromAddressGenerator.from(fromId);
+    setHeader("Date", new Date());
     headers.put("From", new EmailHeader.AddressList(smtpFromAddress));
     headers.put(HDR_TO, new EmailHeader.AddressList());
     headers.put(HDR_CC, new EmailHeader.AddressList());
-    if (change != null) {
-      setChangeSubjectHeader();
-    }
     setHeader("Message-ID", "");
 
     if (fromId != null) {
@@ -244,17 +158,10 @@
     }
 
     setHeader("X-Gerrit-MessageType", messageClass);
-    if (change != null) {
-      setHeader("X-Gerrit-Change-Id", "" + change.getKey().get());
-      setListIdHeader();
-      setChangeUrlHeader();
-      setCommitIdHeader();
-    }
     body = new StringBuilder();
-    inFooter = false;
 
-    if (fromId != null && fromAddressGenerator.isGenericAddress(fromId)) {
-      final Account account = accountCache.get(fromId).getAccount();
+    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();
 
@@ -270,75 +177,6 @@
         body.append(":\n\n");
       }
     }
-
-    if (change != null && db != null) {
-      if (patchSet == null) {
-        try {
-          patchSet = db.patchSets().get(change.currentPatchSetId());
-        } catch (OrmException err) {
-          patchSet = null;
-        }
-      }
-
-      if (patchSet != null && patchSetInfo == null) {
-        try {
-          patchSetInfo = patchSetInfoFactory.get(patchSet.getId());
-        } catch (PatchSetInfoNotAvailableException err) {
-          patchSetInfo = null;
-        }
-      }
-    }
-  }
-
-  private void setListIdHeader() {
-    // 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('@', '.') + ">");
-    if (getSettingsUrl() != null) {
-      setHeader("List-Unsubscribe", "<" + getSettingsUrl() + ">");
-    }
-  }
-
-  private void setChangeUrlHeader() {
-    final String u = getChangeUrl();
-    if (u != null) {
-      setHeader("X-Gerrit-ChangeURL", "<" + u + ">");
-    }
-  }
-
-  private void setCommitIdHeader() {
-    if (patchSet != null && patchSet.getRevision() != null
-        && patchSet.getRevision().get() != null
-        && patchSet.getRevision().get().length() > 0) {
-      setHeader("X-Gerrit-Commit", patchSet.getRevision().get());
-    }
-  }
-
-  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());
   }
 
   protected String getGerritHost() {
@@ -357,18 +195,7 @@
     return SystemReader.getInstance().getHostname();
   }
 
-  /** Get a link to the change; null if the server doesn't know its own address. */
-  protected String getChangeUrl() {
-    if (change != null && getGerritUrl() != null) {
-      final StringBuilder r = new StringBuilder();
-      r.append(getGerritUrl());
-      r.append(change.getChangeId());
-      return r.toString();
-    }
-    return null;
-  }
-
-  private String getSettingsUrl() {
+  public String getSettingsUrl() {
     if (getGerritUrl() != null) {
       final StringBuilder r = new StringBuilder();
       r.append(getGerritUrl());
@@ -379,21 +206,7 @@
   }
 
   protected String getGerritUrl() {
-    return urlProvider.get();
-  }
-
-  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();
+    return args.urlProvider.get();
   }
 
   /** Set a header in the outgoing message. */
@@ -412,67 +225,13 @@
     }
   }
 
-  private void openFooter() {
-    if (!inFooter) {
-      inFooter = true;
-      appendText("-- \n");
-    }
-  }
-
-  /** Format the sender's "cover letter", {@link #getCoverLetter()}. */
-  protected void formatCoverLetter() {
-    final String cover = getCoverLetter();
-    if (!"".equals(cover)) {
-      appendText(cover);
-      appendText("\n\n");
-    }
-  }
-
-  /** Get the text of the "cover letter", from {@link ChangeMessage}. */
-  protected String getCoverLetter() {
-    if (changeMessage != null) {
-      final String txt = changeMessage.getMessage();
-      if (txt != null) {
-        return txt.trim();
-      }
-    }
-    return "";
-  }
-
-  /** Format the change message and the affected file list. */
-  protected void formatChangeDetail() {
-    if (patchSetInfo != null) {
-      appendText(patchSetInfo.getMessage().trim());
-      appendText("\n");
-    } else {
-      appendText(change.getSubject().trim());
-      appendText("\n");
-    }
-
-    if (patchSet != null) {
-      appendText("---\n");
-      for (PatchListEntry p : getPatchList().getPatches()) {
-        appendText(p.getChangeType().getCode() + " " + p.getNewName() + "\n");
-      }
-      appendText("\n");
-    }
-  }
-
-  /** Get the patch list corresponding to this patch set. */
-  protected PatchList getPatchList() {
-    if (patchSet != null) {
-      return patchListCache.get(change, patchSet);
-    }
-    return null;
-  }
-
   /** Lookup a human readable name for an account, usually the "full name". */
   protected String getNameFor(final Account.Id accountId) {
     if (accountId == null) {
       return "Anonymous Coward";
     }
 
-    final Account userAccount = accountCache.get(accountId).getAccount();
+    final Account userAccount = args.accountCache.get(accountId).getAccount();
     String name = userAccount.getFullName();
     if (name == null) {
       name = userAccount.getPreferredEmail();
@@ -483,6 +242,24 @@
     return name;
   }
 
+  protected String getNameEmailFor(Account.Id accountId) {
+    AccountState who = args.accountCache.get(accountId);
+    String name = who.getAccount().getFullName();
+    String email = who.getAccount().getPreferredEmail();
+
+    if (name != null && email != null) {
+      return name + " <" + email + ">";
+
+    } else if (name != null) {
+      return name;
+    } else if (email != null) {
+      return email;
+
+    } else /* (name == null && email == null) */{
+      return "Anonymous Coward #" + accountId;
+    }
+  }
+
   protected boolean shouldSendMessage() {
     if (body.length() == 0) {
       // If we have no message body, don't send.
@@ -490,7 +267,7 @@
       return false;
     }
 
-    if (rcptTo.isEmpty()) {
+    if (smtpRcptTo.isEmpty()) {
       // If we have nobody to send this message to, then all of our
       // selection filters previously for this type of message were
       // unable to match a destination. Don't bother sending it.
@@ -507,19 +284,6 @@
     return true;
   }
 
-  /** Get the project entity the change is in; null if its been deleted. */
-  protected ProjectState getProjectState() {
-    return projectState;
-  }
-
-  /** Get the groups which own the project. */
-  protected Set<AccountGroup.Id> getProjectOwners() {
-    final ProjectState r;
-
-    r = projectCache.get(change.getProject());
-    return r != null ? r.getOwners() : Collections.<AccountGroup.Id> emptySet();
-  }
-
   /** Schedule this message for delivery to the listed accounts. */
   protected void add(final RecipientType rt, final Collection<Account.Id> list) {
     for (final Account.Id id : list) {
@@ -527,89 +291,12 @@
     }
   }
 
-  /** TO or CC all vested parties (change owner, patch set uploader, author). */
-  protected void rcptToAuthors(final RecipientType rt) {
-    add(rt, change.getOwner());
-    if (patchSet != null) {
-      add(rt, patchSet.getUploader());
-    }
-    if (patchSetInfo != null) {
-      add(rt, patchSetInfo.getAuthor());
-      add(rt, patchSetInfo.getCommitter());
-    }
-  }
-
-  private void add(final RecipientType rt, final UserIdentity who) {
+  protected void add(final RecipientType rt, final UserIdentity who) {
     if (who != null && who.getAccount() != null) {
       add(rt, who.getAccount());
     }
   }
 
-  /** BCC any user who has starred this change. */
-  protected void bccStarredBy() {
-    if (db != null) {
-      try {
-        // BCC anyone who has starred this change.
-        //
-        for (StarredChange w : db.starredChanges().byChange(change.getId())) {
-          add(RecipientType.BCC, w.getAccountId());
-        }
-      } catch (OrmException err) {
-        // Just don't BCC everyone. Better to send a partial message to those
-        // we already have queued up then to fail deliver entirely to people
-        // who have a lower interest in the change.
-      }
-    }
-  }
-
-  /** BCC any user who has set "notify all comments" on this project. */
-  protected void bccWatchesNotifyAllComments() {
-    if (db != null) {
-      try {
-        // BCC anyone else who has interest in this project's changes
-        //
-        final ProjectState ps = getProjectState();
-        if (ps != null) {
-          for (final AccountProjectWatch w : db.accountProjectWatches()
-              .notifyAllComments(ps.getProject().getNameKey())) {
-            add(RecipientType.BCC, w.getAccountId());
-          }
-        }
-      } catch (OrmException err) {
-        // Just don't CC everyone. Better to send a partial message to those
-        // we already have queued up then to fail deliver entirely to people
-        // who have a lower interest in the change.
-      }
-    }
-  }
-
-  /** Any user who has published comments on this change. */
-  protected void ccAllApprovals() {
-    ccApprovals(true);
-  }
-
-  /** Users who have non-zero approval codes on the change. */
-  protected void ccExistingReviewers() {
-    ccApprovals(false);
-  }
-
-  private void ccApprovals(final boolean includeZero) {
-    if (db != null) {
-      try {
-        // CC anyone else who has posted an approval mark on this change
-        //
-        for (PatchSetApproval ap : db.patchSetApprovals().byChange(
-            change.getId())) {
-          if (!includeZero && ap.getValue() == 0) {
-            continue;
-          }
-          add(RecipientType.CC, ap.getAccountId());
-        }
-      } catch (OrmException err) {
-      }
-    }
-  }
-
   /** Schedule delivery of this message to the given account. */
   protected void add(final RecipientType rt, final Account.Id to) {
     if (!rcptTo.contains(to) && isVisibleTo(to)) {
@@ -618,30 +305,31 @@
     }
   }
 
-  private boolean isVisibleTo(final Account.Id to) {
-    return projectState == null
-        || change == null
-        || projectState.controlFor(identifiedUserFactory.create(to))
-            .controlFor(change).isVisible();
+  protected boolean isVisibleTo(final Account.Id to) {
+    return true;
   }
 
   /** Schedule delivery of this message to the given account. */
   protected void add(final RecipientType rt, final Address addr) {
     if (addr != null && addr.email != null && addr.email.length() > 0) {
-      smtpRcptTo.add(addr);
-      switch (rt) {
-        case TO:
-          ((EmailHeader.AddressList) headers.get(HDR_TO)).add(addr);
-          break;
-        case CC:
-          ((EmailHeader.AddressList) headers.get(HDR_CC)).add(addr);
-          break;
+      if (args.emailSender.canEmail(addr.email)) {
+        smtpRcptTo.add(addr);
+        switch (rt) {
+          case TO:
+            ((EmailHeader.AddressList) headers.get(HDR_TO)).add(addr);
+            break;
+          case CC:
+            ((EmailHeader.AddressList) headers.get(HDR_CC)).add(addr);
+            break;
+        }
+      } else {
+        log.warn("Not emailing " + addr.email + " (prohibited by allowrcpt)");
       }
     }
   }
 
   private Address toAddress(final Account.Id id) {
-    final Account a = accountCache.get(id).getAccount();
+    final Account a = args.accountCache.get(id).getAccount();
     final String e = a.getPreferredEmail();
     if (e == null) {
       return null;
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 d22cc59..9b201fd 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
@@ -28,14 +28,14 @@
     public RegisterNewEmailSender create(String address);
   }
 
+  private final AuthConfig authConfig;
   private final String addr;
 
   @Inject
-  private AuthConfig authConfig;
-
-  @Inject
-  public RegisterNewEmailSender(@Assisted final String address) {
-    super("registernewemail");
+  public RegisterNewEmailSender(EmailArguments ea, AuthConfig ac,
+      @Assisted final String address) {
+    super(ea, "registernewemail");
+    authConfig = ac;
     addr = address;
   }
 
@@ -56,14 +56,7 @@
     final StringBuilder url = new StringBuilder();
     url.append(getGerritUrl());
     url.append("#VE,");
-    try {
-      url.append(authConfig.getEmailRegistrationToken().newToken(
-          Base64.encodeBytes(addr.getBytes("UTF-8"))));
-    } catch (XsrfException e) {
-      throw new IllegalArgumentException(e);
-    } catch (UnsupportedEncodingException e) {
-      throw new IllegalArgumentException(e);
-    }
+    url.append(getEmailRegistrationToken());
 
     appendText("Welcome to Gerrit Code Review at ");
     appendText(getGerritHost());
@@ -93,4 +86,15 @@
         + "  Replies to this message will not\n");
     appendText("be read or answered.\n");
   }
+
+  public String getEmailRegistrationToken() {
+    try {
+      return authConfig.getEmailRegistrationToken().newToken(
+          Base64.encodeBytes(addr.getBytes("UTF-8")));
+    } catch (XsrfException e) {
+      throw new IllegalArgumentException(e);
+    } catch (UnsupportedEncodingException e) {
+      throw new IllegalArgumentException(e);
+    }
+  }
 }
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 70ed4f5..841aa35 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
@@ -34,15 +34,14 @@
     public ReplacePatchSetSender create(Change change);
   }
 
-  @Inject
-  private SshInfo sshInfo;
-
   private final Set<Account.Id> reviewers = new HashSet<Account.Id>();
   private final Set<Account.Id> extraCC = new HashSet<Account.Id>();
+  private final SshInfo sshInfo;
 
   @Inject
-  public ReplacePatchSetSender(@Assisted Change c) {
-    super(c, "newpatchset");
+  public ReplacePatchSetSender(EmailArguments ea, SshInfo si, @Assisted Change c) {
+    super(ea, c, "newpatchset");
+    sshInfo = si;
   }
 
   public void addReviewers(final Collection<Account.Id> cc) {
@@ -68,7 +67,7 @@
   }
 
   @Override
-  protected void format() {
+  protected void formatChange() {
     formatSalutation();
     formatChangeDetail();
 
@@ -126,24 +125,31 @@
   }
 
   private String getPullUrl() {
-    final List<HostKey> hostKeys = sshInfo.getHostKeys();
-    if (hostKeys.isEmpty()) {
+    final String host = getSshHost();
+    if (host == null) {
       return "";
     }
 
-    final String host = hostKeys.get(0).getHost();
     final StringBuilder r = new StringBuilder();
     r.append("git pull ssh://");
-    if (host.startsWith("*:")) {
-      r.append(getGerritHost());
-      r.append(host.substring(1));
-    } else {
-      r.append(host);
-    }
+    r.append(host);
     r.append("/");
     r.append(projectName);
     r.append(" ");
     r.append(patchSet.getRefName());
     return r.toString();
   }
+
+  public String getSshHost() {
+    final List<HostKey> hostKeys = sshInfo.getHostKeys();
+    if (hostKeys.isEmpty()) {
+      return null;
+    }
+
+    final String host = hostKeys.get(0).getHost();
+    if (host.startsWith("*:")) {
+      return getGerritHost() + host.substring(1);
+    }
+    return host;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplyToChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplyToChangeSender.java
index 99e9565..05d2753 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
@@ -17,9 +17,9 @@
 import com.google.gerrit.reviewdb.Change;
 
 /** Alert a user to a reply to a change, usually commentary made during review. */
-public abstract class ReplyToChangeSender extends OutgoingEmail {
-  protected ReplyToChangeSender(Change c, String mc) {
-    super(c, mc);
+public abstract class ReplyToChangeSender extends ChangeEmail {
+  protected ReplyToChangeSender(EmailArguments ea, Change c, String mc) {
+    super(ea, c, mc);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java
index 1fb31a0..d67d3b3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java
@@ -29,8 +29,11 @@
 import java.io.IOException;
 import java.io.Writer;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.Map;
+import java.util.Set;
 
 /** Sends email via a nearby SMTP server. */
 @Singleton
@@ -47,7 +50,7 @@
   private String smtpPass;
   private Encryption smtpEncryption;
   private boolean sslVerify;
-  private String[] allowrcpt;
+  private Set<String> allowrcpt;
 
   @Inject
   SmtpEmailSender(@GerritServerConfig final Config cfg) {
@@ -79,7 +82,12 @@
 
     smtpUser = cfg.getString("sendemail", null, "smtpuser");
     smtpPass = cfg.getString("sendemail", null, "smtppass");
-    allowrcpt = cfg.getStringList("sendemail", null, "allowrcpt");
+
+    Set<String> rcpt = new HashSet<String>();
+    for (String addr : cfg.getStringList("sendemail", null, "allowrcpt")) {
+      rcpt.add(addr);
+    }
+    allowrcpt = Collections.unmodifiableSet(rcpt);
   }
 
   @Override
@@ -88,7 +96,29 @@
   }
 
   @Override
-  public void send(final Address from, final Collection<Address> rcpt,
+  public boolean canEmail(String address) {
+    if (!isEnabled()) {
+      return false;
+    }
+
+    if (allowrcpt.isEmpty()) {
+      return true;
+    }
+
+    if (allowrcpt.contains(address)) {
+      return true;
+    }
+
+    String domain = address.substring(address.lastIndexOf('@') + 1);
+    if (allowrcpt.contains(domain) || allowrcpt.contains("@" + domain)) {
+      return true;
+    }
+
+    return false;
+  }
+
+  @Override
+  public void send(final Address from, Collection<Address> rcpt,
       final Map<String, EmailHeader> callerHeaders, final String body)
       throws EmailException {
     if (!isEnabled()) {
@@ -161,7 +191,6 @@
 
   private SMTPClient open() throws EmailException {
     final AuthSMTPClient client = new AuthSMTPClient("UTF-8");
-    client.setAllowRcpt(allowrcpt);
 
     if (smtpEncryption == Encryption.SSL) {
       client.enableSSL(sslVerify);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java
index 209d0b7..1a6d2ae 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.server.patch;
 
-import com.google.gerrit.common.data.PatchScriptSettings.Whitespace;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.reviewdb.AccountDiffPreference.Whitespace;
 
 /** Provides a cached list of {@link PatchListEntry}. */
 public interface PatchListCache {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
index 835db51..f55763f 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
@@ -11,19 +11,64 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
+//
+// Some portions (e.g. outputDiff) below are:
+//
+// Copyright (C) 2009, Christian Halstrick <christian.halstrick@sap.com>
+// Copyright (C) 2009, Johannes E. Schindelin
+// Copyright (C) 2009, Johannes Schindelin <johannes.schindelin@gmx.de>
+// and other copyright owners as documented in the project's IP log.
+//
+// This program and the accompanying materials are made available
+// under the terms of the Eclipse Distribution License v1.0 which
+// accompanies this distribution, is reproduced below, and is
+// available at http://www.eclipse.org/org/documents/edl-v10.php
+//
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or
+// without modification, are permitted provided that the following
+// conditions are met:
+//
+// - Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//
+// - Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following
+// disclaimer in the documentation and/or other materials provided
+// with the distribution.
+//
+// - Neither the name of the Eclipse Foundation, Inc. nor the
+// names of its contributors may be used to endorse or promote
+// products derived from this software without specific prior
+// written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+// OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+// NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+//
 
 package com.google.gerrit.server.patch;
 
-import static com.google.gerrit.common.data.PatchScriptSettings.Whitespace.IGNORE_NONE;
 
-import com.google.gerrit.common.data.PatchScriptSettings.Whitespace;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.PatchSet;
 import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.AccountDiffPreference.Whitespace;
 import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.cache.EntryCreator;
 import com.google.gerrit.server.cache.EvictionPolicy;
-import com.google.gerrit.server.cache.SelfPopulatingCache;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
@@ -32,11 +77,16 @@
 import com.google.inject.TypeLiteral;
 import com.google.inject.name.Named;
 
+import org.eclipse.jgit.diff.DiffEntry;
+import org.eclipse.jgit.diff.DiffFormatter;
 import org.eclipse.jgit.diff.Edit;
 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.ReplaceEdit;
-import org.eclipse.jgit.errors.IncorrectObjectTypeException;
-import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Constants;
@@ -51,9 +101,10 @@
 import org.eclipse.jgit.revwalk.RevTree;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.filter.TreeFilter;
+import org.eclipse.jgit.util.io.DisabledOutputStream;
 
 import java.io.IOException;
-import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -63,7 +114,6 @@
 @Singleton
 public class PatchListCacheImpl implements PatchListCache {
   private static final String CACHE_NAME = "diff";
-  private static final boolean dynamic = false;
 
   private static final Pattern BLANK_LINE_RE =
       Pattern.compile("^[ \\t]*(|[{}]|/\\*\\*?|\\*)[ \\t]*$");
@@ -79,6 +129,7 @@
         disk(type, CACHE_NAME) //
             .memoryLimit(128) // very large items, cache only a few
             .evictionPolicy(EvictionPolicy.LRU) // prefer most recent
+            .populateWith(Loader.class) //
         ;
         bind(PatchListCacheImpl.class);
         bind(PatchListCache.class).to(PatchListCacheImpl.class);
@@ -86,37 +137,20 @@
     };
   }
 
-  private final GitRepositoryManager repoManager;
-  private final SelfPopulatingCache<PatchListKey, PatchList> self;
-  private final boolean computeIntraline;
+  private final Cache<PatchListKey, PatchList> cache;
 
   @Inject
-  PatchListCacheImpl(final GitRepositoryManager grm,
-      @GerritServerConfig final Config config,
-      @Named(CACHE_NAME) final Cache<PatchListKey, PatchList> raw) {
-    repoManager = grm;
-    computeIntraline = config.getBoolean("cache", "diff", "intraline", true);
-    self = new SelfPopulatingCache<PatchListKey, PatchList>(raw) {
-      @Override
-      protected PatchList createEntry(final PatchListKey key) throws Exception {
-        return compute(key);
-      }
-    };
+  PatchListCacheImpl(
+      @Named(CACHE_NAME) final Cache<PatchListKey, PatchList> thecache) {
+    cache = thecache;
   }
 
   public PatchList get(final PatchListKey key) {
-    if (dynamic) {
-      try {
-        return compute(key);
-      } catch (Exception e) {
-        throw new RuntimeException("Cannot lookup " + key, e);
-      }
-    }
-    return self.get(key);
+    return cache.get(key);
   }
 
   public PatchList get(final Change change, final PatchSet patchSet) {
-    return get(change, patchSet, IGNORE_NONE);
+    return get(change, patchSet, Whitespace.IGNORE_NONE);
   }
 
   public PatchList get(final Change change, final PatchSet patchSet,
@@ -127,417 +161,406 @@
     return get(new PatchListKey(projectKey, a, b, whitespace));
   }
 
-  private PatchList compute(final PatchListKey key)
-      throws MissingObjectException, IncorrectObjectTypeException, IOException {
-    final Repository repo = repoManager.openRepository(key.projectKey.get());
-    try {
-      return readPatchList(key, repo);
-    } finally {
-      repo.close();
-    }
-  }
+  static class Loader extends EntryCreator<PatchListKey, PatchList> {
+    private final GitRepositoryManager repoManager;
+    private final boolean computeIntraline;
 
-  private PatchList readPatchList(final PatchListKey key, final Repository repo)
-      throws IOException {
-    final RevWalk rw = new RevWalk(repo);
-    final RevCommit b = rw.parseCommit(key.getNewId());
-    final AnyObjectId a = aFor(key, repo, b);
-
-    final List<String> args = new ArrayList<String>();
-    args.add("git");
-    args.add("--git-dir=.");
-    args.add("diff-tree");
-    args.add("-M");
-    switch (key.getWhitespace()) {
-      case IGNORE_NONE:
-        break;
-      case IGNORE_SPACE_AT_EOL:
-        args.add("--ignore-space-at-eol");
-        break;
-      case IGNORE_SPACE_CHANGE:
-        args.add("--ignore-space-change");
-        break;
-      case IGNORE_ALL_SPACE:
-        args.add("--ignore-all-space");
-        break;
-      default:
-        throw new IOException("Unsupported whitespace " + key.getWhitespace());
-    }
-    if (a == null /* want combined diff */) {
-      args.add("--cc");
-      args.add(b.name());
-    } else {
-      args.add("--unified=1");
-      args.add(a.name());
-      args.add(b.name());
+    @Inject
+    Loader(GitRepositoryManager mgr, @GerritServerConfig Config config) {
+      repoManager = mgr;
+      computeIntraline = config.getBoolean("cache", "diff", "intraline", true);
     }
 
-    final org.eclipse.jgit.patch.Patch p = new org.eclipse.jgit.patch.Patch();
-    final Process diffProcess = exec(repo, args);
-    try {
-      diffProcess.getOutputStream().close();
-      diffProcess.getErrorStream().close();
-
-      final InputStream in = diffProcess.getInputStream();
+    @Override
+    public PatchList createEntry(final PatchListKey key) throws Exception {
+      final Repository repo = repoManager.openRepository(key.projectKey.get());
       try {
-        p.parse(in);
+        return readPatchList(key, repo);
       } finally {
-        in.close();
+        repo.close();
       }
-    } finally {
-      try {
-        final int rc = diffProcess.waitFor();
-        if (rc != 0) {
-          throw new IOException("git diff-tree exited abnormally: " + rc);
+    }
+
+    private PatchList readPatchList(final PatchListKey key,
+        final Repository repo) throws IOException {
+      // TODO(jeffschu) correctly handle merge commits
+
+      final RevWalk rw = new RevWalk(repo);
+      final RevCommit b = rw.parseCommit(key.getNewId());
+      final AnyObjectId a = aFor(key, repo, b);
+
+      if (a == null) {
+        return new PatchList(a, b, computeIntraline, new PatchListEntry[0]);
+      }
+
+      RevTree aTree = rw.parseTree(a);
+      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);
+      switch (key.getWhitespace()) {
+        case IGNORE_ALL_SPACE:
+          df.setRawTextFactory(RawTextIgnoreAllWhitespace.FACTORY);
+          break;
+        case IGNORE_NONE:
+          df.setRawTextFactory(RawText.FACTORY);
+          break;
+        case IGNORE_SPACE_AT_EOL:
+          df.setRawTextFactory(RawTextIgnoreTrailingWhitespace.FACTORY);
+          break;
+        case IGNORE_SPACE_CHANGE:
+          df.setRawTextFactory(RawTextIgnoreWhitespaceChange.FACTORY);
+          break;
+      }
+
+      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[cnt];
+      for (int i = 0; i < cnt; i++) {
+        FileHeader fh = df.createFileHeader(diffEntries.get(i));
+        entries[i] = newEntry(repo, aTree, bTree, fh);
+      }
+      return new PatchList(a, b, computeIntraline, entries);
+    }
+
+    private PatchListEntry newEntry(Repository repo, RevTree aTree,
+        RevTree bTree, FileHeader fileHeader) throws IOException {
+      final FileMode oldMode = fileHeader.getOldMode();
+      final FileMode newMode = fileHeader.getNewMode();
+
+      if (oldMode == FileMode.GITLINK || newMode == FileMode.GITLINK) {
+        return new PatchListEntry(fileHeader, Collections.<Edit> emptyList());
+      }
+
+      if (aTree == null // want combined diff
+          || fileHeader.getPatchType() != PatchType.UNIFIED
+          || fileHeader.getHunks().isEmpty()) {
+        return new PatchListEntry(fileHeader, Collections.<Edit> emptyList());
+      }
+
+      List<Edit> edits = fileHeader.toEditList();
+      if (edits.isEmpty()) {
+        return new PatchListEntry(fileHeader, Collections.<Edit> emptyList());
+      }
+      if (!computeIntraline) {
+        return new PatchListEntry(fileHeader, edits);
+      }
+
+      switch (fileHeader.getChangeType()) {
+        case ADD:
+        case DELETE:
+          return new PatchListEntry(fileHeader, edits);
+      }
+
+      Text aContent = null;
+      Text bContent = null;
+
+      for (int i = 0; i < edits.size(); i++) {
+        Edit e = edits.get(i);
+
+        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);
+            combineLineEdits(edits, aContent, bContent);
+            i = -1; // restart the entire scan after combining lines.
+            continue;
+          }
+
+          CharText a = new CharText(aContent, e.getBeginA(), e.getEndA());
+          CharText b = new CharText(bContent, e.getBeginB(), e.getEndB());
+
+          List<Edit> wordEdits = new MyersDiff(a, b).getEdits();
+
+          // Combine edits that are really close together. If they are
+          // just a few characters apart we tend to get better results
+          // by joining them together and taking the whole span.
+          //
+          for (int j = 0; j < wordEdits.size() - 1;) {
+            Edit c = wordEdits.get(j);
+            Edit n = wordEdits.get(j + 1);
+
+            if (n.getBeginA() - c.getEndA() <= 5
+                || n.getBeginB() - c.getEndB() <= 5) {
+              int ab = c.getBeginA();
+              int ae = n.getEndA();
+
+              int bb = c.getBeginB();
+              int be = n.getEndB();
+
+              if (canCoalesce(a, c.getEndA(), n.getBeginA())
+                  && canCoalesce(b, c.getEndB(), n.getBeginB())) {
+                wordEdits.set(j, new Edit(ab, ae, bb, be));
+                wordEdits.remove(j + 1);
+                continue;
+              }
+            }
+
+            j++;
+          }
+
+          // Apply some simple rules to fix up some of the edits. Our
+          // logic above, along with our per-character difference tends
+          // to produce some crazy stuff.
+          //
+          for (int j = 0; j < wordEdits.size(); j++) {
+            Edit c = wordEdits.get(j);
+            int ab = c.getBeginA();
+            int ae = c.getEndA();
+
+            int bb = c.getBeginB();
+            int be = c.getEndB();
+
+            // Sometimes the diff generator produces an INSERT or DELETE
+            // right up against a REPLACE, but we only find this after
+            // we've also played some shifting games on the prior edit.
+            // If that happened to us, coalesce them together so we can
+            // correct this mess for the user. If we don't we wind up
+            // with silly stuff like "es" -> "es = Addresses".
+            //
+            if (1 < j) {
+              Edit p = wordEdits.get(j - 1);
+              if (p.getEndA() == ab || p.getEndB() == bb) {
+                if (p.getEndA() == ab && p.getBeginA() < p.getEndA()) {
+                  ab = p.getBeginA();
+                }
+                if (p.getEndB() == bb && p.getBeginB() < p.getEndB()) {
+                  bb = p.getBeginB();
+                }
+                wordEdits.remove(--j);
+              }
+            }
+
+            // We sometimes collapsed an edit together in a strange way,
+            // 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)) {
+              ab++;
+              bb++;
+            }
+            while (ab < ae && bb < be && a.equals(ae - 1, b, be - 1)) {
+              ae--;
+              be--;
+            }
+
+            // The leading part of an edit and its trailing part in the same
+            // text might be identical. Slide down that edit and use the tail
+            // rather than the leading bit. If however the edit is only on a
+            // whitespace block try to shift it to the left margin, assuming
+            // that it is an indentation change.
+            //
+            boolean aShift = true;
+            if (ab < ae && isOnlyWhitespace(a, ab, ae)) {
+              int lf = findLF(wordEdits, j, a, ab);
+              if (lf < ab && a.charAt(lf) == '\n') {
+                int nb = lf + 1;
+                int p = 0;
+                while (p < ae - ab) {
+                  if (a.equals(ab + p, a, ab + p))
+                    p++;
+                  else
+                    break;
+                }
+                if (p == ae - ab) {
+                  ab = nb;
+                  ae = nb + p;
+                  aShift = false;
+                }
+              }
+            }
+            if (aShift) {
+              while (0 < ab && ab < ae && a.charAt(ab - 1) != '\n'
+                  && a.equals(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)) {
+                  ab++;
+                  ae++;
+                  if (a.charAt(ae - 1) == '\n') {
+                    break;
+                  }
+                }
+              }
+            }
+
+            boolean bShift = true;
+            if (bb < be && isOnlyWhitespace(b, bb, be)) {
+              int lf = findLF(wordEdits, j, b, bb);
+              if (lf < bb && b.charAt(lf) == '\n') {
+                int nb = lf + 1;
+                int p = 0;
+                while (p < be - bb) {
+                  if (b.equals(bb + p, b, bb + p))
+                    p++;
+                  else
+                    break;
+                }
+                if (p == be - bb) {
+                  bb = nb;
+                  be = nb + p;
+                  bShift = false;
+                }
+              }
+            }
+            if (bShift) {
+              while (0 < bb && bb < be && b.charAt(bb - 1) != '\n'
+                  && b.equals(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)) {
+                  bb++;
+                  be++;
+                  if (b.charAt(be - 1) == '\n') {
+                    break;
+                  }
+                }
+              }
+            }
+
+            // If most of a line was modified except the LF was common, make
+            // the LF part of the modification region. This is easier to read.
+            //
+            if (ab < ae //
+                && (ab == 0 || a.charAt(ab - 1) == '\n') //
+                && ae < a.size() && a.charAt(ae) == '\n') {
+              ae++;
+            }
+            if (bb < be //
+                && (bb == 0 || b.charAt(bb - 1) == '\n') //
+                && be < b.size() && b.charAt(be) == '\n') {
+              be++;
+            }
+
+            wordEdits.set(j, new Edit(ab, ae, bb, be));
+          }
+
+          edits.set(i, new ReplaceEdit(e, wordEdits));
         }
-      } catch (InterruptedException ie) {
       }
-    }
 
-    RevTree aTree = a != null ? rw.parseTree(a) : null;
-    RevTree bTree = b.getTree();
-
-    final int cnt = p.getFiles().size();
-    final PatchListEntry[] entries = new PatchListEntry[cnt];
-    for (int i = 0; i < cnt; i++) {
-      entries[i] = newEntry(repo, aTree, bTree, p.getFiles().get(i));
-    }
-    return new PatchList(a, b, computeIntraline, entries);
-  }
-
-  private PatchListEntry newEntry(Repository repo, RevTree aTree,
-      RevTree bTree, FileHeader fileHeader) throws IOException {
-    final FileMode oldMode = fileHeader.getOldMode();
-    final FileMode newMode = fileHeader.getNewMode();
-
-    if (oldMode == FileMode.GITLINK || newMode == FileMode.GITLINK) {
-      return new PatchListEntry(fileHeader, Collections.<Edit> emptyList());
-    }
-
-    if (aTree == null // want combined diff
-        || fileHeader.getPatchType() != PatchType.UNIFIED
-        || fileHeader.getHunks().isEmpty()) {
-      return new PatchListEntry(fileHeader, Collections.<Edit> emptyList());
-    }
-
-    List<Edit> edits = fileHeader.toEditList();
-    if (edits.isEmpty()) {
-      return new PatchListEntry(fileHeader, Collections.<Edit> emptyList());
-    }
-    if (!computeIntraline) {
       return new PatchListEntry(fileHeader, edits);
     }
 
-    switch (fileHeader.getChangeType()) {
-      case ADD:
-      case DELETE:
-        return new PatchListEntry(fileHeader, edits);
-    }
+    private static void combineLineEdits(List<Edit> edits, Text a, Text b) {
+      for (int j = 0; j < edits.size() - 1;) {
+        Edit c = edits.get(j);
+        Edit n = edits.get(j + 1);
 
-    Text aContent = null;
-    Text bContent = null;
+        // Combine edits that are really close together. Right now our rule
+        // is, coalesce two line edits which are only one line apart if that
+        // common context line is either a "pointless line", or is identical
+        // on both sides and starts a new block of code. These are mostly
+        // block reindents to add or remove control flow operators.
+        //
+        final int ad = n.getBeginA() - c.getEndA();
+        final int bd = n.getBeginB() - c.getEndB();
+        if ((1 <= ad && isBlankLineGap(a, c.getEndA(), n.getBeginA()))
+            || (1 <= bd && isBlankLineGap(b, c.getEndB(), n.getBeginB()))
+            || (ad == 1 && bd == 1 && isControlBlockStart(a, c.getEndA()))) {
+          int ab = c.getBeginA();
+          int ae = n.getEndA();
 
-    for (int i = 0; i < edits.size(); i++) {
-      Edit e = edits.get(i);
+          int bb = c.getBeginB();
+          int be = n.getEndB();
 
-      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);
-          combineLineEdits(edits, aContent, bContent);
-          i = -1; // restart the entire scan after combining lines.
+          edits.set(j, new Edit(ab, ae, bb, be));
+          edits.remove(j + 1);
           continue;
         }
 
-        CharText a = new CharText(aContent, e.getBeginA(), e.getEndA());
-        CharText b = new CharText(bContent, e.getBeginB(), e.getEndB());
+        j++;
+      }
+    }
 
-        List<Edit> wordEdits = new MyersDiff(a, b).getEdits();
-
-        // Combine edits that are really close together. If they are
-        // just a few characters apart we tend to get better results
-        // by joining them together and taking the whole span.
-        //
-        for (int j = 0; j < wordEdits.size() - 1;) {
-          Edit c = wordEdits.get(j);
-          Edit n = wordEdits.get(j + 1);
-
-          if (n.getBeginA() - c.getEndA() <= 5
-              || n.getBeginB() - c.getEndB() <= 5) {
-            int ab = c.getBeginA();
-            int ae = n.getEndA();
-
-            int bb = c.getBeginB();
-            int be = n.getEndB();
-
-            if (canCoalesce(a, c.getEndA(), n.getBeginA())
-                && canCoalesce(b, c.getEndB(), n.getBeginB())) {
-              wordEdits.set(j, new Edit(ab, ae, bb, be));
-              wordEdits.remove(j + 1);
-              continue;
-            }
-          }
-
-          j++;
+    private static boolean isBlankLineGap(Text a, int b, int e) {
+      for (; b < e; b++) {
+        if (!BLANK_LINE_RE.matcher(a.getLine(b)).matches()) {
+          return false;
         }
+      }
+      return true;
+    }
 
-        // Apply some simple rules to fix up some of the edits. Our
-        // logic above, along with our per-character difference tends
-        // to produce some crazy stuff.
-        //
-        for (int j = 0; j < wordEdits.size(); j++) {
-          Edit c = wordEdits.get(j);
-          int ab = c.getBeginA();
-          int ae = c.getEndA();
+    private static boolean isControlBlockStart(Text a, int idx) {
+      final String l = a.getLine(idx);
+      return CONTROL_BLOCK_START_RE.matcher(l).find();
+    }
 
-          int bb = c.getBeginB();
-          int be = c.getEndB();
-
-          // Sometimes the diff generator produces an INSERT or DELETE
-          // right up against a REPLACE, but we only find this after
-          // we've also played some shifting games on the prior edit.
-          // If that happened to us, coalesce them together so we can
-          // correct this mess for the user. If we don't we wind up
-          // with silly stuff like "es" -> "es = Addresses".
-          //
-          if (1 < j) {
-            Edit p = wordEdits.get(j - 1);
-            if (p.getEndA() == ab || p.getEndB() == bb) {
-              if (p.getEndA() == ab && p.getBeginA() < p.getEndA()) {
-                ab = p.getBeginA();
-              }
-              if (p.getEndB() == bb && p.getBeginB() < p.getEndB()) {
-                bb = p.getBeginB();
-              }
-              wordEdits.remove(--j);
-            }
-          }
-
-          // We sometimes collapsed an edit together in a strange way,
-          // 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)) {
-            ab++;
-            bb++;
-          }
-          while (ab < ae && bb < be && a.equals(ae - 1, b, be - 1)) {
-            ae--;
-            be--;
-          }
-
-          // The leading part of an edit and its trailing part in the same
-          // text might be identical. Slide down that edit and use the tail
-          // rather than the leading bit. If however the edit is only on a
-          // whitespace block try to shift it to the left margin, assuming
-          // that it is an indentation change.
-          //
-          boolean aShift = true;
-          if (ab < ae && isOnlyWhitespace(a, ab, ae)) {
-            int lf = findLF(wordEdits, j, a, ab);
-            if (lf < ab && a.charAt(lf) == '\n') {
-              int nb = lf + 1;
-              int p = 0;
-              while (p < ae - ab) {
-                if (a.equals(ab + p, a, ab + p))
-                  p++;
-                else
-                  break;
-              }
-              if (p == ae - ab) {
-                ab = nb;
-                ae = nb + p;
-                aShift = false;
-              }
-            }
-          }
-          if (aShift) {
-            while (0 < ab && ab < ae && a.charAt(ab - 1) != '\n'
-                && a.equals(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)) {
-                ab++;
-                ae++;
-                if (a.charAt(ae - 1) == '\n') {
-                  break;
-                }
-              }
-            }
-          }
-
-          boolean bShift = true;
-          if (bb < be && isOnlyWhitespace(b, bb, be)) {
-            int lf = findLF(wordEdits, j, b, bb);
-            if (lf < bb && b.charAt(lf) == '\n') {
-              int nb = lf + 1;
-              int p = 0;
-              while (p < be - bb) {
-                if (b.equals(bb + p, b, bb + p))
-                  p++;
-                else
-                  break;
-              }
-              if (p == be - bb) {
-                bb = nb;
-                be = nb + p;
-                bShift = false;
-              }
-            }
-          }
-          if (bShift) {
-            while (0 < bb && bb < be && b.charAt(bb - 1) != '\n'
-                && b.equals(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)) {
-                bb++;
-                be++;
-                if (b.charAt(be - 1) == '\n') {
-                  break;
-                }
-              }
-            }
-          }
-
-          // If most of a line was modified except the LF was common, make
-          // the LF part of the modification region. This is easier to read.
-          //
-          if (ab < ae //
-              && (ab == 0 || a.charAt(ab - 1) == '\n') //
-              && ae < a.size() && a.charAt(ae) == '\n') {
-            ae++;
-          }
-          if (bb < be //
-              && (bb == 0 || b.charAt(bb - 1) == '\n') //
-              && be < b.size() && b.charAt(be) == '\n') {
-            be++;
-          }
-
-          wordEdits.set(j, new Edit(ab, ae, bb, be));
+    private static boolean canCoalesce(CharText a, int b, int e) {
+      while (b < e) {
+        if (a.charAt(b++) == '\n') {
+          return false;
         }
+      }
+      return true;
+    }
 
-        edits.set(i, new ReplaceEdit(e, wordEdits));
+    private static int findLF(List<Edit> edits, int j, CharText t, int b) {
+      int lf = b;
+      int limit = 0 < j ? edits.get(j - 1).getEndB() : 0;
+      while (limit < lf && t.charAt(lf) != '\n') {
+        lf--;
+      }
+      return lf;
+    }
+
+    private static boolean isOnlyWhitespace(CharText t, final int b, final int e) {
+      for (int c = b; c < e; c++) {
+        if (!Character.isWhitespace(t.charAt(c))) {
+          return false;
+        }
+      }
+      return b < e;
+    }
+
+    private static Text read(Repository repo, String path, RevTree tree)
+        throws IOException {
+      TreeWalk tw = TreeWalk.forPath(repo, 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) {
+        return Text.EMPTY;
+      }
+      return new Text(ldr.getCachedBytes());
+    }
+
+    private static AnyObjectId aFor(final PatchListKey key,
+        final Repository repo, final RevCommit b) throws IOException {
+      if (key.getOldId() != null) {
+        return key.getOldId();
+      }
+
+      switch (b.getParentCount()) {
+        case 0:
+          return emptyTree(repo);
+        case 1:
+          return b.getParent(0);
+        default:
+          // merge commit, return null to force combined diff behavior
+          return null;
       }
     }
 
-    return new PatchListEntry(fileHeader, edits);
-  }
-
-  private static void combineLineEdits(List<Edit> edits, Text a, Text b) {
-    for (int j = 0; j < edits.size() - 1;) {
-      Edit c = edits.get(j);
-      Edit n = edits.get(j + 1);
-
-      // Combine edits that are really close together. Right now our rule
-      // is, coalesce two line edits which are only one line apart if that
-      // common context line is either a "pointless line", or is identical
-      // on both sides and starts a new block of code. These are mostly
-      // block reindents to add or remove control flow operators.
-      //
-      final int ad = n.getBeginA() - c.getEndA();
-      final int bd = n.getBeginB() - c.getEndB();
-      if ((1 <= ad && isBlankLineGap(a, c.getEndA(), n.getBeginA()))
-          || (1 <= bd && isBlankLineGap(b, c.getEndB(), n.getBeginB()))
-          || (ad == 1 && bd == 1 && isControlBlockStart(a, c.getEndA()))) {
-        int ab = c.getBeginA();
-        int ae = n.getEndA();
-
-        int bb = c.getBeginB();
-        int be = n.getEndB();
-
-        edits.set(j, new Edit(ab, ae, bb, be));
-        edits.remove(j + 1);
-        continue;
-      }
-
-      j++;
+    private static ObjectId emptyTree(final Repository repo) throws IOException {
+      return new ObjectWriter(repo).writeCanonicalTree(new byte[0]);
     }
   }
-
-  private static boolean isBlankLineGap(Text a, int b, int e) {
-    for (; b < e; b++) {
-      if (!BLANK_LINE_RE.matcher(a.getLine(b)).matches()) {
-        return false;
-      }
-    }
-    return true;
-  }
-
-  private static boolean isControlBlockStart(Text a, int idx) {
-    final String l = a.getLine(idx);
-    return CONTROL_BLOCK_START_RE.matcher(l).find();
-  }
-
-  private static boolean canCoalesce(CharText a, int b, int e) {
-    while (b < e) {
-      if (a.charAt(b++) == '\n') {
-        return false;
-      }
-    }
-    return true;
-  }
-
-  private static int findLF(List<Edit> edits, int j, CharText t, int b) {
-    int lf = b;
-    int limit = 0 < j ? edits.get(j - 1).getEndB() : 0;
-    while (limit < lf && t.charAt(lf) != '\n') {
-      lf--;
-    }
-    return lf;
-  }
-
-  private static boolean isOnlyWhitespace(CharText t, final int b, final int e) {
-    for (int c = b; c < e; c++) {
-      if (!Character.isWhitespace(t.charAt(c))) {
-        return false;
-      }
-    }
-    return b < e;
-  }
-
-  private static Text read(Repository repo, String path, RevTree tree)
-      throws IOException {
-    TreeWalk tw = TreeWalk.forPath(repo, 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) {
-      return Text.EMPTY;
-    }
-    return new Text(ldr.getCachedBytes());
-  }
-
-  private static AnyObjectId aFor(final PatchListKey key,
-      final Repository repo, final RevCommit b) throws IOException {
-    if (key.getOldId() != null) {
-      return key.getOldId();
-    }
-
-    switch (b.getParentCount()) {
-      case 0:
-        return emptyTree(repo);
-      case 1:
-        return b.getParent(0);
-      default:
-        // merge commit, return null to force combined diff behavior
-        return null;
-    }
-  }
-
-  private static Process exec(final Repository repo, final List<String> args)
-      throws IOException {
-    final String[] argv = args.toArray(new String[args.size()]);
-    return Runtime.getRuntime().exec(argv, null, repo.getDirectory());
-  }
-
-  private static ObjectId emptyTree(final Repository repo) throws IOException {
-    return new ObjectWriter(repo).writeCanonicalTree(new byte[0]);
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
index 35dd571..26ee17b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
@@ -21,8 +21,8 @@
 import static org.eclipse.jgit.lib.ObjectIdSerialization.writeCanBeNull;
 import static org.eclipse.jgit.lib.ObjectIdSerialization.writeNotNull;
 
-import com.google.gerrit.common.data.PatchScriptSettings.Whitespace;
 import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.AccountDiffPreference.Whitespace;
 
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.ObjectId;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PublishComments.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PublishComments.java
index 6fdb06e..f3e2890 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PublishComments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PublishComments.java
@@ -284,7 +284,6 @@
       cm.setPatchSet(patchSet, patchSetInfoFactory.get(patchSetId));
       cm.setChangeMessage(message);
       cm.setPatchLineComments(drafts);
-      cm.setReviewDb(db);
       cm.send();
     } catch (EmailException e) {
       log.error("Cannot send comments by email for patch set " + patchSetId, e);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
index 0ee2c9e..c9435d3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.common.data.ApprovalType;
 import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.reviewdb.ApprovalCategory;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.PatchSet;
 import com.google.gerrit.reviewdb.PatchSetApproval;
@@ -24,7 +25,6 @@
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.workflow.CategoryFunction;
 import com.google.gerrit.server.workflow.FunctionState;
 import com.google.gwtorm.client.OrmException;
@@ -37,6 +37,25 @@
 
 /** Access control management for a user accessing a single change. */
 public class ChangeControl {
+  public static class GenericFactory {
+    private final ProjectControl.GenericFactory projectControl;
+
+    @Inject
+    GenericFactory(ProjectControl.GenericFactory p) {
+      projectControl = p;
+    }
+
+    public ChangeControl controlFor(Change change, CurrentUser user)
+        throws NoSuchChangeException {
+      final Project.NameKey projectKey = change.getProject();
+      try {
+        return projectControl.controlFor(projectKey, user).controlFor(change);
+      } catch (NoSuchProjectException e) {
+        throw new NoSuchChangeException(change.getId(), e);
+      }
+    }
+  }
+
   public static class Factory {
     private final ProjectControl.Factory projectControl;
     private final Provider<ReviewDb> db;
@@ -140,6 +159,10 @@
     ;
   }
 
+  public short normalize(ApprovalCategory.Id category, short score) {
+    return getRefControl().normalize(category, score);
+  }
+
   /** Can this user add a patch set to this change? */
   public boolean canAddPatchSet() {
     return getRefControl().canUpload();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index 740a2a6..48eef87 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -19,9 +19,7 @@
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.SelfPopulatingCache;
-import com.google.gerrit.server.config.WildProjectName;
-import com.google.gwtorm.client.OrmException;
+import com.google.gerrit.server.cache.EntryCreator;
 import com.google.gwtorm.client.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Module;
@@ -29,7 +27,6 @@
 import com.google.inject.TypeLiteral;
 import com.google.inject.name.Named;
 
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 
@@ -43,59 +40,20 @@
       @Override
       protected void configure() {
         final TypeLiteral<Cache<Project.NameKey, ProjectState>> type =
-          new TypeLiteral<Cache<Project.NameKey, ProjectState>>() {};
-        core(type, CACHE_NAME);
+            new TypeLiteral<Cache<Project.NameKey, ProjectState>>() {};
+        core(type, CACHE_NAME).populateWith(Loader.class);
         bind(ProjectCacheImpl.class);
         bind(ProjectCache.class).to(ProjectCacheImpl.class);
       }
     };
   }
 
-  private final ProjectState.Factory projectStateFactory;
-  private final SchemaFactory<ReviewDb> schema;
-  private final SelfPopulatingCache<Project.NameKey, ProjectState> byName;
+  private final Cache<Project.NameKey, ProjectState> byName;
 
   @Inject
-  ProjectCacheImpl(final ProjectState.Factory psf,
-      final SchemaFactory<ReviewDb> sf,
-      @WildProjectName final Project.NameKey wp,
+  ProjectCacheImpl(
       @Named(CACHE_NAME) final Cache<Project.NameKey, ProjectState> byName) {
-    projectStateFactory = psf;
-    schema = sf;
-
-    this.byName =
-        new SelfPopulatingCache<Project.NameKey, ProjectState>(byName) {
-          @Override
-          public ProjectState createEntry(final Project.NameKey key)
-              throws Exception {
-            return lookup(key);
-          }
-        };
-  }
-
-  /**
-   * Lookup for a state of a specified project on database
-   *
-   * @param key the project name key
-   * @return the project state
-   * @throws OrmException
-   */
-  private ProjectState lookup(final Project.NameKey key) throws OrmException {
-    final ReviewDb db = schema.open();
-    try {
-      final Project p = db.projects().get(key);
-      if (p == null) {
-        return null;
-      }
-
-      final Collection<RefRight> rights =
-          Collections.unmodifiableCollection(db.refRights().byProject(
-              p.getNameKey()).toList());
-
-      return projectStateFactory.create(p, rights);
-    } finally {
-      db.close();
-    }
+    this.byName = byName;
   }
 
   /**
@@ -119,4 +77,34 @@
   public void evictAll() {
     byName.removeAll();
   }
+
+  static class Loader extends EntryCreator<Project.NameKey, ProjectState> {
+    private final ProjectState.Factory projectStateFactory;
+    private final SchemaFactory<ReviewDb> schema;
+
+    @Inject
+    Loader(ProjectState.Factory psf, SchemaFactory<ReviewDb> sf) {
+      projectStateFactory = psf;
+      schema = sf;
+    }
+
+    @Override
+    public ProjectState createEntry(Project.NameKey key) throws Exception {
+      final ReviewDb db = schema.open();
+      try {
+        final Project p = db.projects().get(key);
+        if (p == null) {
+          return null;
+        }
+
+        final Collection<RefRight> rights =
+            Collections.unmodifiableCollection(db.refRights().byProject(
+                p.getNameKey()).toList());
+
+        return projectStateFactory.create(p, rights);
+      } finally {
+        db.close();
+      }
+    }
+  }
 }
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 24fe3f0..67f543b 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
@@ -33,6 +33,24 @@
   public static final int VISIBLE = 1 << 0;
   public static final int OWNER = 1 << 1;
 
+  public static class GenericFactory {
+    private final ProjectCache projectCache;
+
+    @Inject
+    GenericFactory(final ProjectCache pc) {
+      projectCache = pc;
+    }
+
+    public ProjectControl controlFor(Project.NameKey nameKey, CurrentUser user)
+        throws NoSuchProjectException {
+      final ProjectState p = projectCache.get(nameKey);
+      if (p == null) {
+        throw new NoSuchProjectException(nameKey);
+      }
+      return p.controlFor(user);
+    }
+  }
+
   public static class Factory {
     private final ProjectCache projectCache;
     private final Provider<CurrentUser> user;
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 32fa01c..66ea6e3 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
@@ -28,12 +28,16 @@
 import static com.google.gerrit.reviewdb.ApprovalCategory.PUSH_TAG_SIGNED;
 import static com.google.gerrit.reviewdb.ApprovalCategory.READ;
 
+import com.google.gerrit.common.data.ParamertizedString;
 import com.google.gerrit.reviewdb.AccountGroup;
 import com.google.gerrit.reviewdb.ApprovalCategory;
 import com.google.gerrit.reviewdb.RefRight;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 
+import dk.brics.automaton.RegExp;
+
+import org.apache.commons.lang.StringUtils;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevObject;
@@ -45,10 +49,13 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.SortedMap;
 import java.util.TreeMap;
+import java.util.regex.Pattern;
 
 
 /** Manages access control for Git references (aka branches, tags). */
@@ -59,9 +66,17 @@
   private Boolean canForgeAuthor;
   private Boolean canForgeCommitter;
 
-  RefControl(final ProjectControl projectControl, final String refName) {
+  RefControl(final ProjectControl projectControl, String ref) {
+    if (isRE(ref)) {
+      ref = shortestExample(ref);
+
+    } else if (ref.endsWith("/*")) {
+      ref = ref.substring(0, ref.length() - 1);
+
+    }
+
     this.projectControl = projectControl;
-    this.refName = refName;
+    this.refName = ref;
   }
 
   public String getRefName() {
@@ -94,11 +109,12 @@
     // calls us to find out if there is ownership of all references in
     // order to determine project level ownership.
     //
-    if (!RefRight.ALL.equals(getRefName()) && getProjectControl().isOwner()) {
-      return true;
+    if (getRefName().equals(
+        RefRight.ALL.substring(0, RefRight.ALL.length() - 1))) {
+      return getCurrentUser().isAdministrator();
+    } else {
+      return getProjectControl().isOwner();
     }
-
-    return false;
   }
 
   /** Can this user see this reference exists? */
@@ -233,6 +249,24 @@
     return canPerform(FORGE_IDENTITY, FORGE_SERVER);
   }
 
+  public short normalize(ApprovalCategory.Id category, short score) {
+    short minAllowed = 0, maxAllowed = 0;
+    for (RefRight r : getApplicableRights(category)) {
+      if (getCurrentUser().getEffectiveGroups().contains(r.getAccountGroupId())) {
+        minAllowed = (short) Math.min(minAllowed, r.getMinValue());
+        maxAllowed = (short) Math.max(maxAllowed, r.getMaxValue());
+      }
+    }
+
+    if (score < minAllowed) {
+      score = minAllowed;
+    }
+    if (score > maxAllowed) {
+      score = maxAllowed;
+    }
+    return score;
+  }
+
   /**
    * Convenience holder class used to map a ref pattern to the list of
    * {@code RefRight}s that use it in the database.
@@ -302,19 +336,95 @@
     return val >= level;
   }
 
-  public static final Comparator<String> DESCENDING_SORT =
+  /**
+   * Order the Ref Pattern by the most specific. This sort is done by:
+   * <ul>
+   * <li>1 - The minor value of Levenshtein string distance between the branch
+   * name and the regex string shortest example. A shorter distance is a more
+   * specific match.
+   * <li>2 - Finites first, infinities after.
+   * <li>3 - Number of transitions.
+   * <li>4 - Length of the expression text.
+   * </ul>
+   *
+   * Levenshtein distance is a measure of the similarity between two strings.
+   * The distance is the number of deletions, insertions, or substitutions
+   * required to transform one string into another.
+   *
+   * For example, if given refs/heads/m* and refs/heads/*, the distances are 5
+   * and 6. It means that refs/heads/m* is more specific because it's closer to
+   * refs/heads/master than refs/heads/*.
+   *
+   * Another example could be refs/heads/* and refs/heads/[a-zA-Z]*, the
+   * distances are both 6. Both are infinite, but refs/heads/[a-zA-Z]* has more
+   * transitions, which after all turns it more specific.
+   */
+  private final Comparator<String> BY_MOST_SPECIFIC_SORT =
       new Comparator<String>() {
+        public int compare(final String pattern1, final String pattern2) {
+          int cmp = distance(pattern2) - distance(pattern1);
+          if (cmp == 0) {
+            boolean p1_finite = finite(pattern1);
+            boolean p2_finite = finite(pattern2);
 
-    @Override
-    public int compare(String a, String b) {
-      int aLength = a.length();
-      int bLength = b.length();
-      if (bLength == aLength) {
-        return a.compareTo(b);
-      }
-      return bLength - aLength;
-    }
-  };
+            if (p1_finite && !p2_finite) {
+              cmp = -1;
+            } else if (!p1_finite && p2_finite) {
+              cmp = 1;
+            } else /* if (f1 == f2) */{
+              cmp = 0;
+            }
+          }
+          if (cmp == 0) {
+            cmp = transitions(pattern1) - transitions(pattern2);
+          }
+          if (cmp == 0) {
+            cmp = pattern2.length() - pattern1.length();
+          }
+          return cmp;
+        }
+
+        private int distance(String pattern) {
+          String example;
+          if (isRE(pattern)) {
+            example = shortestExample(pattern);
+
+          } else if (pattern.endsWith("/*")) {
+            example = pattern.substring(0, pattern.length() - 1) + '1';
+
+          } else if (pattern.equals(getRefName())) {
+            return 0;
+
+          } else {
+            return Math.max(pattern.length(), getRefName().length());
+          }
+          return StringUtils.getLevenshteinDistance(example, getRefName());
+        }
+
+        private boolean finite(String pattern) {
+          if (isRE(pattern)) {
+            return toRegExp(pattern).toAutomaton().isFinite();
+
+          } else if (pattern.endsWith("/*")) {
+            return false;
+
+          } else {
+            return true;
+          }
+        }
+
+        private int transitions(String pattern) {
+          if (isRE(pattern)) {
+            return toRegExp(pattern).toAutomaton().getNumberOfTransitions();
+
+          } else if (pattern.endsWith("/*")) {
+            return pattern.length();
+
+          } else {
+            return pattern.length();
+          }
+        }
+      };
 
   /**
    * Sorts all given rights into a map, ordered by descending length of
@@ -338,10 +448,10 @@
    * @param actionRights
    * @return A sorted map keyed off the ref pattern of all rights.
    */
-  private static SortedMap<String, RefRightsForPattern> sortedRightsByPattern(
+  private SortedMap<String, RefRightsForPattern> sortedRightsByPattern(
       List<RefRight> actionRights) {
     SortedMap<String, RefRightsForPattern> rights =
-      new TreeMap<String, RefRightsForPattern>(DESCENDING_SORT);
+      new TreeMap<String, RefRightsForPattern>(BY_MOST_SPECIFIC_SORT);
     for (RefRight actionRight : actionRights) {
       RefRightsForPattern patternRights =
         rights.get(actionRight.getRefPattern());
@@ -391,7 +501,7 @@
   private List<RefRight> filter(Collection<RefRight> all) {
     List<RefRight> mine = new ArrayList<RefRight>(all.size());
     for (RefRight right : all) {
-      if (matches(getRefName(), right.getRefPattern())) {
+      if (matches(right.getRefPattern())) {
         mine.add(right);
       }
     }
@@ -402,13 +512,70 @@
     return projectControl.getProjectState();
   }
 
-  public static boolean matches(String refName, String refPattern) {
-    if (refPattern.endsWith("/*")) {
+  private boolean matches(String refPattern) {
+    if (isTemplate(refPattern)) {
+      ParamertizedString template = new ParamertizedString(refPattern);
+      HashMap<String, String> p = new HashMap<String, String>();
+
+      if (getCurrentUser() instanceof IdentifiedUser) {
+        p.put("username", ((IdentifiedUser) getCurrentUser()).getUserName());
+      } else {
+        // Right now we only template the username. If not available
+        // this rule cannot be matched at all.
+        //
+        return false;
+      }
+
+      if (isRE(refPattern)) {
+        for (Map.Entry<String, String> ent : p.entrySet()) {
+          ent.setValue(escape(ent.getValue()));
+        }
+      }
+
+      refPattern = template.replace(p);
+    }
+
+    if (isRE(refPattern)) {
+      return Pattern.matches(refPattern, getRefName());
+
+    } else if (refPattern.endsWith("/*")) {
       String prefix = refPattern.substring(0, refPattern.length() - 1);
-      return refName.startsWith(prefix);
+      return getRefName().startsWith(prefix);
 
     } else {
-      return refName.equals(refPattern);
+      return getRefName().equals(refPattern);
     }
   }
+
+  private static boolean isTemplate(String refPattern) {
+    return 0 <= refPattern.indexOf("${");
+  }
+
+  private static String escape(String value) {
+    // Right now the only special character allowed in a
+    // variable value is a . in the username.
+    //
+    return value.replace(".", "\\.");
+  }
+
+  private static boolean isRE(String refPattern) {
+    return refPattern.startsWith(RefRight.REGEX_PREFIX);
+  }
+
+  public static String shortestExample(String pattern) {
+    if (isRE(pattern)) {
+      return toRegExp(pattern).toAutomaton().getShortestExample(true);
+    } else if (pattern.endsWith("/*")) {
+      return pattern.substring(0, pattern.length() - 1) + '1';
+    } else {
+      return pattern;
+    }
+  }
+
+  private static RegExp toRegExp(String refPattern) {
+    if (isRE(refPattern)) {
+      refPattern = refPattern.substring(1);
+    }
+    return new RegExp(refPattern);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java
index 5e95c18..88ea4d3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.query;
 
+import com.google.gwtorm.client.OrmException;
+
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -21,64 +23,90 @@
 import java.util.List;
 
 /** Requires all predicates to be true. */
-public final class AndPredicate extends Predicate {
-  private final Predicate[] children;
+public class AndPredicate<T> extends Predicate<T> {
+  private final List<Predicate<T>> children;
+  private final int cost;
 
-  public AndPredicate(final Predicate... that) {
+  protected AndPredicate(final Predicate<T>... that) {
     this(Arrays.asList(that));
   }
 
-  public AndPredicate(final Collection<Predicate> that) {
-    final ArrayList<Predicate> tmp = new ArrayList<Predicate>(that.size());
-    for (Predicate p : that) {
-      if (p instanceof AndPredicate) {
-        tmp.addAll(p.getChildren());
+  protected AndPredicate(final Collection<? extends Predicate<T>> that) {
+    final ArrayList<Predicate<T>> t = new ArrayList<Predicate<T>>(that.size());
+    int c = 0;
+    for (Predicate<T> p : that) {
+      if (getClass() == p.getClass()) {
+        for (Predicate<T> gp : p.getChildren()) {
+          t.add(gp);
+          c += gp.getCost();
+        }
       } else {
-        tmp.add(p);
+        t.add(p);
+        c += p.getCost();
       }
     }
-    if (tmp.size() < 2) {
+    if (t.size() < 2) {
       throw new IllegalArgumentException("Need at least two predicates");
     }
-    children = new Predicate[tmp.size()];
-    tmp.toArray(children);
+    children = t;
+    cost = c;
   }
 
   @Override
-  public List<Predicate> getChildren() {
-    return Collections.unmodifiableList(Arrays.asList(children));
+  public final List<Predicate<T>> getChildren() {
+    return Collections.unmodifiableList(children);
   }
 
   @Override
-  public int getChildCount() {
-    return children.length;
+  public final int getChildCount() {
+    return children.size();
   }
 
   @Override
-  public Predicate getChild(final int i) {
-    return children[i];
+  public final Predicate<T> getChild(final int i) {
+    return children.get(i);
+  }
+
+  @Override
+  public Predicate<T> copy(final Collection<? extends Predicate<T>> children) {
+    return new AndPredicate<T>(children);
+  }
+
+  @Override
+  public boolean match(final T object) throws OrmException {
+    for (final Predicate<T> c : children) {
+      if (!c.match(object)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  @Override
+  public int getCost() {
+    return cost;
   }
 
   @Override
   public int hashCode() {
-    return children[0].hashCode() * 31 + children[1].hashCode();
+    return getChild(0).hashCode() * 31 + getChild(1).hashCode();
   }
 
   @Override
   public boolean equals(final Object other) {
-    return other instanceof AndPredicate
-        && getChildren().equals(((AndPredicate) other).getChildren());
+    return getClass() == other.getClass()
+        && getChildren().equals(((Predicate<?>) other).getChildren());
   }
 
   @Override
-  public String toString() {
+  public final String toString() {
     final StringBuilder r = new StringBuilder();
     r.append("(");
-    for (int i = 0; i < children.length; i++) {
+    for (int i = 0; i < getChildCount(); i++) {
       if (i != 0) {
         r.append(" ");
       }
-      r.append(children[i]);
+      r.append(getChild(i));
     }
     r.append(")");
     return r.toString();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/ChangeQueryBuilder.java
deleted file mode 100644
index b12b56d..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/ChangeQueryBuilder.java
+++ /dev/null
@@ -1,79 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.query;
-
-import com.google.gerrit.reviewdb.RevId;
-import com.google.inject.Singleton;
-
-import org.eclipse.jgit.lib.AbbreviatedObjectId;
-
-/**
- * Parses a query string meant to be applied to change objects.
- * <p>
- * This class is thread-safe, and may be reused across threads to parse queries.
- */
-@Singleton
-public class ChangeQueryBuilder extends QueryBuilder {
-  public static final String FIELD_CHANGE = "change";
-  public static final String FIELD_COMMIT = "commit";
-  public static final String FIELD_REVIEWER = "reviewer";
-  public static final String FIELD_OWNER = "owner";
-
-  private static final String CHANGE_RE = "^[1-9][0-9]*$";
-  private static final String COMMIT_RE =
-      "^([0-9a-fA-F]{4," + RevId.LEN + "})$";
-
-  @Operator
-  public Predicate change(final String value) {
-    match(value, CHANGE_RE);
-    return new OperatorPredicate(FIELD_CHANGE, value);
-  }
-
-  @Operator
-  public Predicate commit(final String value) {
-    final AbbreviatedObjectId id = AbbreviatedObjectId.fromString(value);
-    return new ObjectIdPredicate(FIELD_COMMIT, id);
-  }
-
-  @Operator
-  public Predicate owner(final String value) {
-    return new OperatorPredicate(FIELD_OWNER, value);
-  }
-
-  @Operator
-  public Predicate reviewer(final String value) {
-    return new OperatorPredicate(FIELD_REVIEWER, value);
-  }
-
-  @Override
-  protected Predicate defaultField(final String value)
-      throws QueryParseException {
-    if (value.matches(CHANGE_RE)) {
-      return change(value);
-
-    } else if (value.matches(COMMIT_RE)) {
-      return commit(value);
-
-    } else {
-      throw error("Unsupported query:" + value);
-    }
-  }
-
-  private static void match(String val, String re) {
-    if (!val.matches(re)) {
-      throw new IllegalArgumentException("Invalid value :" + val);
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/IntPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/IntPredicate.java
new file mode 100644
index 0000000..b1806b4
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/IntPredicate.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query;
+
+/** Predicate to filter a field by matching integer value. */
+public abstract class IntPredicate<T> extends OperatorPredicate<T> {
+  private final int value;
+
+  public IntPredicate(final String name, final String value) {
+    super(name, value);
+    this.value = Integer.parseInt(value);
+  }
+
+  public IntPredicate(final String name, final int value) {
+    super(name, String.valueOf(value));
+    this.value = value;
+  }
+
+  public int intValue() {
+    return value;
+  }
+
+  @Override
+  public int hashCode() {
+    return getOperator().hashCode() * 31 + value;
+  }
+
+  @Override
+  public boolean equals(final Object other) {
+    if (getClass() == other.getClass()) {
+      final IntPredicate<?> p = (IntPredicate<?>) other;
+      return getOperator().equals(p.getOperator())
+          && intValue() == p.intValue();
+    }
+    return false;
+  }
+
+  @Override
+  public String toString() {
+    return getOperator() + ":" + getValue();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/NotPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/NotPredicate.java
index ddb03a6..9e651a3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/NotPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/NotPredicate.java
@@ -14,25 +14,57 @@
 
 package com.google.gerrit.server.query;
 
+import com.google.gwtorm.client.OrmException;
+
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 
 /** Negates the result of another predicate. */
-public final class NotPredicate extends Predicate {
-  private final Predicate that;
+public class NotPredicate<T> extends Predicate<T> {
+  private final Predicate<T> that;
 
-  public NotPredicate(final Predicate that) {
+  protected NotPredicate(final Predicate<T> that) {
+    if (that instanceof NotPredicate) {
+      throw new IllegalArgumentException("Double negation unsupported");
+    }
     this.that = that;
   }
 
   @Override
-  public Predicate not() {
+  public final List<Predicate<T>> getChildren() {
+    return Collections.singletonList(that);
+  }
+
+  @Override
+  public final int getChildCount() {
+    return 1;
+  }
+
+  @Override
+  public final Predicate<T> getChild(final int i) {
+    if (i != 0) {
+      throw new ArrayIndexOutOfBoundsException(i);
+    }
     return that;
   }
 
   @Override
-  public List<Predicate> getChildren() {
-    return Collections.singletonList(that);
+  public Predicate<T> copy(final Collection<? extends Predicate<T>> children) {
+    if (children.size() != 1) {
+      throw new IllegalArgumentException("Expected exactly one child");
+    }
+    return new NotPredicate<T>(children.iterator().next());
+  }
+
+  @Override
+  public boolean match(final T object) throws OrmException {
+    return !that.match(object);
+  }
+
+  @Override
+  public int getCost() {
+    return that.getCost();
   }
 
   @Override
@@ -42,12 +74,12 @@
 
   @Override
   public boolean equals(final Object other) {
-    return other instanceof NotPredicate
-        && getChildren().equals(((Predicate) other).getChildren());
+    return getClass() == other.getClass()
+        && getChildren().equals(((Predicate<?>) other).getChildren());
   }
 
   @Override
-  public String toString() {
+  public final String toString() {
     return "-" + that.toString();
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/ObjectIdPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/ObjectIdPredicate.java
index bd9eeea..0d68fea 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/ObjectIdPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/ObjectIdPredicate.java
@@ -14,12 +14,14 @@
 
 package com.google.gerrit.server.query;
 
+import com.google.gerrit.server.query.OperatorPredicate;
+
 import org.eclipse.jgit.lib.AbbreviatedObjectId;
 import org.eclipse.jgit.lib.ObjectId;
 
 
 /** Predicate for a field of {@link ObjectId}. */
-public final class ObjectIdPredicate extends OperatorPredicate {
+public abstract class ObjectIdPredicate<T> extends OperatorPredicate<T> {
   private final AbbreviatedObjectId id;
 
   public ObjectIdPredicate(final String name, final AbbreviatedObjectId id) {
@@ -47,7 +49,7 @@
   @Override
   public boolean equals(Object other) {
     if (other instanceof ObjectIdPredicate) {
-      final ObjectIdPredicate p = (ObjectIdPredicate) other;
+      final ObjectIdPredicate<?> p = (ObjectIdPredicate<?>) other;
       return getOperator().equals(p.getOperator()) && id.equals(p.id);
     }
     return false;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/OperatorPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/OperatorPredicate.java
index fbd6af1..4c6e203 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/OperatorPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/OperatorPredicate.java
@@ -14,9 +14,11 @@
 
 package com.google.gerrit.server.query;
 
+import java.util.Collection;
+
 
 /** Predicate to filter a field by matching value. */
-public class OperatorPredicate extends Predicate {
+public abstract class OperatorPredicate<T> extends Predicate<T> {
   private final String name;
   private final String value;
 
@@ -34,6 +36,14 @@
   }
 
   @Override
+  public Predicate<T> copy(final Collection<? extends Predicate<T>> children) {
+    if (!children.isEmpty()) {
+      throw new IllegalArgumentException("Expected 0 children");
+    }
+    return this;
+  }
+
+  @Override
   public int hashCode() {
     return getOperator().hashCode() * 31 + getValue().hashCode();
   }
@@ -41,7 +51,7 @@
   @Override
   public boolean equals(final Object other) {
     if (getClass() == other.getClass()) {
-      final OperatorPredicate p = (OperatorPredicate) other;
+      final OperatorPredicate<?> p = (OperatorPredicate<?>) other;
       return getOperator().equals(p.getOperator())
           && getValue().equals(p.getValue());
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/OrPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/OrPredicate.java
index a8b8d2b..08f50f4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/OrPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/OrPredicate.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.query;
 
+import com.google.gwtorm.client.OrmException;
+
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -21,64 +23,90 @@
 import java.util.List;
 
 /** Requires one predicate to be true. */
-public final class OrPredicate extends Predicate {
-  private final Predicate[] children;
+public class OrPredicate<T> extends Predicate<T> {
+  private final List<Predicate<T>> children;
+  private final int cost;
 
-  public OrPredicate(final Predicate... that) {
+  protected OrPredicate(final Predicate<T>... that) {
     this(Arrays.asList(that));
   }
 
-  public OrPredicate(final Collection<Predicate> that) {
-    final ArrayList<Predicate> tmp = new ArrayList<Predicate>(that.size());
-    for (Predicate p : that) {
-      if (p instanceof OrPredicate) {
-        tmp.addAll(p.getChildren());
+  protected OrPredicate(final Collection<? extends Predicate<T>> that) {
+    final ArrayList<Predicate<T>> t = new ArrayList<Predicate<T>>(that.size());
+    int c = 0;
+    for (Predicate<T> p : that) {
+      if (getClass() == p.getClass()) {
+        for (Predicate<T> gp : p.getChildren()) {
+          t.add(gp);
+          c += gp.getCost();
+        }
       } else {
-        tmp.add(p);
+        t.add(p);
+        c += p.getCost();
       }
     }
-    if (tmp.size() < 2) {
+    if (t.size() < 2) {
       throw new IllegalArgumentException("Need at least two predicates");
     }
-    children = new Predicate[tmp.size()];
-    tmp.toArray(children);
+    children = t;
+    cost = c;
   }
 
   @Override
-  public List<Predicate> getChildren() {
-    return Collections.unmodifiableList(Arrays.asList(children));
+  public final List<Predicate<T>> getChildren() {
+    return Collections.unmodifiableList(children);
   }
 
   @Override
-  public int getChildCount() {
-    return children.length;
+  public final int getChildCount() {
+    return children.size();
   }
 
   @Override
-  public Predicate getChild(final int i) {
-    return children[i];
+  public final Predicate<T> getChild(final int i) {
+    return children.get(i);
+  }
+
+  @Override
+  public Predicate<T> copy(final Collection<? extends Predicate<T>> children) {
+    return new OrPredicate<T>(children);
+  }
+
+  @Override
+  public boolean match(final T object) throws OrmException {
+    for (final Predicate<T> c : children) {
+      if (c.match(object)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public int getCost() {
+    return cost;
   }
 
   @Override
   public int hashCode() {
-    return children[0].hashCode() * 31 + children[1].hashCode();
+    return getChild(0).hashCode() * 31 + getChild(1).hashCode();
   }
 
   @Override
   public boolean equals(final Object other) {
-    return other instanceof OrPredicate
-        && getChildren().equals(((OrPredicate) other).getChildren());
+    return getClass() == other.getClass()
+        && getChildren().equals(((Predicate<?>) other).getChildren());
   }
 
   @Override
-  public String toString() {
+  public final String toString() {
     final StringBuilder r = new StringBuilder();
     r.append("(");
-    for (int i = 0; i < children.length; i++) {
+    for (int i = 0; i < getChildCount(); i++) {
       if (i != 0) {
         r.append(" OR ");
       }
-      r.append(children[i]);
+      r.append(getChild(i));
     }
     r.append(")");
     return r.toString();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java
index 70da79d..2455cbd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.query;
 
+import com.google.gwtorm.client.OrmException;
+
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
@@ -21,44 +23,60 @@
 /**
  * An abstract predicate tree for any form of query.
  * <p>
- * Implementations should be immutable, and therefore also be thread-safe. They
- * also should ensure their immutable promise by defensively copying any
- * structures which might be modified externally, but were passed into the
- * object's constructor.
+ * Implementations should be immutable, such that the meaning of a predicate
+ * never changes once constructed. They should ensure their immutable promise by
+ * defensively copying any structures which might be modified externally, but
+ * was passed into the object's constructor.
+ * <p>
+ * However, implementations <i>may</i> retain non-thread-safe caches internally,
+ * to speed up evaluation operations within the context of one thread's
+ * evaluation of the predicate. As a result, callers should assume predicates
+ * are not thread-safe, but that two predicate graphs produce the same results
+ * given the same inputs if they are {@link #equals(Object)}.
  * <p>
  * Predicates should support deep inspection whenever possible, so that generic
  * algorithms can be written to operate against them. Predicates which contain
  * other predicates should override {@link #getChildren()} to return the list of
  * children nested within the predicate.
+ *
+ * @type <T> type of object the predicate can evaluate in memory.
  */
-public abstract class Predicate {
+public abstract class Predicate<T> {
   /** Combine the passed predicates into a single AND node. */
-  public static Predicate and(final Predicate... that) {
-    return new AndPredicate(that);
+  public static <T> Predicate<T> and(final Predicate<T>... that) {
+    return new AndPredicate<T>(that);
   }
 
   /** Combine the passed predicates into a single AND node. */
-  public static Predicate and(final Collection<Predicate> that) {
-    return new AndPredicate(that);
+  public static <T> Predicate<T> and(
+      final Collection<? extends Predicate<T>> that) {
+    return new AndPredicate<T>(that);
   }
 
   /** Combine the passed predicates into a single OR node. */
-  public static Predicate or(final Predicate... that) {
-    return new OrPredicate(that);
+  public static <T> Predicate<T> or(final Predicate<T>... that) {
+    return new OrPredicate<T>(that);
   }
 
   /** Combine the passed predicates into a single OR node. */
-  public static Predicate or(final Collection<Predicate> that) {
-    return new OrPredicate(that);
+  public static <T> Predicate<T> or(
+      final Collection<? extends Predicate<T>> that) {
+    return new OrPredicate<T>(that);
   }
 
-  /** Invert the passed node; same as {@code that.not()}. */
-  public static Predicate not(final Predicate that) {
-    return that.not();
+  /** Invert the passed node. */
+  @SuppressWarnings("unchecked")
+  public static <T> Predicate<T> not(final Predicate<T> that) {
+    if (that instanceof NotPredicate) {
+      // Negate of a negate is the original predicate.
+      //
+      return that.getChild(0);
+    }
+    return new NotPredicate<T>(that);
   }
 
   /** Get the children of this predicate, if any. */
-  public List<Predicate> getChildren() {
+  public List<Predicate<T>> getChildren() {
     return Collections.emptyList();
   }
 
@@ -68,14 +86,22 @@
   }
 
   /** Same as {@code getChildren().get(i)} */
-  public Predicate getChild(final int i) {
+  public Predicate<T> getChild(final int i) {
     return getChildren().get(i);
   }
 
-  /** Obtain the inverse of this predicate. */
-  public Predicate not() {
-    return new NotPredicate(this);
-  }
+  /** Create a copy of this predicate, with new children. */
+  public abstract Predicate<T> copy(Collection<? extends Predicate<T>> children);
+
+  /**
+   * Does this predicate match this object?
+   *
+   * @throws OrmException
+   */
+  public abstract boolean match(T object) throws OrmException;
+
+  /** @return a cost estimate to run this predicate, higher figures cost more. */
+  public abstract int getCost();
 
   @Override
   public abstract int hashCode();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java
index da967f4..a0aeb58 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java
@@ -24,6 +24,7 @@
 import static com.google.gerrit.server.query.QueryParser.NOT;
 import static com.google.gerrit.server.query.QueryParser.OR;
 import static com.google.gerrit.server.query.QueryParser.SINGLE_WORD;
+import static com.google.gerrit.server.query.QueryParser.VARIABLE_ASSIGN;
 
 import org.antlr.runtime.tree.Tree;
 
@@ -34,15 +35,14 @@
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 /**
  * Base class to support writing parsers for query languages.
  * <p>
- * This class is thread-safe, and may be reused across threads to parse queries,
- * so implementations of this class should also strive to be thread-safe.
- * <p>
  * Subclasses may document their supported query operators by declaring public
  * methods that perform the query conversion into a {@link Predicate}. For
  * example, to support "is:starred", "is:unread", and nothing else, a subclass
@@ -70,33 +70,53 @@
  * <p>
  * Subclasses may also declare a handler for values which appear without
  * operator by overriding {@link #defaultField(String)}.
+ *
+ * @param <T> type of object the predicates can evaluate in memory.
  */
-public abstract class QueryBuilder {
-  private final Map<String, OperatorFactory> opFactories =
-      new HashMap<String, OperatorFactory>();
+public abstract class QueryBuilder<T> {
+  /**
+   * Defines the operators known by a QueryBuilder.
+   *
+   * This class is thread-safe and may be reused or cached.
+   *
+   * @param <T> type of object the predicates can evaluate in memory.
+   * @param <Q> type of the query builder subclass.
+   */
+  public static class Definition<T, Q extends QueryBuilder<T>> {
+    private final Map<String, OperatorFactory<T, Q>> opFactories =
+        new HashMap<String, OperatorFactory<T, Q>>();
 
-  protected QueryBuilder() {
-    // Guess at the supported operators by scanning methods.
-    //
-    Class<?> c = getClass();
-    while (c != QueryBuilder.class) {
-      for (final Method method : c.getDeclaredMethods()) {
-        if (method.getAnnotation(Operator.class) != null
-            && Predicate.class.isAssignableFrom(method.getReturnType())
-            && method.getParameterTypes().length == 1
-            && method.getParameterTypes()[0] == String.class
-            && (method.getModifiers() & Modifier.ABSTRACT) == 0
-            && (method.getModifiers() & Modifier.PUBLIC) == Modifier.PUBLIC) {
-          final String name = method.getName().toLowerCase();
-          if (!opFactories.containsKey(name)) {
-            opFactories.put(name, new ReflectionFactory(name, method));
+    public Definition(Class<Q> clazz) {
+      // Guess at the supported operators by scanning methods.
+      //
+      Class<?> c = clazz;
+      while (c != QueryBuilder.class) {
+        for (final Method method : c.getDeclaredMethods()) {
+          if (method.getAnnotation(Operator.class) != null
+              && Predicate.class.isAssignableFrom(method.getReturnType())
+              && method.getParameterTypes().length == 1
+              && method.getParameterTypes()[0] == String.class
+              && (method.getModifiers() & Modifier.ABSTRACT) == 0
+              && (method.getModifiers() & Modifier.PUBLIC) == Modifier.PUBLIC) {
+            final String name = method.getName().toLowerCase();
+            if (!opFactories.containsKey(name)) {
+              opFactories.put(name, new ReflectionFactory<T, Q>(name, method));
+            }
           }
         }
+        c = c.getSuperclass();
       }
-      c = c.getSuperclass();
     }
   }
 
+  @SuppressWarnings("unchecked")
+  private final Map<String, OperatorFactory> opFactories;
+
+  @SuppressWarnings("unchecked")
+  protected QueryBuilder(Definition<T, ? extends QueryBuilder<T>> def) {
+    opFactories = (Map) def.opFactories;
+  }
+
   /**
    * Parse a user supplied query string into a predicate.
    *
@@ -107,11 +127,11 @@
    *         due to an operator not being supported, or due to an invalid value
    *         being passed to a recognized operator.
    */
-  public Predicate parse(final String query) throws QueryParseException {
+  public Predicate<T> parse(final String query) throws QueryParseException {
     return toPredicate(QueryParser.parse(query));
   }
 
-  private Predicate toPredicate(final Tree r) throws QueryParseException,
+  private Predicate<T> toPredicate(final Tree r) throws QueryParseException,
       IllegalArgumentException {
     switch (r.getType()) {
       case AND:
@@ -127,12 +147,26 @@
       case FIELD_NAME:
         return operator(r.getText(), onlyChildOf(r));
 
+      case VARIABLE_ASSIGN: {
+        final String var = r.getText();
+        final Tree opTree = onlyChildOf(r);
+        if (opTree.getType() == FIELD_NAME) {
+          final Tree val = onlyChildOf(opTree);
+          if (val.getType() == SINGLE_WORD && "*".equals(val.getText())) {
+            final String op = opTree.getText();
+            final WildPatternPredicate<T> pat = new WildPatternPredicate<T>(op);
+            return new VariablePredicate<T>(var, pat);
+          }
+        }
+        return new VariablePredicate<T>(var, toPredicate(opTree));
+      }
+
       default:
         throw error("Unsupported operator: " + r);
     }
   }
 
-  private Predicate operator(final String name, final Tree val)
+  private Predicate<T> operator(final String name, final Tree val)
       throws QueryParseException {
     switch (val.getType()) {
       // Expand multiple values, "foo:(a b c)", as though they were written
@@ -140,13 +174,13 @@
       //
       case AND:
       case OR: {
-        final Predicate[] p = new Predicate[val.getChildCount()];
-        for (int i = 0; i < p.length; i++) {
+        List<Predicate<T>> p = new ArrayList<Predicate<T>>(val.getChildCount());
+        for (int i = 0; i < val.getChildCount(); i++) {
           final Tree c = val.getChild(i);
           if (c.getType() != DEFAULT_FIELD) {
             throw error("Nested operator not expected: " + c);
           }
-          p[i] = operator(name, onlyChildOf(c));
+          p.add(operator(name, onlyChildOf(c)));
         }
         return val.getType() == AND ? and(p) : or(p);
       }
@@ -163,16 +197,17 @@
     }
   }
 
-  private Predicate operator(final String name, final String value)
+  @SuppressWarnings("unchecked")
+  private Predicate<T> operator(final String name, final String value)
       throws QueryParseException {
     final OperatorFactory f = opFactories.get(name);
     if (f == null) {
       throw error("Unsupported operator " + name + ":" + value);
     }
-    return f.create(value);
+    return f.create(this, value);
   }
 
-  private Predicate defaultField(final Tree r) throws QueryParseException {
+  private Predicate<T> defaultField(final Tree r) throws QueryParseException {
     switch (r.getType()) {
       case SINGLE_WORD:
       case EXACT_PHRASE:
@@ -197,14 +232,65 @@
    * @return predicate representing this value.
    * @throws QueryParseException the parser does not recognize this value.
    */
-  protected Predicate defaultField(final String value)
+  protected Predicate<T> defaultField(final String value)
       throws QueryParseException {
     throw error("Unsupported query:" + value);
   }
 
-  private Predicate[] children(final Tree r) throws QueryParseException,
+  /**
+   * Locate a predicate in the predicate tree.
+   *
+   * @param p the predicate to find.
+   * @param clazz type of the predicate instance.
+   * @return the predicate, null if not found.
+   */
+  @SuppressWarnings("unchecked")
+  public <P extends Predicate<T>> P find(Predicate<T> p, Class<P> clazz) {
+    if (clazz.isAssignableFrom(p.getClass())) {
+      return (P) p;
+    }
+
+    for (Predicate<T> c : p.getChildren()) {
+      P r = find(c, clazz);
+      if (r != null) {
+        return r;
+      }
+    }
+
+    return null;
+  }
+
+  /**
+   * Locate a predicate in the predicate tree.
+   *
+   * @param p the predicate to find.
+   * @param clazz type of the predicate instance.
+   * @param name name of the operator.
+   * @return the predicate, null if not found.
+   */
+  @SuppressWarnings("unchecked")
+  public <P extends OperatorPredicate<T>> P find(Predicate<T> p,
+      Class<P> clazz, String name) {
+    if (p instanceof OperatorPredicate
+        && ((OperatorPredicate) p).getOperator().equals(name)
+        && clazz.isAssignableFrom(p.getClass())) {
+      return (P) p;
+    }
+
+    for (Predicate<T> c : p.getChildren()) {
+      P r = find(c, clazz, name);
+      if (r != null) {
+        return r;
+      }
+    }
+
+    return null;
+  }
+
+  @SuppressWarnings("unchecked")
+  private Predicate<T>[] children(final Tree r) throws QueryParseException,
       IllegalArgumentException {
-    final Predicate[] p = new Predicate[r.getChildCount()];
+    final Predicate<T>[] p = new Predicate[r.getChildCount()];
     for (int i = 0; i < p.length; i++) {
       p[i] = toPredicate(r.getChild(i));
     }
@@ -227,8 +313,8 @@
   }
 
   /** Converts a value string passed to an operator into a {@link Predicate}. */
-  protected interface OperatorFactory {
-    Predicate create(String value) throws QueryParseException;
+  protected interface OperatorFactory<T, Q extends QueryBuilder<T>> {
+    Predicate<T> create(Q builder, String value) throws QueryParseException;
   }
 
   /** Denotes a method which is a query operator. */
@@ -237,7 +323,8 @@
   protected @interface Operator {
   }
 
-  private class ReflectionFactory implements OperatorFactory {
+  private static class ReflectionFactory<T, Q extends QueryBuilder<T>>
+      implements OperatorFactory<T, Q> {
     private final String name;
     private final Method method;
 
@@ -246,15 +333,20 @@
       this.method = method;
     }
 
+    @SuppressWarnings("unchecked")
     @Override
-    public Predicate create(final String value) throws QueryParseException {
+    public Predicate<T> create(Q builder, String value)
+        throws QueryParseException {
       try {
-        return (Predicate) method.invoke(QueryBuilder.this, value);
+        return (Predicate<T>) method.invoke(builder, value);
       } catch (RuntimeException e) {
         throw error("Error in operator " + name + ":" + value, e);
       } catch (IllegalAccessException e) {
         throw error("Error in operator " + name + ":" + value, e);
       } catch (InvocationTargetException e) {
+        if (e.getCause() instanceof QueryParseException) {
+          throw (QueryParseException) e.getCause();
+        }
         throw error("Error in operator " + name + ":" + value, e.getCause());
       }
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryRewriter.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryRewriter.java
new file mode 100644
index 0000000..fea37e1
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryRewriter.java
@@ -0,0 +1,518 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query;
+
+import com.google.inject.name.Named;
+
+import java.lang.annotation.Annotation;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Rewrites a Predicate tree by applying rewrite rules.
+ * <p>
+ * Subclasses may document their rewrite rules by declaring public methods with
+ * {@link Rewrite} annotations, such as:
+ *
+ * <pre>
+ * &#064;Rewrite(&quot;A=(owner:*) B=(status:*)&quot;)
+ * public Predicate r1_ownerStatus(@Named(&quot;A&quot;) OperatorPredicate owner,
+ *     &#064;Named(&quot;B&quot;) OperatorPredicate status) {
+ * }
+ * </pre>
+ * <p>
+ * Rewrite methods are applied in order by declared name, so naming methods with
+ * a numeric prefix to ensure a specific ordering (if required) is suggested.
+ *
+ * @type <T> type of object the predicate can evaluate in memory.
+ */
+public abstract class QueryRewriter<T> {
+  /**
+   * Defines the rewrite rules known by a QueryRewriter.
+   *
+   * This class is thread-safe and may be reused or cached.
+   *
+   * @param <T> type of object the predicates can evaluate in memory.
+   * @param <R> type of the rewriter subclass.
+   */
+  public static class Definition<T, R extends QueryRewriter<T>> {
+    private final List<RewriteRule<T>> rewriteRules;
+
+    public Definition(Class<R> clazz, QueryBuilder<T> qb) {
+      rewriteRules = new ArrayList<RewriteRule<T>>();
+
+      Class<?> c = clazz;
+      while (c != QueryRewriter.class) {
+        final Method[] declared = c.getDeclaredMethods();
+        Arrays.sort(declared, new Comparator<Method>() {
+          @Override
+          public int compare(Method o1, Method o2) {
+            return o1.getName().compareTo(o2.getName());
+          }
+        });
+        for (Method m : declared) {
+          final Rewrite rp = m.getAnnotation(Rewrite.class);
+          if ((m.getModifiers() & Modifier.ABSTRACT) != Modifier.ABSTRACT
+              && (m.getModifiers() & Modifier.PUBLIC) == Modifier.PUBLIC
+              && rp != null) {
+            rewriteRules.add(new MethodRewrite(qb, rp.value(), m));
+          }
+        }
+        c = c.getSuperclass();
+      }
+    }
+  }
+
+  private final List<RewriteRule<T>> rewriteRules;
+
+  protected QueryRewriter(final Definition<T, ? extends QueryRewriter<T>> def) {
+    this.rewriteRules = def.rewriteRules;
+  }
+
+  /** Combine the passed predicates into a single AND node. */
+  public Predicate<T> and(Collection<? extends Predicate<T>> that) {
+    return Predicate.and(that);
+  }
+
+  /** Combine the passed predicates into a single AND node. */
+  public Predicate<T> and(Predicate<T>... that) {
+    return and(Arrays.asList(that));
+  }
+
+  /** Combine the passed predicates into a single OR node. */
+  public Predicate<T> or(Collection<? extends Predicate<T>> that) {
+    return Predicate.or(that);
+  }
+
+  /** Combine the passed predicates into a single OR node. */
+  public Predicate<T> or(Predicate<T>... that) {
+    return or(Arrays.asList(that));
+  }
+
+  /** Invert the passed node. */
+  public Predicate<T> not(Predicate<T> that) {
+    return Predicate.not(that);
+  }
+
+  /**
+   * Apply rewrites to a graph until it stops changing.
+   *
+   * @param in the graph to rewrite.
+   * @return the rewritten graph.
+   */
+  public Predicate<T> rewrite(Predicate<T> in) {
+    Predicate<T> old;
+    do {
+      old = in;
+      in = rewriteOne(in);
+
+      if (old.equals(in) && in.getChildCount() > 0) {
+        List<Predicate<T>> n = new ArrayList<Predicate<T>>(in.getChildCount());
+        for (Predicate<T> p : in.getChildren()) {
+          n.add(rewrite(p));
+        }
+        n = removeDuplicates(n);
+        if (n.size() == 1 && (isAND(in) || isOR(in))) {
+          in = n.get(0);
+        } else {
+          in = in.copy(n);
+        }
+      }
+
+    } while (!old.equals(in));
+    return replaceGenericNodes(in);
+  }
+
+  protected Predicate<T> replaceGenericNodes(final Predicate<T> in) {
+    if (in.getClass() == NotPredicate.class) {
+      return not(replaceGenericNodes(in.getChild(0)));
+
+    } else if (in.getClass() == AndPredicate.class) {
+      List<Predicate<T>> n = new ArrayList<Predicate<T>>(in.getChildCount());
+      for (Predicate<T> c : in.getChildren()) {
+        n.add(replaceGenericNodes(c));
+      }
+      return and(n);
+
+    } else if (in.getClass() == OrPredicate.class) {
+      List<Predicate<T>> n = new ArrayList<Predicate<T>>(in.getChildCount());
+      for (Predicate<T> c : in.getChildren()) {
+        n.add(replaceGenericNodes(c));
+      }
+      return or(n);
+
+    } else {
+      return in;
+    }
+  }
+
+  private Predicate<T> rewriteOne(Predicate<T> input) {
+    Predicate<T> best = null;
+    for (RewriteRule<T> r : rewriteRules) {
+      Predicate<T> n = r.rewrite(this, input);
+      if (n == null) {
+        continue;
+      }
+
+      if (!r.useBestCost()) {
+        return n;
+      }
+
+      if (best == null || n.getCost() < best.getCost()) {
+        best = n;
+        continue;
+      }
+    }
+    return best != null ? best : input;
+  }
+
+  private static class MatchResult<T> {
+    private static final MatchResult<?> FAIL = new MatchResult<Object>(null);
+    private static final MatchResult<?> OK = new MatchResult<Object>(null);
+
+    @SuppressWarnings("unchecked")
+    static <T> MatchResult<T> fail() {
+      return (MatchResult<T>) FAIL;
+    }
+
+    @SuppressWarnings("unchecked")
+    static <T> MatchResult<T> ok() {
+      return (MatchResult<T>) OK;
+    }
+
+    final Predicate<T> extra;
+
+    MatchResult(Predicate<T> extra) {
+      this.extra = extra;
+    }
+
+    boolean success() {
+      return this != FAIL;
+    }
+  }
+
+  private MatchResult<T> match(final Map<String, Predicate<T>> outVars,
+      final Predicate<T> pattern, final Predicate<T> actual) {
+    if (pattern instanceof VariablePredicate) {
+      final VariablePredicate<T> v = (VariablePredicate<T>) pattern;
+      final MatchResult<T> r = match(outVars, v.getChild(0), actual);
+      if (r.success()) {
+        Predicate<T> old = outVars.get(v.getName());
+        if (old == null) {
+          outVars.put(v.getName(), actual);
+          return r;
+        } else if (old.equals(actual)) {
+          return r;
+        } else {
+          return MatchResult.fail();
+        }
+      } else {
+        return MatchResult.fail();
+      }
+    }
+
+    final int cnt = pattern.getChildCount();
+    if ((isAND(pattern) && isAND(actual)) //
+        || (isOR(pattern) && isOR(actual)) //
+        || (isNOT(pattern) && isNOT(actual)) //
+    ) {
+      // Order doesn't actually matter here. That does make our logic quite
+      // a bit more complex as we need to consult each child at most once,
+      // but in any order.
+      //
+      final LinkedList<Predicate<T>> have = dup(actual);
+      final LinkedList<Predicate<T>> extra = new LinkedList<Predicate<T>>();
+      for (final Predicate<T> pat : pattern.getChildren()) {
+        boolean found = false;
+        for (final Iterator<Predicate<T>> i = have.iterator(); i.hasNext();) {
+          final MatchResult<T> r = match(outVars, pat, i.next());
+          if (r.success()) {
+            found = true;
+            i.remove();
+            if (r.extra != null) {
+              extra.add(r.extra);
+            }
+            break;
+          }
+        }
+        if (!found) {
+          return MatchResult.fail();
+        }
+      }
+      have.addAll(extra);
+      switch (have.size()) {
+        case 0:
+          return MatchResult.ok();
+        case 1:
+          if (isNOT(actual)) {
+            return new MatchResult<T>(actual.copy(have));
+          }
+          return new MatchResult<T>(have.get(0));
+        default:
+          return new MatchResult<T>(actual.copy(have));
+      }
+
+    } else if (pattern.equals(actual)) {
+      return MatchResult.ok();
+
+    } else if (pattern instanceof WildPatternPredicate
+        && actual instanceof OperatorPredicate
+        && ((OperatorPredicate<T>) pattern).getOperator().equals(
+            ((OperatorPredicate<T>) actual).getOperator())) {
+      return MatchResult.ok();
+
+    } else {
+      return MatchResult.fail();
+    }
+  }
+
+  private static <T> LinkedList<Predicate<T>> dup(final Predicate<T> actual) {
+    return new LinkedList<Predicate<T>>(actual.getChildren());
+  }
+
+  /**
+   * Denotes a method which wants to replace a predicate expression.
+   * <p>
+   * This annotation must be applied to a public method which returns
+   * {@link Predicate}. The arguments of the method should {@link Predicate}, or
+   * any subclass of it. The annotation value is a query language string which
+   * describes the subtree this rewrite applies to. Method arguments should be
+   * named with a {@link Named} annotation, and the same names should be used in
+   * the query.
+   * <p>
+   * For example:
+   *
+   * <pre>
+   * &#064;Rewrite(&quot;A=(owner:*) B=(status:*)&quot;)
+   * public Predicate ownerStatus(@Named(&quot;A&quot;) OperatorPredicate owner,
+   *     &#064;Named(&quot;B&quot;) OperatorPredicate status) {
+   * }
+   * </pre>
+   *
+   * matches an AND Predicate with at least two children, one being an operator
+   * predicate called "owner" and the other being an operator predicate called
+   * "status". The variables in the query are matched by name against the
+   * parameters.
+   */
+  @Retention(RetentionPolicy.RUNTIME)
+  @Target(ElementType.METHOD)
+  protected @interface Rewrite {
+    String value();
+  }
+
+  @Retention(RetentionPolicy.RUNTIME)
+  @Target(ElementType.METHOD)
+  protected @interface NoCostComputation {
+  }
+
+  /** Applies a rewrite rule to a Predicate. */
+  protected interface RewriteRule<T> {
+    /**
+     * Apply a rewrite rule to the Predicate.
+     *
+     * @param input the input predicate to be tested, and possibly rewritten.
+     * @return a rewritten form of the predicate if this rule matches with the
+     *         tree {@code input} and has a rewrite for it; {@code null} if this
+     *         rule does not want this predicate.
+     */
+    Predicate<T> rewrite(QueryRewriter<T> rewriter, Predicate<T> input);
+
+    /** @return true if the best cost should be selected. */
+    boolean useBestCost();
+  }
+
+  /** Implements the magic behind {@link Rewrite} annotations. */
+  private static class MethodRewrite<T> implements RewriteRule<T> {
+    private final Method method;
+    private final Predicate<T> pattern;
+    private final String[] argNames;
+    private final Class<? extends Predicate<T>>[] argTypes;
+    private final boolean useBestCost;
+
+    @SuppressWarnings("unchecked")
+    MethodRewrite(QueryBuilder<T> queryBuilder, String patternText, Method m) {
+      method = m;
+      useBestCost = m.getAnnotation(NoCostComputation.class) == null;
+
+      Predicate<T> p;
+      try {
+        p = queryBuilder.parse(patternText);
+      } catch (QueryParseException e) {
+        throw new RuntimeException("Bad @Rewrite(\"" + patternText + "\")"
+            + " on " + m.toGenericString() + " in " + m.getDeclaringClass()
+            + ": " + e.getMessage(), e);
+      }
+      if (!Predicate.class.isAssignableFrom(m.getReturnType())) {
+        throw new RuntimeException(m.toGenericString() + " in "
+            + m.getDeclaringClass() + " must return " + Predicate.class);
+      }
+
+      pattern = p;
+      argNames = new String[method.getParameterTypes().length];
+      argTypes = new Class[argNames.length];
+      for (int i = 0; i < argNames.length; i++) {
+        Named name = null;
+        for (Annotation a : method.getParameterAnnotations()[i]) {
+          if (a instanceof Named) {
+            name = (Named) a;
+            break;
+          }
+        }
+        if (name == null) {
+          throw new RuntimeException("Argument " + (i + 1) + " of "
+              + m.toGenericString() + " in " + m.getDeclaringClass()
+              + " has no @Named annotation");
+        }
+        if (!Predicate.class.isAssignableFrom(method.getParameterTypes()[i])) {
+          throw new RuntimeException("Argument " + (i + 1) + " of "
+              + m.toGenericString() + " in " + m.getDeclaringClass()
+              + " must be of type " + Predicate.class);
+        }
+        argNames[i] = name.value();
+        argTypes[i] = (Class<Predicate<T>>) method.getParameterTypes()[i];
+      }
+    }
+
+    @Override
+    public boolean useBestCost() {
+      return useBestCost;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public Predicate<T> rewrite(QueryRewriter<T> rewriter,
+        final Predicate<T> input) {
+      final HashMap<String, Predicate<T>> args =
+          new HashMap<String, Predicate<T>>();
+      final MatchResult<T> res = rewriter.match(args, pattern, input);
+      if (!res.success()) {
+        return null;
+      }
+
+      final Predicate[] argList = new Predicate[argNames.length];
+      for (int i = 0; i < argList.length; i++) {
+        argList[i] = args.get(argNames[i]);
+        if (argList[i] == null) {
+          final String a = "@Named(\"" + argNames[i] + "\")";
+          throw error(new IllegalStateException("No value bound for " + a));
+        }
+        if (!argTypes[i].isInstance(argList[i])) {
+          return null;
+        }
+      }
+
+      final Predicate<T> rep;
+      try {
+        rep = (Predicate<T>) method.invoke(rewriter, (Object[]) argList);
+      } catch (IllegalArgumentException e) {
+        throw error(e);
+      } catch (IllegalAccessException e) {
+        throw error(e);
+      } catch (InvocationTargetException e) {
+        throw error(e.getCause());
+      }
+
+      if (rep instanceof RewritePredicate) {
+        ((RewritePredicate) rep).init(method.getName(), argList);
+      }
+
+      if (res.extra == null) {
+        return rep;
+      }
+
+      Predicate<T> extra = removeDuplicates(res.extra);
+      Predicate<T>[] newArgs = new Predicate[] {extra, rep};
+      return input.copy(Arrays.asList(newArgs));
+    }
+
+    private IllegalArgumentException error(Throwable e) {
+      final String msg = "Cannot apply " + method.getName();
+      return new IllegalArgumentException(msg, e);
+    }
+  }
+
+  private static <T> Predicate<T> removeDuplicates(Predicate<T> in) {
+    if (in.getChildCount() > 0) {
+      List<Predicate<T>> n = removeDuplicates(in.getChildren());
+      if (n.size() == 1 && (isAND(in) || isOR(in))) {
+        in = n.get(0);
+      } else {
+        in = in.copy(n);
+      }
+    }
+    return in;
+  }
+
+  private static <T> List<Predicate<T>> removeDuplicates(List<Predicate<T>> n) {
+    List<Predicate<T>> r = new ArrayList<Predicate<T>>();
+    for (Predicate<T> p : n) {
+      if (!r.contains(p)) {
+        r.add(p);
+      }
+    }
+    return r;
+  }
+
+  private static <T> void expand(final List<Predicate<T>> out,
+      final List<Predicate<T>> allOR, final List<Predicate<T>> tmp,
+      final List<Predicate<T>> nonOR) {
+    if (tmp.size() == allOR.size()) {
+      final int sz = nonOR.size() + tmp.size();
+      final List<Predicate<T>> newList = new ArrayList<Predicate<T>>(sz);
+      newList.addAll(nonOR);
+      newList.addAll(tmp);
+      out.add(Predicate.and(newList));
+
+    } else {
+      for (final Predicate<T> c : allOR.get(tmp.size()).getChildren()) {
+        try {
+          tmp.add(c);
+          expand(out, allOR, tmp, nonOR);
+        } finally {
+          tmp.remove(tmp.size() - 1);
+        }
+      }
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  private static <T> boolean isAND(final Predicate<T> p) {
+    return p instanceof AndPredicate;
+  }
+
+  @SuppressWarnings("unchecked")
+  private static <T> boolean isOR(final Predicate<T> p) {
+    return p instanceof OrPredicate;
+  }
+
+  @SuppressWarnings("unchecked")
+  private static <T> boolean isNOT(final Predicate<T> p) {
+    return p instanceof NotPredicate;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/RewritePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/RewritePredicate.java
new file mode 100644
index 0000000..c0a00ca
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/RewritePredicate.java
@@ -0,0 +1,68 @@
+// 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;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+public abstract class RewritePredicate<T> extends Predicate<T> {
+  private String name = getClass().getName();
+  private List<Predicate<T>> children = Collections.emptyList();
+
+  void init(String name, Predicate<T>[] args) {
+    this.name = name;
+    this.children = Arrays.asList(args);
+  }
+
+  @Override
+  public Predicate<T> copy(Collection<? extends Predicate<T>> children) {
+    return this;
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    return getClass() == other.getClass()
+        && children.equals(((RewritePredicate) other).children);
+  }
+
+  @Override
+  public int hashCode() {
+    int h = getClass().hashCode();
+    if (!children.isEmpty()) {
+      h *= 31;
+      h += children.get(0).hashCode();
+    }
+    return h;
+  }
+
+  @Override
+  public final String toString() {
+    final StringBuilder r = new StringBuilder();
+    r.append(name);
+    if (!children.isEmpty()) {
+      r.append("(");
+      for (int i = 0; i < children.size(); i++) {
+        if (i != 0) {
+          r.append(" ");
+        }
+        r.append(children.get(i));
+      }
+      r.append(")");
+    }
+    return r.toString();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/VariablePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/VariablePredicate.java
new file mode 100644
index 0000000..0f6f957
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/VariablePredicate.java
@@ -0,0 +1,96 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query;
+
+import com.google.gwtorm.client.OrmException;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Holds another predicate in a named variable.
+ *
+ * @see QueryRewriter
+ */
+public class VariablePredicate<T> extends Predicate<T> {
+  private final String name;
+  private final Predicate<T> that;
+
+  protected VariablePredicate(final String name, final Predicate<T> that) {
+    this.name = name;
+    this.that = that;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  @Override
+  public final List<Predicate<T>> getChildren() {
+    return Collections.singletonList(that);
+  }
+
+  @Override
+  public final int getChildCount() {
+    return 1;
+  }
+
+  @Override
+  public final Predicate<T> getChild(final int i) {
+    if (i != 0) {
+      throw new ArrayIndexOutOfBoundsException(i);
+    }
+    return that;
+  }
+
+  @Override
+  public Predicate<T> copy(final Collection<? extends Predicate<T>> children) {
+    if (children.size() != 1) {
+      throw new IllegalArgumentException("Expected exactly one child");
+    }
+    return new VariablePredicate<T>(getName(), children.iterator().next());
+  }
+
+  @Override
+  public boolean match(final T object) throws OrmException {
+    return that.match(object);
+  }
+
+  @Override
+  public int getCost() {
+    return that.getCost();
+  }
+
+  @Override
+  public int hashCode() {
+    return getName().hashCode() * 31 + that.hashCode();
+  }
+
+  @Override
+  public boolean equals(final Object other) {
+    if (getClass() == other.getClass()) {
+      final VariablePredicate<?> v = (VariablePredicate<?>) other;
+      return getName().equals(v.getName())
+          && getChildren().equals(v.getChildren());
+    }
+    return false;
+  }
+
+  @Override
+  public final String toString() {
+    return getName() + "=(" + that.toString() + ")";
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/WildPatternPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/WildPatternPredicate.java
new file mode 100644
index 0000000..61c8ea9
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/WildPatternPredicate.java
@@ -0,0 +1,59 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query;
+
+/**
+ * Predicate only for use in rewrite rule patterns.
+ * <p>
+ * May <b>only</b> be used when nested immediately within a
+ * {@link VariablePredicate}. Within the QueryRewriter this predicate matches
+ * any other operator whose name matches this predicate's operator name.
+ *
+ * @see QueryRewriter
+ */
+public final class WildPatternPredicate<T> extends OperatorPredicate<T> {
+  public WildPatternPredicate(final String name) {
+    super(name, "*");
+  }
+
+  @Override
+  public boolean match(final T object) {
+    throw new UnsupportedOperationException("Cannot match " + toString());
+  }
+
+  @Override
+  public int getCost() {
+    return 0;
+  }
+
+  @Override
+  public int hashCode() {
+    return getOperator().hashCode() * 31;
+  }
+
+  @Override
+  public boolean equals(final Object other) {
+    if (getClass() == other.getClass()) {
+      final WildPatternPredicate<?> p = (WildPatternPredicate<?>) other;
+      return getOperator().equals(p.getOperator());
+    }
+    return false;
+  }
+
+  @Override
+  public String toString() {
+    return getOperator() + ":" + getValue();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AbstractResultSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AbstractResultSet.java
new file mode 100644
index 0000000..2b8d37d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AbstractResultSet.java
@@ -0,0 +1,31 @@
+// 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.gwtorm.client.ResultSet;
+
+import java.util.ArrayList;
+import java.util.List;
+
+abstract class AbstractResultSet<T> implements ResultSet<T> {
+  @Override
+  public List<T> toList() {
+    ArrayList<T> r = new ArrayList<T>();
+    for (T t : this) {
+      r.add(t);
+    }
+    return r;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AgePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AgePredicate.java
new file mode 100644
index 0000000..18a0c82
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AgePredicate.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.query.change;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gwtorm.client.OrmException;
+import com.google.inject.Provider;
+
+class AgePredicate extends OperatorPredicate<ChangeData> {
+  private final Provider<ReviewDb> dbProvider;
+  private final long cut;
+
+  AgePredicate(Provider<ReviewDb> dbProvider, String value) {
+    super(ChangeQueryBuilder.FIELD_AGE, value);
+    this.dbProvider = dbProvider;
+
+    long s = ConfigUtil.getTimeUnit(getValue(), 0, SECONDS);
+    long ms = MILLISECONDS.convert(s, SECONDS);
+    this.cut = (System.currentTimeMillis() - ms) + 1;
+  }
+
+  long getCut() {
+    return cut;
+  }
+
+  @Override
+  public boolean match(final ChangeData object) throws OrmException {
+    Change change = object.change(dbProvider);
+    return change != null && change.getLastUpdatedOn().getTime() < cut;
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java
new file mode 100644
index 0000000..e74b390
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java
@@ -0,0 +1,149 @@
+// 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.server.query.AndPredicate;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.client.ResultSet;
+import com.google.gwtorm.client.impl.ListResultSet;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+class AndSource extends AndPredicate<ChangeData> implements ChangeDataSource {
+  private static final Comparator<Predicate<ChangeData>> CMP =
+      new Comparator<Predicate<ChangeData>>() {
+        @Override
+        public int compare(Predicate<ChangeData> a, Predicate<ChangeData> b) {
+          int ai = a instanceof ChangeDataSource ? 0 : 1;
+          int bi = b instanceof ChangeDataSource ? 0 : 1;
+          int cmp = ai - bi;
+
+          if (cmp == 0 //
+              && a instanceof ChangeDataSource //
+              && b instanceof ChangeDataSource) {
+            ai = ((ChangeDataSource) a).hasChange() ? 0 : 1;
+            bi = ((ChangeDataSource) b).hasChange() ? 0 : 1;
+            cmp = ai - bi;
+          }
+
+          if (cmp == 0) {
+            cmp = a.getCost() - b.getCost();
+          }
+
+          if (cmp == 0 //
+              && a instanceof ChangeDataSource //
+              && b instanceof ChangeDataSource) {
+            ChangeDataSource as = (ChangeDataSource) a;
+            ChangeDataSource bs = (ChangeDataSource) b;
+            cmp = as.getCardinality() - bs.getCardinality();
+          }
+
+          return cmp;
+        }
+      };
+
+  private static List<Predicate<ChangeData>> sort(
+      Collection<? extends Predicate<ChangeData>> that) {
+    ArrayList<Predicate<ChangeData>> r =
+        new ArrayList<Predicate<ChangeData>>(that);
+    Collections.sort(r, CMP);
+    return r;
+  }
+
+  private int cardinality = -1;
+
+  AndSource(final Collection<? extends Predicate<ChangeData>> that) {
+    super(sort(that));
+  }
+
+  @Override
+  public boolean hasChange() {
+    ChangeDataSource source = source();
+    return source != null && source.hasChange();
+  }
+
+  @Override
+  public ResultSet<ChangeData> read() throws OrmException {
+    ChangeDataSource source = source();
+    if (source == null) {
+      throw new OrmException("No ChangeDataSource: " + this);
+    }
+
+    // TODO(spearce) This probably should be more lazy.
+    //
+    ArrayList<ChangeData> r = new ArrayList<ChangeData>();
+    ChangeData last = null;
+    boolean skipped = false;
+    for (ChangeData data : source.read()) {
+      if (match(data)) {
+        r.add(data);
+      } else {
+        skipped = true;
+      }
+      last = data;
+    }
+
+    if (skipped && last != null && source instanceof Paginated) {
+      // If we our source is a paginated source and we skipped at
+      // least one of its results, we may not have filled the full
+      // limit the caller wants.  Restart the source and continue.
+      //
+      Paginated p = (Paginated) source;
+      while (skipped && r.size() < p.limit()) {
+        ChangeData lastBeforeRestart = last;
+        skipped = false;
+        last = null;
+        for (ChangeData data : p.restart(lastBeforeRestart)) {
+          if (match(data)) {
+            r.add(data);
+          } else {
+            skipped = true;
+          }
+          last = data;
+        }
+      }
+    }
+
+    return new ListResultSet<ChangeData>(r);
+  }
+
+  private ChangeDataSource source() {
+    for (Predicate<ChangeData> p : getChildren()) {
+      if (p instanceof ChangeDataSource) {
+        return (ChangeDataSource) p;
+      }
+    }
+    return null;
+  }
+
+  @Override
+  public int getCardinality() {
+    if (cardinality < 0) {
+      cardinality = Integer.MAX_VALUE;
+      for (Predicate<ChangeData> p : getChildren()) {
+        if (p instanceof ChangeDataSource) {
+          int c = ((ChangeDataSource) p).getCardinality();
+          cardinality = Math.min(cardinality, c);
+        }
+      }
+    }
+    return cardinality;
+  }
+}
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
new file mode 100644
index 0000000..ae48fdc
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BranchPredicate.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.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;
+
+class BranchPredicate extends OperatorPredicate<ChangeData> {
+  private final Provider<ReviewDb> dbProvider;
+  private final String shortName;
+
+  BranchPredicate(Provider<ReviewDb> dbProvider, String branch) {
+    super(ChangeQueryBuilder.FIELD_BRANCH, branch);
+    this.dbProvider = dbProvider;
+    this.shortName = new Branch.NameKey(null, branch).getShortName();
+  }
+
+  @Override
+  public boolean match(final ChangeData object) throws OrmException {
+    Change change = object.change(dbProvider);
+    if (change == null) {
+      return false;
+    }
+    return shortName.equals(change.getDest().getShortName());
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeCosts.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeCosts.java
new file mode 100644
index 0000000..ba42803
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeCosts.java
@@ -0,0 +1,39 @@
+// 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;
+
+public class ChangeCosts {
+  public static final int IDS_MEMORY = 1;
+  public static final int CHANGES_SCAN = 2;
+  public static final int TR_SCAN = 20;
+  public static final int APPROVALS_SCAN = 30;
+  public static final int PATCH_SETS_SCAN = 30;
+
+  /** Estimated matches for a Change-Id string. */
+  public static final int CARD_KEY = 5;
+
+  /** Estimated matches for a commit SHA-1 string. */
+  public static final int CARD_COMMIT = 5;
+
+  /** Estimated matches for a tracking/bug id string. */
+  public static final int CARD_TRACKING_IDS = 5;
+
+  public static int cost(int cost, int cardinality) {
+    return Math.max(1, cost) * Math.max(0, cardinality);
+  }
+
+  private ChangeCosts() {
+  }
+}
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
new file mode 100644
index 0000000..479a5ef
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -0,0 +1,189 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.PatchLineComment;
+import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.reviewdb.PatchSetApproval;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.TrackingId;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.patch.PatchList;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gerrit.server.patch.PatchListEntry;
+import com.google.gwtorm.client.OrmException;
+import com.google.inject.Provider;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+public class ChangeData {
+  private final Change.Id legacyId;
+  private Change change;
+  private Collection<PatchSet> patches;
+  private Collection<PatchSetApproval> approvals;
+  private Collection<PatchSetApproval> currentApprovals;
+  private Collection<String> currentFiles;
+  private Collection<PatchLineComment> comments;
+  private Collection<TrackingId> trackingIds;
+  private CurrentUser visibleTo;
+
+  public ChangeData(final Change.Id id) {
+    legacyId = id;
+  }
+
+  public ChangeData(final Change c) {
+    legacyId = c.getId();
+    change = c;
+  }
+
+  public void setCurrentFilePaths(Collection<String> filePaths) {
+    currentFiles = filePaths;
+  }
+
+  public Collection<String> currentFilePaths(Provider<ReviewDb> db,
+      PatchListCache cache) throws OrmException {
+    if (currentFiles == null) {
+      Change c = change(db);
+      if (c == null) {
+        return null;
+      }
+      PatchSet ps = currentPatchSet(db);
+      if (ps == null) {
+        return null;
+      }
+
+      PatchList p = cache.get(c, ps);
+      List<String> r = new ArrayList<String>(p.getPatches().size());
+      for (PatchListEntry e : p.getPatches()) {
+        switch (e.getChangeType()) {
+          case ADDED:
+          case MODIFIED:
+          case COPIED:
+            r.add(e.getNewName());
+            break;
+          case DELETED:
+            r.add(e.getOldName());
+            break;
+          case RENAMED:
+            r.add(e.getOldName());
+            r.add(e.getNewName());
+            break;
+        }
+      }
+      currentFiles = r;
+    }
+    return currentFiles;
+  }
+
+  public Change.Id getId() {
+    return legacyId;
+  }
+
+  public Change getChange() {
+    return change;
+  }
+
+  public boolean hasChange() {
+    return change != null;
+  }
+
+  boolean fastIsVisibleTo(CurrentUser user) {
+    return visibleTo == user;
+  }
+
+  void cacheVisibleTo(CurrentUser user) {
+    visibleTo = user;
+  }
+
+  public Change change(Provider<ReviewDb> db) throws OrmException {
+    if (change == null) {
+      change = db.get().changes().get(legacyId);
+    }
+    return change;
+  }
+
+  public PatchSet currentPatchSet(Provider<ReviewDb> db) throws OrmException {
+    Change c = change(db);
+    if (c == null) {
+      return null;
+    }
+    for (PatchSet p : patches(db)) {
+      if (p.getId().equals(c.currentPatchSetId())) {
+        return p;
+      }
+    }
+    return null;
+  }
+
+  public Collection<PatchSetApproval> currentApprovals(Provider<ReviewDb> db)
+      throws OrmException {
+    if (currentApprovals == null) {
+      Change c = change(db);
+      if (c == null) {
+        currentApprovals = Collections.emptyList();
+      } else {
+        currentApprovals = approvalsFor(db, c.currentPatchSetId());
+      }
+    }
+    return currentApprovals;
+  }
+
+  public Collection<PatchSetApproval> approvalsFor(Provider<ReviewDb> db,
+      PatchSet.Id psId) throws OrmException {
+    List<PatchSetApproval> r = new ArrayList<PatchSetApproval>();
+    for (PatchSetApproval p : approvals(db)) {
+      if (p.getPatchSetId().equals(psId)) {
+        r.add(p);
+      }
+    }
+    return r;
+  }
+
+  public Collection<PatchSet> patches(Provider<ReviewDb> db)
+      throws OrmException {
+    if (patches == null) {
+      patches = db.get().patchSets().byChange(legacyId).toList();
+    }
+    return patches;
+  }
+
+  public Collection<PatchSetApproval> approvals(Provider<ReviewDb> db)
+      throws OrmException {
+    if (approvals == null) {
+      approvals = db.get().patchSetApprovals().byChange(legacyId).toList();
+    }
+    return approvals;
+  }
+
+  public Collection<PatchLineComment> comments(Provider<ReviewDb> db)
+      throws OrmException {
+    if (comments == null) {
+      comments = db.get().patchComments().byChange(legacyId).toList();
+    }
+    return comments;
+  }
+
+  public Collection<TrackingId> trackingIds(Provider<ReviewDb> db)
+      throws OrmException {
+    if (trackingIds == null) {
+      trackingIds = db.get().trackingIds().byChange(legacyId).toList();
+    }
+    return trackingIds;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeDataResultSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeDataResultSet.java
new file mode 100644
index 0000000..fc7ba59
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeDataResultSet.java
@@ -0,0 +1,130 @@
+// 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.PatchSetApproval;
+import com.google.gwtorm.client.ResultSet;
+
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+abstract class ChangeDataResultSet<T> extends AbstractResultSet<ChangeData> {
+  static ResultSet<ChangeData> change(final ResultSet<Change> rs) {
+    return new ChangeDataResultSet<Change>(rs, true) {
+      @Override
+      ChangeData convert(Change t) {
+        return new ChangeData(t);
+      }
+    };
+  }
+
+  static ResultSet<ChangeData> patchSet(final ResultSet<PatchSet> rs) {
+    return new ChangeDataResultSet<PatchSet>(rs, false) {
+      @Override
+      ChangeData convert(PatchSet t) {
+        return new ChangeData(t.getId().getParentKey());
+      }
+    };
+  }
+
+  static ResultSet<ChangeData> patchSetApproval(
+      final ResultSet<PatchSetApproval> rs) {
+    return new ChangeDataResultSet<PatchSetApproval>(rs, false) {
+      @Override
+      ChangeData convert(PatchSetApproval t) {
+        return new ChangeData(t.getPatchSetId().getParentKey());
+      }
+    };
+  }
+
+  private final ResultSet<T> source;
+  private final boolean unique;
+
+  ChangeDataResultSet(ResultSet<T> source, boolean unique) {
+    this.source = source;
+    this.unique = unique;
+  }
+
+  @Override
+  public Iterator<ChangeData> iterator() {
+    if (unique) {
+      return new Iterator<ChangeData>() {
+        private final Iterator<T> itr = source.iterator();
+
+        @Override
+        public boolean hasNext() {
+          return itr.hasNext();
+        }
+
+        @Override
+        public ChangeData next() {
+          return convert(itr.next());
+        }
+
+        @Override
+        public void remove() {
+          throw new UnsupportedOperationException();
+        }
+      };
+
+    } else {
+      return new Iterator<ChangeData>() {
+        private final Iterator<T> itr = source.iterator();
+        private final HashSet<Change.Id> seen = new HashSet<Change.Id>();
+        private ChangeData next;
+
+        @Override
+        public boolean hasNext() {
+          if (next != null) {
+            return true;
+          }
+          while (itr.hasNext()) {
+            ChangeData d = convert(itr.next());
+            if (seen.add(d.getId())) {
+              next = d;
+              return true;
+            }
+          }
+          return false;
+        }
+
+        @Override
+        public ChangeData next() {
+          if (hasNext()) {
+            ChangeData r = next;
+            next = null;
+            return r;
+          }
+          throw new NoSuchElementException();
+        }
+
+        @Override
+        public void remove() {
+          throw new UnsupportedOperationException();
+        }
+      };
+    }
+  }
+
+  @Override
+  public void close() {
+    source.close();
+  }
+
+  abstract ChangeData convert(T t);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeDataSource.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeDataSource.java
new file mode 100644
index 0000000..a770b54
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeDataSource.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.query.change;
+
+import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.client.ResultSet;
+
+public interface ChangeDataSource {
+  /** @return an estimate of the number of results from {@link #read()}. */
+  public int getCardinality();
+
+  /** @return true if all returned ChangeData.hasChange() will be true. */
+  public boolean hasChange();
+
+  /** @return read from the database and return the changes. */
+  public abstract ResultSet<ChangeData> read() throws OrmException;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java
new file mode 100644
index 0000000..83107bb
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java
@@ -0,0 +1,69 @@
+// 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.gwtorm.client.ResultSet;
+import com.google.inject.Provider;
+
+class ChangeIdPredicate extends OperatorPredicate<ChangeData> implements
+    ChangeDataSource {
+  private final Provider<ReviewDb> dbProvider;
+
+  ChangeIdPredicate(Provider<ReviewDb> dbProvider, String id) {
+    super(ChangeQueryBuilder.FIELD_CHANGE, id);
+    this.dbProvider = dbProvider;
+  }
+
+  @Override
+  public boolean match(final ChangeData cd) throws OrmException {
+    Change change = cd.change(dbProvider);
+    if (change == null) {
+      return false;
+    }
+
+    String key = change.getKey().get();
+    if (key.equals(getValue()) || key.startsWith(getValue())) {
+      return true;
+    }
+    return false;
+  }
+
+  @Override
+  public ResultSet<ChangeData> read() throws OrmException {
+    Change.Key a = new Change.Key(getValue());
+    Change.Key b = a.max();
+    return ChangeDataResultSet.change( //
+        dbProvider.get().changes().byKeyRange(a, b));
+  }
+
+  @Override
+  public boolean hasChange() {
+    return true;
+  }
+
+  @Override
+  public int getCost() {
+    return ChangeCosts.cost(ChangeCosts.CHANGES_SCAN, getCardinality());
+  }
+
+  @Override
+  public int getCardinality() {
+    return ChangeCosts.CARD_KEY;
+  }
+}
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
new file mode 100644
index 0000000..1b9f051
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -0,0 +1,453 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.RevId;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.server.config.WildProjectName;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.query.IntPredicate;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryBuilder;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gwtorm.client.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.lib.AbbreviatedObjectId;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.regex.Pattern;
+
+/**
+ * Parses a query string meant to be applied to change objects.
+ */
+public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
+  private static final Pattern PAT_LEGACY_ID = Pattern.compile("^[1-9][0-9]*$");
+  private static final Pattern PAT_CHANGE_ID =
+      Pattern.compile("^[iI][0-9a-f]{4,}.*$");
+  private static final Pattern DEF_CHANGE =
+      Pattern.compile("^([1-9][0-9]*|[iI][0-9a-f]{4,}.*)$");
+
+  private static final Pattern PAT_COMMIT =
+      Pattern.compile("^([0-9a-fA-F]{4," + RevId.LEN + "})$");
+  private static final Pattern PAT_EMAIL =
+      Pattern.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+");
+
+  private static final Pattern PAT_LABEL =
+      Pattern.compile("^[a-zA-Z][a-zA-Z0-9]*((=|>=|<=)[+-]?|[+-])\\d+$");
+
+  public static final String FIELD_AGE = "age";
+  public static final String FIELD_BRANCH = "branch";
+  public static final String FIELD_CHANGE = "change";
+  public static final String FIELD_COMMIT = "commit";
+  public static final String FIELD_DRAFTBY = "draftby";
+  public static final String FIELD_FILE = "file";
+  public static final String FIELD_IS = "is";
+  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_OWNER = "owner";
+  public static final String FIELD_PROJECT = "project";
+  public static final String FIELD_REF = "ref";
+  public static final String FIELD_REVIEWER = "reviewer";
+  public static final String FIELD_STARREDBY = "starredby";
+  public static final String FIELD_STATUS = "status";
+  public static final String FIELD_TOPIC = "topic";
+  public static final String FIELD_TR = "tr";
+  public static final String FIELD_VISIBLETO = "visibleto";
+  public static final String FIELD_WATCHEDBY = "watchedby";
+
+  private static final QueryBuilder.Definition<ChangeData, ChangeQueryBuilder> mydef =
+      new QueryBuilder.Definition<ChangeData, ChangeQueryBuilder>(
+          ChangeQueryBuilder.class);
+
+  static class Arguments {
+    final Provider<ReviewDb> dbProvider;
+    final Provider<ChangeQueryRewriter> rewriter;
+    final IdentifiedUser.GenericFactory userFactory;
+    final ChangeControl.Factory changeControlFactory;
+    final ChangeControl.GenericFactory changeControlGenericFactory;
+    final AccountResolver accountResolver;
+    final GroupCache groupCache;
+    final AuthConfig authConfig;
+    final ApprovalTypes approvalTypes;
+    final Project.NameKey wildProjectName;
+    final PatchListCache patchListCache;
+
+    @Inject
+    Arguments(Provider<ReviewDb> dbProvider,
+        Provider<ChangeQueryRewriter> rewriter,
+        IdentifiedUser.GenericFactory userFactory,
+        ChangeControl.Factory changeControlFactory,
+        ChangeControl.GenericFactory changeControlGenericFactory,
+        AccountResolver accountResolver, GroupCache groupCache,
+        AuthConfig authConfig, ApprovalTypes approvalTypes,
+        @WildProjectName Project.NameKey wildProjectName,
+        PatchListCache patchListCache) {
+      this.dbProvider = dbProvider;
+      this.rewriter = rewriter;
+      this.userFactory = userFactory;
+      this.changeControlFactory = changeControlFactory;
+      this.changeControlGenericFactory = changeControlGenericFactory;
+      this.accountResolver = accountResolver;
+      this.groupCache = groupCache;
+      this.authConfig = authConfig;
+      this.approvalTypes = approvalTypes;
+      this.wildProjectName = wildProjectName;
+      this.patchListCache = patchListCache;
+    }
+  }
+
+  public interface Factory {
+    ChangeQueryBuilder create(CurrentUser user);
+  }
+
+  private final Arguments args;
+  private final CurrentUser currentUser;
+  private boolean allowsFile;
+
+  @Inject
+  ChangeQueryBuilder(Arguments args, @Assisted CurrentUser currentUser) {
+    super(mydef);
+    this.args = args;
+    this.currentUser = currentUser;
+  }
+
+  public void setAllowFile(boolean on) {
+    allowsFile = on;
+  }
+
+  @Operator
+  public Predicate<ChangeData> age(String value) {
+    return new AgePredicate(args.dbProvider, value);
+  }
+
+  @Operator
+  public Predicate<ChangeData> change(String query) {
+    if (PAT_LEGACY_ID.matcher(query).matches()) {
+      return new LegacyChangeIdPredicate(args.dbProvider, Change.Id
+          .parse(query));
+
+    } else if (PAT_CHANGE_ID.matcher(query).matches()) {
+      if (query.charAt(0) == 'i') {
+        query = "I" + query.substring(1);
+      }
+      return new ChangeIdPredicate(args.dbProvider, query);
+    }
+
+    throw new IllegalArgumentException();
+  }
+
+  @Operator
+  public Predicate<ChangeData> status(String statusName) {
+    if ("open".equals(statusName)) {
+      return status_open();
+
+    } else if ("closed".equals(statusName)) {
+      return ChangeStatusPredicate.closed(args.dbProvider);
+
+    } else if ("reviewed".equalsIgnoreCase(statusName)) {
+      return new IsReviewedPredicate(args.dbProvider);
+
+    } else {
+      return new ChangeStatusPredicate(args.dbProvider, statusName);
+    }
+  }
+
+  public Predicate<ChangeData> status_open() {
+    return ChangeStatusPredicate.open(args.dbProvider);
+  }
+
+  @Operator
+  public Predicate<ChangeData> has(String value) {
+    if ("star".equalsIgnoreCase(value)) {
+      return new IsStarredByPredicate(args.dbProvider, currentUser);
+    }
+
+    if ("draft".equalsIgnoreCase(value)) {
+      if (currentUser instanceof IdentifiedUser) {
+        return new HasDraftByPredicate(args.dbProvider,
+            ((IdentifiedUser) currentUser).getAccountId());
+      }
+    }
+
+    throw new IllegalArgumentException();
+  }
+
+  @Operator
+  public Predicate<ChangeData> is(String value) {
+    if ("starred".equalsIgnoreCase(value)) {
+      return new IsStarredByPredicate(args.dbProvider, currentUser);
+    }
+
+    if ("watched".equalsIgnoreCase(value)) {
+      return new IsWatchedByPredicate(args, currentUser);
+    }
+
+    if ("visible".equalsIgnoreCase(value)) {
+      return is_visible();
+    }
+
+    if ("reviewed".equalsIgnoreCase(value)) {
+      return new IsReviewedPredicate(args.dbProvider);
+    }
+
+    try {
+      return status(value);
+    } catch (IllegalArgumentException e) {
+      // not status: alias?
+    }
+
+    throw new IllegalArgumentException();
+  }
+
+  @Operator
+  public Predicate<ChangeData> commit(String id) {
+    return new CommitPredicate(args.dbProvider, AbbreviatedObjectId
+        .fromString(id));
+  }
+
+  @Operator
+  public Predicate<ChangeData> project(String name) {
+    return new ProjectPredicate(args.dbProvider, name);
+  }
+
+  @Operator
+  public Predicate<ChangeData> branch(String name) {
+    return new BranchPredicate(args.dbProvider, name);
+  }
+
+  @Operator
+  public Predicate<ChangeData> topic(String name) {
+    return new TopicPredicate(args.dbProvider, name);
+  }
+
+  @Operator
+  public Predicate<ChangeData> ref(String ref) {
+    return new RefPredicate(args.dbProvider, ref);
+  }
+
+  @Operator
+  public Predicate<ChangeData> file(String file) throws QueryParseException {
+    if (!allowsFile) {
+      throw error("operator not permitted here: file:" + file);
+    }
+
+    if (file.startsWith("^")) {
+      return new RegexFilePredicate(args.dbProvider, args.patchListCache, file);
+    }
+
+    throw new IllegalArgumentException();
+  }
+
+  @Operator
+  public Predicate<ChangeData> label(String name) {
+    return new LabelPredicate(args.changeControlGenericFactory,
+        args.userFactory, args.dbProvider, args.approvalTypes, name);
+  }
+
+  @Operator
+  public Predicate<ChangeData> starredby(String who)
+      throws QueryParseException, OrmException {
+    Account account = args.accountResolver.find(who);
+    if (account == null) {
+      throw error("User " + who + " not found");
+    }
+    return new IsStarredByPredicate(args.dbProvider, //
+        args.userFactory.create(args.dbProvider, account.getId()));
+  }
+
+  @Operator
+  public Predicate<ChangeData> watchedby(String who)
+      throws QueryParseException, OrmException {
+    Account account = args.accountResolver.find(who);
+    if (account == null) {
+      throw error("User " + who + " not found");
+    }
+    return new IsWatchedByPredicate(args, args.userFactory.create(
+        args.dbProvider, account.getId()));
+  }
+
+  @Operator
+  public Predicate<ChangeData> draftby(String who) throws QueryParseException,
+      OrmException {
+    Account account = args.accountResolver.find(who);
+    if (account == null) {
+      throw error("User " + who + " not found");
+    }
+    return new HasDraftByPredicate(args.dbProvider, account.getId());
+  }
+
+  @Operator
+  public Predicate<ChangeData> visibleto(String who)
+      throws QueryParseException, OrmException {
+    Account account = args.accountResolver.find(who);
+    if (account != null) {
+      return visibleto(args.userFactory
+          .create(args.dbProvider, account.getId()));
+    }
+
+    // If its not an account, maybe its a group?
+    //
+    AccountGroup g = args.groupCache.get(new AccountGroup.NameKey(who));
+    if (g != null) {
+      return visibleto(new SingleGroupUser(args.authConfig, g.getId()));
+    }
+
+    Collection<AccountGroup> matches =
+        args.groupCache.get(new AccountGroup.ExternalNameKey(who));
+    if (matches != null && !matches.isEmpty()) {
+      HashSet<AccountGroup.Id> ids = new HashSet<AccountGroup.Id>();
+      for (AccountGroup group : matches) {
+        ids.add(group.getId());
+      }
+      return visibleto(new SingleGroupUser(args.authConfig, ids));
+    }
+
+    throw error("No user or group matches \"" + who + "\".");
+  }
+
+  public Predicate<ChangeData> visibleto(CurrentUser user) {
+    return new IsVisibleToPredicate(args.dbProvider, //
+        args.changeControlFactory, //
+        user);
+  }
+
+  public Predicate<ChangeData> is_visible() {
+    return visibleto(currentUser);
+  }
+
+  @Operator
+  public Predicate<ChangeData> owner(String who) throws QueryParseException,
+      OrmException {
+    Account account = args.accountResolver.find(who);
+    if (account == null) {
+      throw error("User " + who + " not found");
+    }
+    return new OwnerPredicate(args.dbProvider, account.getId());
+  }
+
+  @Operator
+  public Predicate<ChangeData> reviewer(String nameOrEmail)
+      throws QueryParseException, OrmException {
+    Account account = args.accountResolver.find(nameOrEmail);
+    if (account == null) {
+      throw error("Reviewer " + nameOrEmail + " not found");
+    }
+    return new ReviewerPredicate(args.dbProvider, account.getId());
+  }
+
+  @Operator
+  public Predicate<ChangeData> tr(String trackingId) {
+    return new TrackingIdPredicate(args.dbProvider, trackingId);
+  }
+
+  @Operator
+  public Predicate<ChangeData> bug(String trackingId) {
+    return tr(trackingId);
+  }
+
+  @Operator
+  public Predicate<ChangeData> limit(String limit) {
+    return limit(Integer.parseInt(limit));
+  }
+
+  public Predicate<ChangeData> limit(int limit) {
+    return new IntPredicate<ChangeData>(FIELD_LIMIT, limit) {
+      @Override
+      public boolean match(ChangeData object) {
+        return true;
+      }
+
+      @Override
+      public int getCost() {
+        return 0;
+      }
+    };
+  }
+
+  @Operator
+  public Predicate<ChangeData> sortkey_after(String sortKey) {
+    return new SortKeyPredicate.After(args.dbProvider, sortKey);
+  }
+
+  @Operator
+  public Predicate<ChangeData> sortkey_before(String sortKey) {
+    return new SortKeyPredicate.Before(args.dbProvider, sortKey);
+  }
+
+  @Operator
+  public Predicate<ChangeData> resume_sortkey(String sortKey) {
+    return sortkey_before(sortKey);
+  }
+
+  @SuppressWarnings("unchecked")
+  public boolean hasLimit(Predicate<ChangeData> p) {
+    return find(p, IntPredicate.class, FIELD_LIMIT) != null;
+  }
+
+  @SuppressWarnings("unchecked")
+  public int getLimit(Predicate<ChangeData> p) {
+    return ((IntPredicate) find(p, IntPredicate.class, FIELD_LIMIT)).intValue();
+  }
+
+  @SuppressWarnings("unchecked")
+  public boolean hasSortKey(Predicate<ChangeData> p) {
+    return find(p, SortKeyPredicate.class, "sortkey_after") != null
+        || find(p, SortKeyPredicate.class, "sortkey_before") != null;
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  protected Predicate<ChangeData> defaultField(String query)
+      throws QueryParseException {
+    if (query.startsWith("refs/")) {
+      return ref(query);
+
+    } else if (DEF_CHANGE.matcher(query).matches()) {
+      return change(query);
+
+    } else if (PAT_COMMIT.matcher(query).matches()) {
+      return commit(query);
+
+    } else if (PAT_EMAIL.matcher(query).find()) {
+      try {
+        return Predicate.or(owner(query), reviewer(query));
+      } catch (OrmException err) {
+        throw error("Cannot lookup user", err);
+      }
+
+    } else if (PAT_LABEL.matcher(query).find()) {
+      return label(query);
+
+    } else {
+      throw error("Unsupported query:" + query);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java
new file mode 100644
index 0000000..98b12f7
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java
@@ -0,0 +1,601 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.ChangeAccess;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.query.IntPredicate;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryRewriter;
+import com.google.gerrit.server.query.RewritePredicate;
+import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.client.ResultSet;
+import com.google.inject.Inject;
+import com.google.inject.OutOfScopeException;
+import com.google.inject.Provider;
+import com.google.inject.name.Named;
+
+import java.util.Collection;
+
+public class ChangeQueryRewriter extends QueryRewriter<ChangeData> {
+  private static final QueryRewriter.Definition<ChangeData, ChangeQueryRewriter> mydef =
+      new QueryRewriter.Definition<ChangeData, ChangeQueryRewriter>(
+          ChangeQueryRewriter.class, new ChangeQueryBuilder(
+              new ChangeQueryBuilder.Arguments( //
+                  new InvalidProvider<ReviewDb>(), //
+                  new InvalidProvider<ChangeQueryRewriter>(), //
+                  null, null, null, null, null, null, null, null, null),
+              null));
+
+  private final Provider<ReviewDb> dbProvider;
+
+  @Inject
+  ChangeQueryRewriter(Provider<ReviewDb> dbProvider) {
+    super(mydef);
+    this.dbProvider = dbProvider;
+  }
+
+  @Override
+  public Predicate<ChangeData> and(Collection<? extends Predicate<ChangeData>> l) {
+    return hasSource(l) ? new AndSource(l) : super.and(l);
+  }
+
+  @Override
+  public Predicate<ChangeData> or(Collection<? extends Predicate<ChangeData>> l) {
+    return hasSource(l) ? new OrSource(l) : super.or(l);
+  }
+
+  @Rewrite("-status:open")
+  @NoCostComputation
+  public Predicate<ChangeData> r00_notOpen() {
+    return ChangeStatusPredicate.closed(dbProvider);
+  }
+
+  @Rewrite("-status:closed")
+  @NoCostComputation
+  public Predicate<ChangeData> r00_notClosed() {
+    return ChangeStatusPredicate.open(dbProvider);
+  }
+
+  @SuppressWarnings("unchecked")
+  @NoCostComputation
+  @Rewrite("-status:merged")
+  public Predicate<ChangeData> r00_notMerged() {
+    return or(ChangeStatusPredicate.open(dbProvider),
+        new ChangeStatusPredicate(dbProvider, Change.Status.ABANDONED));
+  }
+
+  @SuppressWarnings("unchecked")
+  @NoCostComputation
+  @Rewrite("-status:abandoned")
+  public Predicate<ChangeData> r00_notAbandoned() {
+    return or(ChangeStatusPredicate.open(dbProvider),
+        new ChangeStatusPredicate(dbProvider, Change.Status.MERGED));
+  }
+
+  @SuppressWarnings("unchecked")
+  @NoCostComputation
+  @Rewrite("sortkey_before:z A=(age:*)")
+  public Predicate<ChangeData> r00_ageToSortKey(@Named("A") AgePredicate a) {
+    String cut = ChangeUtil.sortKey(a.getCut(), Integer.MAX_VALUE);
+    return and(new SortKeyPredicate.Before(dbProvider, cut), a);
+  }
+
+  @SuppressWarnings("unchecked")
+  @NoCostComputation
+  @Rewrite("A=(limit:*) B=(limit:*)")
+  public Predicate<ChangeData> r00_smallestLimit(
+      @Named("A") IntPredicate<ChangeData> a,
+      @Named("B") IntPredicate<ChangeData> b) {
+    return a.intValue() <= b.intValue() ? a : b;
+  }
+
+  @SuppressWarnings("unchecked")
+  @NoCostComputation
+  @Rewrite("A=(sortkey_before:*) B=(sortkey_before:*)")
+  public Predicate<ChangeData> r00_oldestSortKey(
+      @Named("A") SortKeyPredicate.Before a,
+      @Named("B") SortKeyPredicate.Before b) {
+    return a.getValue().compareTo(b.getValue()) <= 0 ? a : b;
+  }
+
+  @SuppressWarnings("unchecked")
+  @NoCostComputation
+  @Rewrite("A=(sortkey_after:*) B=(sortkey_after:*)")
+  public Predicate<ChangeData> r00_newestSortKey(
+      @Named("A") SortKeyPredicate.After a, @Named("B") SortKeyPredicate.After b) {
+    return a.getValue().compareTo(b.getValue()) >= 0 ? a : b;
+  }
+
+  @Rewrite("status:open P=(project:*) S=(sortkey_after:*) L=(limit:*)")
+  public Predicate<ChangeData> r10_byProjectOpenPrev(
+      @Named("P") final ProjectPredicate p,
+      @Named("S") final SortKeyPredicate.After s,
+      @Named("L") final IntPredicate<ChangeData> l) {
+    return new PaginatedSource(500, s.getValue(), l.intValue()) {
+      @Override
+      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+          throws OrmException {
+        return a.byProjectOpenPrev(p.getValueKey(), key, limit);
+      }
+
+      @Override
+      public boolean match(ChangeData cd) throws OrmException {
+        return cd.change(dbProvider).getStatus().isOpen() //
+            && p.match(cd) //
+            && s.match(cd);
+      }
+    };
+  }
+
+  @Rewrite("status:open P=(project:*) S=(sortkey_before:*) L=(limit:*)")
+  public Predicate<ChangeData> r10_byProjectOpenNext(
+      @Named("P") final ProjectPredicate p,
+      @Named("S") final SortKeyPredicate.Before s,
+      @Named("L") final IntPredicate<ChangeData> l) {
+    return new PaginatedSource(500, s.getValue(), l.intValue()) {
+      @Override
+      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+          throws OrmException {
+        return a.byProjectOpenNext(p.getValueKey(), key, limit);
+      }
+
+      @Override
+      public boolean match(ChangeData cd) throws OrmException {
+        return cd.change(dbProvider).getStatus().isOpen() //
+            && p.match(cd) //
+            && s.match(cd);
+      }
+    };
+  }
+
+  @Rewrite("status:merged P=(project:*) S=(sortkey_after:*) L=(limit:*)")
+  public Predicate<ChangeData> r10_byProjectMergedPrev(
+      @Named("P") final ProjectPredicate p,
+      @Named("S") final SortKeyPredicate.After s,
+      @Named("L") final IntPredicate<ChangeData> l) {
+    return new PaginatedSource(40000, s.getValue(), l.intValue()) {
+      @Override
+      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+          throws OrmException {
+        return a.byProjectClosedPrev(Change.Status.MERGED.getCode(), //
+            p.getValueKey(), key, limit);
+      }
+
+      @Override
+      public boolean match(ChangeData cd) throws OrmException {
+        return cd.change(dbProvider).getStatus() == Change.Status.MERGED
+            && p.match(cd) //
+            && s.match(cd);
+      }
+    };
+  }
+
+  @Rewrite("status:merged P=(project:*) S=(sortkey_before:*) L=(limit:*)")
+  public Predicate<ChangeData> r10_byProjectMergedNext(
+      @Named("P") final ProjectPredicate p,
+      @Named("S") final SortKeyPredicate.Before s,
+      @Named("L") final IntPredicate<ChangeData> l) {
+    return new PaginatedSource(40000, s.getValue(), l.intValue()) {
+      @Override
+      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+          throws OrmException {
+        return a.byProjectClosedNext(Change.Status.MERGED.getCode(), //
+            p.getValueKey(), key, limit);
+      }
+
+      @Override
+      public boolean match(ChangeData cd) throws OrmException {
+        return cd.change(dbProvider).getStatus() == Change.Status.MERGED
+            && p.match(cd) //
+            && s.match(cd);
+      }
+    };
+  }
+
+  @Rewrite("status:abandoned P=(project:*) S=(sortkey_after:*) L=(limit:*)")
+  public Predicate<ChangeData> r10_byProjectAbandonedPrev(
+      @Named("P") final ProjectPredicate p,
+      @Named("S") final SortKeyPredicate.After s,
+      @Named("L") final IntPredicate<ChangeData> l) {
+    return new PaginatedSource(40000, s.getValue(), l.intValue()) {
+      @Override
+      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+          throws OrmException {
+        return a.byProjectClosedPrev(Change.Status.ABANDONED.getCode(), //
+            p.getValueKey(), key, limit);
+      }
+
+      @Override
+      public boolean match(ChangeData cd) throws OrmException {
+        return cd.change(dbProvider).getStatus() == Change.Status.ABANDONED
+            && p.match(cd) //
+            && s.match(cd);
+      }
+    };
+  }
+
+  @Rewrite("status:abandoned P=(project:*) S=(sortkey_before:*) L=(limit:*)")
+  public Predicate<ChangeData> r10_byProjectAbandonedNext(
+      @Named("P") final ProjectPredicate p,
+      @Named("S") final SortKeyPredicate.Before s,
+      @Named("L") final IntPredicate<ChangeData> l) {
+    return new PaginatedSource(40000, s.getValue(), l.intValue()) {
+      @Override
+      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+          throws OrmException {
+        return a.byProjectClosedNext(Change.Status.ABANDONED.getCode(), //
+            p.getValueKey(), key, limit);
+      }
+
+      @Override
+      public boolean match(ChangeData cd) throws OrmException {
+        return cd.change(dbProvider).getStatus() == Change.Status.ABANDONED
+            && p.match(cd) //
+            && s.match(cd);
+      }
+    };
+  }
+
+  @Rewrite("status:open S=(sortkey_after:*) L=(limit:*)")
+  public Predicate<ChangeData> r20_byOpenPrev(
+      @Named("S") final SortKeyPredicate.After s,
+      @Named("L") final IntPredicate<ChangeData> l) {
+    return new PaginatedSource(2000, s.getValue(), l.intValue()) {
+      @Override
+      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+          throws OrmException {
+        return a.allOpenPrev(key, limit);
+      }
+
+      @Override
+      public boolean match(ChangeData cd) throws OrmException {
+        return cd.change(dbProvider).getStatus().isOpen() && s.match(cd);
+      }
+    };
+  }
+
+  @Rewrite("status:open S=(sortkey_before:*) L=(limit:*)")
+  public Predicate<ChangeData> r20_byOpenNext(
+      @Named("S") final SortKeyPredicate.Before s,
+      @Named("L") final IntPredicate<ChangeData> l) {
+    return new PaginatedSource(2000, s.getValue(), l.intValue()) {
+      @Override
+      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+          throws OrmException {
+        return a.allOpenNext(key, limit);
+      }
+
+      @Override
+      public boolean match(ChangeData cd) throws OrmException {
+        return cd.change(dbProvider).getStatus().isOpen() && s.match(cd);
+      }
+    };
+  }
+
+  @Rewrite("status:merged S=(sortkey_after:*) L=(limit:*)")
+  public Predicate<ChangeData> r20_byMergedPrev(
+      @Named("S") final SortKeyPredicate.After s,
+      @Named("L") final IntPredicate<ChangeData> l) {
+    return new PaginatedSource(50000, s.getValue(), l.intValue()) {
+      @Override
+      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+          throws OrmException {
+        return a.allClosedPrev(Change.Status.MERGED.getCode(), key, limit);
+      }
+
+      @Override
+      public boolean match(ChangeData cd) throws OrmException {
+        return cd.change(dbProvider).getStatus() == Change.Status.MERGED
+            && s.match(cd);
+      }
+    };
+  }
+
+  @Rewrite("status:merged S=(sortkey_before:*) L=(limit:*)")
+  public Predicate<ChangeData> r20_byMergedNext(
+      @Named("S") final SortKeyPredicate.Before s,
+      @Named("L") final IntPredicate<ChangeData> l) {
+    return new PaginatedSource(50000, s.getValue(), l.intValue()) {
+      @Override
+      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+          throws OrmException {
+        return a.allClosedNext(Change.Status.MERGED.getCode(), key, limit);
+      }
+
+      @Override
+      public boolean match(ChangeData cd) throws OrmException {
+        return cd.change(dbProvider).getStatus() == Change.Status.MERGED
+            && s.match(cd);
+      }
+    };
+  }
+
+  @Rewrite("status:abandoned S=(sortkey_after:*) L=(limit:*)")
+  public Predicate<ChangeData> r20_byAbandonedPrev(
+      @Named("S") final SortKeyPredicate.After s,
+      @Named("L") final IntPredicate<ChangeData> l) {
+    return new PaginatedSource(50000, s.getValue(), l.intValue()) {
+      @Override
+      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+          throws OrmException {
+        return a.allClosedPrev(Change.Status.ABANDONED.getCode(), key, limit);
+      }
+
+      @Override
+      public boolean match(ChangeData cd) throws OrmException {
+        return cd.change(dbProvider).getStatus() == Change.Status.ABANDONED
+            && s.match(cd);
+      }
+    };
+  }
+
+  @Rewrite("status:abandoned S=(sortkey_before:*) L=(limit:*)")
+  public Predicate<ChangeData> r20_byAbandonedNext(
+      @Named("S") final SortKeyPredicate.Before s,
+      @Named("L") final IntPredicate<ChangeData> l) {
+    return new PaginatedSource(50000, s.getValue(), l.intValue()) {
+      @Override
+      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+          throws OrmException {
+        return a.allClosedNext(Change.Status.ABANDONED.getCode(), key, limit);
+      }
+
+      @Override
+      public boolean match(ChangeData cd) throws OrmException {
+        return cd.change(dbProvider).getStatus() == Change.Status.ABANDONED
+            && s.match(cd);
+      }
+    };
+  }
+
+  @SuppressWarnings("unchecked")
+  @Rewrite("status:closed S=(sortkey_after:*) L=(limit:*)")
+  public Predicate<ChangeData> r20_byClosedPrev(
+      @Named("S") final SortKeyPredicate.After s,
+      @Named("L") final IntPredicate<ChangeData> l) {
+    return or(r20_byMergedPrev(s, l), r20_byAbandonedPrev(s, l));
+  }
+
+  @SuppressWarnings("unchecked")
+  @Rewrite("status:closed S=(sortkey_after:*) L=(limit:*)")
+  public Predicate<ChangeData> r20_byClosedNext(
+      @Named("S") final SortKeyPredicate.Before s,
+      @Named("L") final IntPredicate<ChangeData> l) {
+    return or(r20_byMergedNext(s, l), r20_byAbandonedNext(s, l));
+  }
+
+  @Rewrite("status:open O=(owner:*)")
+  public Predicate<ChangeData> r25_byOwnerOpen(
+      @Named("O") final OwnerPredicate o) {
+    return new ChangeSource(50) {
+      @Override
+      ResultSet<Change> scan(ChangeAccess a) throws OrmException {
+        return a.byOwnerOpen(o.getAccountId());
+      }
+
+      @Override
+      public boolean match(ChangeData cd) throws OrmException {
+        return cd.change(dbProvider).getStatus().isOpen() && o.match(cd);
+      }
+    };
+  }
+
+  @Rewrite("status:closed O=(owner:*)")
+  public Predicate<ChangeData> r25_byOwnerClosed(
+      @Named("O") final OwnerPredicate o) {
+    return new ChangeSource(5000) {
+      @Override
+      ResultSet<Change> scan(ChangeAccess a) throws OrmException {
+        return a.byOwnerClosedAll(o.getAccountId());
+      }
+
+      @Override
+      public boolean match(ChangeData cd) throws OrmException {
+        return cd.change(dbProvider).getStatus().isClosed() && o.match(cd);
+      }
+    };
+  }
+
+  @SuppressWarnings("unchecked")
+  @Rewrite("O=(owner:*)")
+  public Predicate<ChangeData> r26_byOwner(@Named("O") OwnerPredicate o) {
+    return or(r25_byOwnerOpen(o), r25_byOwnerClosed(o));
+  }
+
+  @Rewrite("status:open R=(reviewer:*)")
+  public Predicate<ChangeData> r30_byReviewerOpen(
+      @Named("R") final ReviewerPredicate r) {
+    return new Source() {
+      @Override
+      public ResultSet<ChangeData> read() throws OrmException {
+        return ChangeDataResultSet.patchSetApproval(dbProvider.get()
+            .patchSetApprovals().openByUser(r.getAccountId()));
+      }
+
+      @Override
+      public boolean match(ChangeData cd) throws OrmException {
+        Change change = cd.change(dbProvider);
+        return change != null && change.getStatus().isOpen() && r.match(cd);
+      }
+
+      @Override
+      public int getCardinality() {
+        return 50;
+      }
+
+      @Override
+      public int getCost() {
+        return ChangeCosts.cost(ChangeCosts.APPROVALS_SCAN, getCardinality());
+      }
+    };
+  }
+
+  @Rewrite("status:closed R=(reviewer:*)")
+  public Predicate<ChangeData> r30_byReviewerClosed(
+      @Named("R") final ReviewerPredicate r) {
+    return new Source() {
+      @Override
+      public ResultSet<ChangeData> read() throws OrmException {
+        return ChangeDataResultSet.patchSetApproval(dbProvider.get()
+            .patchSetApprovals().closedByUserAll(r.getAccountId()));
+      }
+
+      @Override
+      public boolean match(ChangeData cd) throws OrmException {
+        Change change = cd.change(dbProvider);
+        return change != null && change.getStatus().isClosed() && r.match(cd);
+      }
+
+      @Override
+      public int getCardinality() {
+        return 5000;
+      }
+
+      @Override
+      public int getCost() {
+        return ChangeCosts.cost(ChangeCosts.APPROVALS_SCAN, getCardinality());
+      }
+    };
+  }
+
+  @SuppressWarnings("unchecked")
+  @Rewrite("R=(reviewer:*)")
+  public Predicate<ChangeData> r31_byReviewer(
+      @Named("R") final ReviewerPredicate r) {
+    return or(r30_byReviewerOpen(r), r30_byReviewerClosed(r));
+  }
+
+  @SuppressWarnings("unchecked")
+  @Rewrite("status:submitted")
+  public Predicate<ChangeData> r99_allSubmitted() {
+    return new ChangeSource(50) {
+      @Override
+      ResultSet<Change> scan(ChangeAccess a) throws OrmException {
+        return a.allSubmitted();
+      }
+
+      @Override
+      public boolean match(ChangeData cd) throws OrmException {
+        return cd.change(dbProvider).getStatus() == Change.Status.SUBMITTED;
+      }
+    };
+  }
+
+  @SuppressWarnings("unchecked")
+  @Rewrite("P=(project:*)")
+  public Predicate<ChangeData> r99_byProject(
+      @Named("P") final ProjectPredicate p) {
+    return new ChangeSource(1000000) {
+      @Override
+      ResultSet<Change> scan(ChangeAccess a) throws OrmException {
+        return a.byProject(p.getValueKey());
+      }
+
+      @Override
+      public boolean match(ChangeData cd) throws OrmException {
+        return p.match(cd);
+      }
+    };
+  }
+
+  private static boolean hasSource(Collection<? extends Predicate<ChangeData>> l) {
+    for (Predicate<ChangeData> p : l) {
+      if (p instanceof ChangeDataSource) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private abstract static class Source extends RewritePredicate<ChangeData>
+      implements ChangeDataSource {
+    @Override
+    public boolean hasChange() {
+      return false;
+    }
+  }
+
+  private abstract class ChangeSource extends Source {
+    private final int cardinality;
+
+    ChangeSource(int card) {
+      this.cardinality = card;
+    }
+
+    abstract ResultSet<Change> scan(ChangeAccess a) throws OrmException;
+
+    @Override
+    public ResultSet<ChangeData> read() throws OrmException {
+      return ChangeDataResultSet.change(scan(dbProvider.get().changes()));
+    }
+
+    @Override
+    public boolean hasChange() {
+      return true;
+    }
+
+    @Override
+    public int getCardinality() {
+      return cardinality;
+    }
+
+    @Override
+    public int getCost() {
+      return ChangeCosts.cost(ChangeCosts.CHANGES_SCAN, getCardinality());
+    }
+  }
+
+  private abstract class PaginatedSource extends ChangeSource implements
+      Paginated {
+    private final String startKey;
+    private final int limit;
+
+    PaginatedSource(int card, String start, int lim) {
+      super(card);
+      this.startKey = start;
+      this.limit = lim;
+    }
+
+    @Override
+    public int limit() {
+      return limit;
+    }
+
+    @Override
+    ResultSet<Change> scan(ChangeAccess a) throws OrmException {
+      return scan(a, startKey, limit);
+    }
+
+    @Override
+    public ResultSet<ChangeData> restart(ChangeData last) throws OrmException {
+      return ChangeDataResultSet.change(scan(dbProvider.get().changes(), //
+          last.change(dbProvider).getSortKey(), //
+          limit));
+    }
+
+    abstract ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+        throws OrmException;
+  }
+
+  private static final class InvalidProvider<T> implements Provider<T> {
+    @Override
+    public T get() {
+      throw new OutOfScopeException("Not available at init");
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
new file mode 100644
index 0000000..4ae2278
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
@@ -0,0 +1,126 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gwtorm.client.OrmException;
+import com.google.inject.Provider;
+
+import java.util.ArrayList;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+
+/**
+ * Predicate for a {@link Change.Status}.
+ * <p>
+ * The actual name of this operator can differ, it usually comes as {@code
+ * status:} but may also be {@code is:} to help do-what-i-meanery for end-users
+ * searching for changes. Either operator name has the same meaning.
+ */
+final class ChangeStatusPredicate extends OperatorPredicate<ChangeData> {
+  private static final Map<String, Change.Status> byName;
+  private static final EnumMap<Change.Status, String> byEnum;
+
+  static {
+    byName = new HashMap<String, Change.Status>();
+    byEnum = new EnumMap<Change.Status, String>(Change.Status.class);
+    for (final Change.Status s : Change.Status.values()) {
+      final String name = s.name().toLowerCase();
+      byName.put(name, s);
+      byEnum.put(s, name);
+    }
+  }
+
+  static Predicate<ChangeData> open(Provider<ReviewDb> dbProvider) {
+    List<Predicate<ChangeData>> r = new ArrayList<Predicate<ChangeData>>(4);
+    for (final Change.Status e : Change.Status.values()) {
+      if (e.isOpen()) {
+        r.add(new ChangeStatusPredicate(dbProvider, e));
+      }
+    }
+    return r.size() == 1 ? r.get(0) : or(r);
+  }
+
+  static Predicate<ChangeData> closed(Provider<ReviewDb> dbProvider) {
+    List<Predicate<ChangeData>> r = new ArrayList<Predicate<ChangeData>>(4);
+    for (final Change.Status e : Change.Status.values()) {
+      if (e.isClosed()) {
+        r.add(new ChangeStatusPredicate(dbProvider, e));
+      }
+    }
+    return r.size() == 1 ? r.get(0) : or(r);
+  }
+
+  private static Change.Status parse(final String value) {
+    final Change.Status s = byName.get(value);
+    if (s == null) {
+      throw new IllegalArgumentException();
+    }
+    return s;
+  }
+
+  private final Provider<ReviewDb> dbProvider;
+  private final Change.Status status;
+
+  ChangeStatusPredicate(Provider<ReviewDb> dbProvider, String value) {
+    this(dbProvider, parse(value));
+  }
+
+  ChangeStatusPredicate(Provider<ReviewDb> dbProvider, Change.Status status) {
+    super(ChangeQueryBuilder.FIELD_STATUS, byEnum.get(status));
+    this.dbProvider = dbProvider;
+    this.status = status;
+  }
+
+  Change.Status getStatus() {
+    return status;
+  }
+
+  @Override
+  public boolean match(final ChangeData object) throws OrmException {
+    Change change = object.change(dbProvider);
+    return change != null && status.equals(change.getStatus());
+  }
+
+  @Override
+  public int getCost() {
+    return 0;
+  }
+
+  @Override
+  public int hashCode() {
+    return status.hashCode();
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (other instanceof ChangeStatusPredicate) {
+      final ChangeStatusPredicate p = (ChangeStatusPredicate) other;
+      return status.equals(p.status);
+    }
+    return false;
+  }
+
+  @Override
+  public String toString() {
+    return getOperator() + ":" + getValue();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java
new file mode 100644
index 0000000..c03cddc
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java
@@ -0,0 +1,77 @@
+// 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.PatchSet;
+import com.google.gerrit.reviewdb.RevId;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.query.ObjectIdPredicate;
+import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.client.ResultSet;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.lib.AbbreviatedObjectId;
+import org.eclipse.jgit.lib.ObjectId;
+
+class CommitPredicate extends ObjectIdPredicate<ChangeData> implements
+    ChangeDataSource {
+  private final Provider<ReviewDb> dbProvider;
+
+  CommitPredicate(Provider<ReviewDb> dbProvider, AbbreviatedObjectId id) {
+    super(ChangeQueryBuilder.FIELD_COMMIT, id);
+    this.dbProvider = dbProvider;
+  }
+
+  @Override
+  public boolean match(final ChangeData object) throws OrmException {
+    for (PatchSet p : object.patches(dbProvider)) {
+      if (p.getRevision() != null && p.getRevision().get() != null) {
+        final ObjectId id = ObjectId.fromString(p.getRevision().get());
+        if (abbreviated().prefixCompare(id) == 0) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public ResultSet<ChangeData> read() throws OrmException {
+    final RevId id = new RevId(abbreviated().name());
+    if (id.isComplete()) {
+      return ChangeDataResultSet.patchSet(//
+          dbProvider.get().patchSets().byRevision(id));
+
+    } else {
+      return ChangeDataResultSet.patchSet(//
+          dbProvider.get().patchSets().byRevisionRange(id, id.max()));
+    }
+  }
+
+  @Override
+  public boolean hasChange() {
+    return false;
+  }
+
+  @Override
+  public int getCardinality() {
+    return ChangeCosts.CARD_COMMIT;
+  }
+
+  @Override
+  public int getCost() {
+    return ChangeCosts.cost(ChangeCosts.PATCH_SETS_SCAN, getCardinality());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java
new file mode 100644
index 0000000..07d4dd2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java
@@ -0,0 +1,81 @@
+// 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.Account;
+import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.PatchLineComment;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.client.ResultSet;
+import com.google.gwtorm.client.impl.ListResultSet;
+import com.google.inject.Provider;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+
+class HasDraftByPredicate extends OperatorPredicate<ChangeData> implements
+    ChangeDataSource {
+  private final Provider<ReviewDb> db;
+  private final Account.Id accountId;
+
+  HasDraftByPredicate(Provider<ReviewDb> db, Account.Id accountId) {
+    super(ChangeQueryBuilder.FIELD_DRAFTBY, accountId.toString());
+    this.db = db;
+    this.accountId = accountId;
+  }
+
+  @Override
+  public boolean match(final ChangeData object) throws OrmException {
+    for (PatchLineComment c : object.comments(db)) {
+      if (c.getStatus() == PatchLineComment.Status.DRAFT
+          && c.getAuthor().equals(accountId)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public ResultSet<ChangeData> read() throws OrmException {
+    HashSet<Change.Id> ids = new HashSet<Change.Id>();
+    for (PatchLineComment sc : db.get().patchComments()
+        .draftByAuthor(accountId)) {
+      ids.add(sc.getKey().getParentKey().getParentKey().getParentKey());
+    }
+
+    ArrayList<ChangeData> r = new ArrayList<ChangeData>(ids.size());
+    for (Change.Id id : ids) {
+      r.add(new ChangeData(id));
+    }
+    return new ListResultSet<ChangeData>(r);
+  }
+
+  @Override
+  public boolean hasChange() {
+    return false;
+  }
+
+  @Override
+  public int getCardinality() {
+    return 20;
+  }
+
+  @Override
+  public int getCost() {
+    return ChangeCosts.cost(ChangeCosts.PATCH_SETS_SCAN, getCardinality());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java
new file mode 100644
index 0000000..46c7741
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsReviewedPredicate.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.query.change;
+
+import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.reviewdb.PatchSetApproval;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gwtorm.client.OrmException;
+import com.google.inject.Provider;
+
+class IsReviewedPredicate extends OperatorPredicate<ChangeData> {
+  private final Provider<ReviewDb> dbProvider;
+
+  IsReviewedPredicate(Provider<ReviewDb> dbProvider) {
+    super(ChangeQueryBuilder.FIELD_IS, "reviewed");
+    this.dbProvider = dbProvider;
+  }
+
+  @Override
+  public boolean match(final ChangeData object) throws OrmException {
+    Change c = object.change(dbProvider);
+    if (c == null) {
+      return false;
+    }
+
+    PatchSet.Id current = c.currentPatchSetId();
+    for (PatchSetApproval p : object.approvals(dbProvider)) {
+      if (p.getPatchSetId().equals(current) && p.getValue() != 0) {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  @Override
+  public int getCost() {
+    return 2;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByPredicate.java
new file mode 100644
index 0000000..aaf8478
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByPredicate.java
@@ -0,0 +1,68 @@
+// 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.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.client.ResultSet;
+import com.google.inject.Provider;
+
+class IsStarredByPredicate extends OperatorPredicate<ChangeData> implements
+    ChangeDataSource {
+  private static String describe(CurrentUser user) {
+    if (user instanceof IdentifiedUser) {
+      return ((IdentifiedUser) user).getAccountId().toString();
+    }
+    return user.toString();
+  }
+
+  private final Provider<ReviewDb> db;
+  private final CurrentUser user;
+
+  IsStarredByPredicate(Provider<ReviewDb> db, CurrentUser user) {
+    super(ChangeQueryBuilder.FIELD_STARREDBY, describe(user));
+    this.db = db;
+    this.user = user;
+  }
+
+  @Override
+  public boolean match(final ChangeData object) {
+    return user.getStarredChanges().contains(object.getId());
+  }
+
+  @Override
+  public ResultSet<ChangeData> read() throws OrmException {
+    return ChangeDataResultSet.change( //
+        db.get().changes().get(user.getStarredChanges()));
+  }
+
+  @Override
+  public boolean hasChange() {
+    return true;
+  }
+
+  @Override
+  public int getCardinality() {
+    return 10;
+  }
+
+  @Override
+  public int getCost() {
+    return ChangeCosts.cost(ChangeCosts.IDS_MEMORY, getCardinality());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsVisibleToPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsVisibleToPredicate.java
new file mode 100644
index 0000000..020e709
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsVisibleToPredicate.java
@@ -0,0 +1,73 @@
+// 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.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gwtorm.client.OrmException;
+import com.google.inject.Provider;
+
+class IsVisibleToPredicate extends OperatorPredicate<ChangeData> {
+  private static String describe(CurrentUser user) {
+    if (user instanceof IdentifiedUser) {
+      return ((IdentifiedUser) user).getAccountId().toString();
+    }
+    if (user instanceof SingleGroupUser) {
+      return "group:" + ((SingleGroupUser) user).getEffectiveGroups() //
+          .iterator().next().toString();
+    }
+    return user.toString();
+  }
+
+  private final Provider<ReviewDb> db;
+  private final ChangeControl.Factory changeControl;
+  private final CurrentUser user;
+
+  IsVisibleToPredicate(Provider<ReviewDb> db,
+      ChangeControl.Factory changeControlFactory, CurrentUser user) {
+    super(ChangeQueryBuilder.FIELD_VISIBLETO, describe(user));
+    this.db = db;
+    this.changeControl = changeControlFactory;
+    this.user = user;
+  }
+
+  @Override
+  public boolean match(final ChangeData cd) throws OrmException {
+    if (cd.fastIsVisibleTo(user)) {
+      return true;
+    }
+    try {
+      Change c = cd.change(db);
+      if (c != null && changeControl.controlFor(c).forUser(user).isVisible()) {
+        cd.cacheVisibleTo(user);
+        return true;
+      } else {
+        return false;
+      }
+    } catch (NoSuchChangeException e) {
+      return false;
+    }
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
new file mode 100644
index 0000000..870be73
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
@@ -0,0 +1,121 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.reviewdb.AccountProjectWatch;
+import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gwtorm.client.OrmException;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+class IsWatchedByPredicate extends OperatorPredicate<ChangeData> {
+  private static String describe(CurrentUser user) {
+    if (user instanceof IdentifiedUser) {
+      return ((IdentifiedUser) user).getAccountId().toString();
+    }
+    return user.toString();
+  }
+
+  private final ChangeQueryBuilder.Arguments args;
+  private final CurrentUser user;
+
+  private Map<Project.NameKey, List<Predicate<ChangeData>>> rules;
+
+  IsWatchedByPredicate(ChangeQueryBuilder.Arguments args, CurrentUser user) {
+    super(ChangeQueryBuilder.FIELD_WATCHEDBY, describe(user));
+    this.args = args;
+    this.user = user;
+  }
+
+  @Override
+  public boolean match(final ChangeData cd) throws OrmException {
+    if (rules == null) {
+      ChangeQueryBuilder builder = new ChangeQueryBuilder(args, user);
+      rules = new HashMap<Project.NameKey, List<Predicate<ChangeData>>>();
+      for (AccountProjectWatch w : user.getNotificationFilters()) {
+        List<Predicate<ChangeData>> list = rules.get(w.getProjectNameKey());
+        if (list == null) {
+          list = new ArrayList<Predicate<ChangeData>>(4);
+          rules.put(w.getProjectNameKey(), list);
+        }
+
+        Predicate<ChangeData> p = compile(builder, w);
+        if (p != null) {
+          list.add(p);
+        }
+      }
+    }
+
+    if (rules.isEmpty()) {
+      return false;
+    }
+
+    Change change = cd.change(args.dbProvider);
+    if (change == null) {
+      return false;
+    }
+
+    Project.NameKey project = change.getDest().getParentKey();
+    List<Predicate<ChangeData>> list = rules.get(project);
+    if (list == null) {
+      list = rules.get(args.wildProjectName);
+    }
+    if (list != null) {
+      for (Predicate<ChangeData> p : list) {
+        if (p.match(cd)) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  @SuppressWarnings("unchecked")
+  private Predicate<ChangeData> compile(ChangeQueryBuilder builder,
+      AccountProjectWatch w) {
+    Predicate<ChangeData> p = builder.is_visible();
+    if (w.getFilter() != null) {
+      try {
+        p = Predicate.and(builder.parse(w.getFilter()), p);
+        if (builder.find(p, IsWatchedByPredicate.class) != null) {
+          // If the query is going to infinite loop, assume it
+          // will never match and return null. Yes this test
+          // prevents you from having a filter that matches what
+          // another user is filtering on. :-)
+          //
+          return null;
+        }
+        p = args.rewriter.get().rewrite(p);
+      } catch (QueryParseException e) {
+        return null;
+      }
+    }
+    return p;
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java
new file mode 100644
index 0000000..e76c278
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java
@@ -0,0 +1,172 @@
+// 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.common.data.ApprovalType;
+import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.reviewdb.ApprovalCategory;
+import com.google.gerrit.reviewdb.PatchSetApproval;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gwtorm.client.OrmException;
+import com.google.inject.Provider;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+class LabelPredicate extends OperatorPredicate<ChangeData> {
+  private static enum Test {
+    EQ {
+      @Override
+      public boolean match(short psValue, short expValue) {
+        return psValue == expValue;
+      }
+    },
+    GT_EQ {
+      @Override
+      public boolean match(short psValue, short expValue) {
+        return psValue >= expValue;
+      }
+    },
+    LT_EQ {
+      @Override
+      public boolean match(short psValue, short expValue) {
+        return psValue <= expValue;
+      }
+    };
+
+    abstract boolean match(short psValue, short expValue);
+  }
+
+  private static ApprovalCategory.Id category(ApprovalTypes types, String toFind) {
+    if (types.getApprovalType(new ApprovalCategory.Id(toFind)) != null) {
+      return new ApprovalCategory.Id(toFind);
+    }
+
+    for (ApprovalType at : types.getApprovalTypes()) {
+      String name = at.getCategory().getName();
+      if (toFind.equalsIgnoreCase(name)) {
+        return at.getCategory().getId();
+
+      } else if (toFind.equalsIgnoreCase(name.replace(" ", ""))) {
+        return at.getCategory().getId();
+      }
+    }
+
+    for (ApprovalType at : types.getApprovalTypes()) {
+      if (toFind.equalsIgnoreCase(at.getCategory().getAbbreviatedName())) {
+        return at.getCategory().getId();
+      }
+    }
+
+    return new ApprovalCategory.Id(toFind);
+  }
+
+  private static Test op(String op) {
+    if ("=".equals(op)) {
+      return Test.EQ;
+
+    } else if (">=".equals(op)) {
+      return Test.GT_EQ;
+
+    } else if ("<=".equals(op)) {
+      return Test.LT_EQ;
+
+    } else {
+      throw new IllegalArgumentException("Unsupported operation " + op);
+    }
+  }
+
+  private static short value(String value) {
+    if (value.startsWith("+")) {
+      value = value.substring(1);
+    }
+    return Short.parseShort(value);
+  }
+
+  private final ChangeControl.GenericFactory ccFactory;
+  private final IdentifiedUser.GenericFactory userFactory;
+  private final Provider<ReviewDb> dbProvider;
+  private final Test test;
+  private final ApprovalCategory.Id category;
+  private final short expVal;
+
+  LabelPredicate(ChangeControl.GenericFactory ccFactory,
+      IdentifiedUser.GenericFactory userFactory, Provider<ReviewDb> dbProvider,
+      ApprovalTypes types, String value) {
+    super(ChangeQueryBuilder.FIELD_LABEL, value);
+    this.ccFactory = ccFactory;
+    this.userFactory = userFactory;
+    this.dbProvider = dbProvider;
+
+    Matcher m1 = Pattern.compile("(=|>=|<=)([+-]?\\d+)$").matcher(value);
+    Matcher m2 = Pattern.compile("([+-]\\d+)$").matcher(value);
+    if (m1.find()) {
+      category = category(types, value.substring(0, m1.start()));
+      test = op(m1.group(1));
+      expVal = value(m1.group(2));
+
+    } else if (m2.find()) {
+      category = category(types, value.substring(0, m2.start()));
+      test = Test.EQ;
+      expVal = value(m2.group(1));
+
+    } else {
+      category = category(types, value);
+      test = Test.EQ;
+      expVal = 1;
+    }
+  }
+
+  @Override
+  public boolean match(final ChangeData object) throws OrmException {
+    for (PatchSetApproval p : object.currentApprovals(dbProvider)) {
+      if (p.getCategoryId().equals(category)) {
+        short psVal = p.getValue();
+        if (test.match(psVal, expVal)) {
+          // Double check the value is still permitted for the user.
+          //
+          try {
+            ChangeControl cc = ccFactory.controlFor(object.change(dbProvider), //
+                userFactory.create(dbProvider, p.getAccountId()));
+            if (!cc.isVisible()) {
+              // The user can't see the change anymore.
+              //
+              continue;
+            }
+            psVal = cc.normalize(category, psVal);
+          } catch (NoSuchChangeException e) {
+            // The project has disappeared.
+            //
+            continue;
+          }
+
+          if (test.match(psVal, expVal)) {
+            return true;
+          }
+        }
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public int getCost() {
+    return 2;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java
new file mode 100644
index 0000000..ac544a3
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java
@@ -0,0 +1,68 @@
+// 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.gwtorm.client.ResultSet;
+import com.google.gwtorm.client.impl.ListResultSet;
+import com.google.inject.Provider;
+
+import java.util.Collections;
+
+class LegacyChangeIdPredicate extends OperatorPredicate<ChangeData> implements
+    ChangeDataSource {
+  private final Provider<ReviewDb> db;
+  private final Change.Id id;
+
+  LegacyChangeIdPredicate(Provider<ReviewDb> db, Change.Id id) {
+    super(ChangeQueryBuilder.FIELD_CHANGE, id.toString());
+    this.db = db;
+    this.id = id;
+  }
+
+  @Override
+  public boolean match(final ChangeData object) {
+    return id.equals(object.getId());
+  }
+
+  @Override
+  public ResultSet<ChangeData> read() throws OrmException {
+    Change c = db.get().changes().get(id);
+    if (c != null) {
+      return new ListResultSet<ChangeData>( //
+          Collections.singletonList(new ChangeData(c)));
+    } else {
+      return new ListResultSet<ChangeData>(Collections.<ChangeData> emptyList());
+    }
+  }
+
+  @Override
+  public boolean hasChange() {
+    return true;
+  }
+
+  @Override
+  public int getCardinality() {
+    return 1;
+  }
+
+  @Override
+  public int getCost() {
+    return ChangeCosts.IDS_MEMORY;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OrSource.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OrSource.java
new file mode 100644
index 0000000..617a14a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OrSource.java
@@ -0,0 +1,78 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.server.query.OrPredicate;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.client.ResultSet;
+import com.google.gwtorm.client.impl.ListResultSet;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+
+class OrSource extends OrPredicate<ChangeData> implements ChangeDataSource {
+  private int cardinality = -1;
+
+  OrSource(final Collection<? extends Predicate<ChangeData>> that) {
+    super(that);
+  }
+
+  @Override
+  public ResultSet<ChangeData> read() throws OrmException {
+    // TODO(spearce) This probably should be more lazy.
+    //
+    ArrayList<ChangeData> r = new ArrayList<ChangeData>();
+    HashSet<Change.Id> have = new HashSet<Change.Id>();
+    for (Predicate<ChangeData> p : getChildren()) {
+      if (p instanceof ChangeDataSource) {
+        for (ChangeData cd : ((ChangeDataSource) p).read()) {
+          if (have.add(cd.getId())) {
+            r.add(cd);
+          }
+        }
+      } else {
+        throw new OrmException("No ChangeDataSource: " + p);
+      }
+    }
+    return new ListResultSet<ChangeData>(r);
+  }
+
+  @Override
+  public boolean hasChange() {
+    for (Predicate<ChangeData> p : getChildren()) {
+      if (!(p instanceof ChangeDataSource)
+          || !((ChangeDataSource) p).hasChange()) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  @Override
+  public int getCardinality() {
+    if (cardinality < 0) {
+      cardinality = 0;
+      for (Predicate<ChangeData> p : getChildren()) {
+        if (p instanceof ChangeDataSource) {
+          cardinality += ((ChangeDataSource) p).getCardinality();
+        }
+      }
+    }
+    return cardinality;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerPredicate.java
new file mode 100644
index 0000000..224dce9
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerPredicate.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.server.query.change;
+
+import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gwtorm.client.OrmException;
+import com.google.inject.Provider;
+
+class OwnerPredicate extends OperatorPredicate<ChangeData> {
+  private final Provider<ReviewDb> dbProvider;
+  private final Account.Id id;
+
+  OwnerPredicate(Provider<ReviewDb> dbProvider, Account.Id id) {
+    super(ChangeQueryBuilder.FIELD_OWNER, id.toString());
+    this.dbProvider = dbProvider;
+    this.id = id;
+  }
+
+  Account.Id getAccountId() {
+    return id;
+  }
+
+  @Override
+  public boolean match(final ChangeData object) throws OrmException {
+    Change change = object.change(dbProvider);
+    return change != null && id.equals(change.getOwner());
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/Paginated.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/Paginated.java
new file mode 100644
index 0000000..b046db6
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/Paginated.java
@@ -0,0 +1,24 @@
+// 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.gwtorm.client.OrmException;
+import com.google.gwtorm.client.ResultSet;
+
+interface Paginated {
+  int limit();
+
+  ResultSet<ChangeData> restart(ChangeData last) throws OrmException;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ProjectPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ProjectPredicate.java
new file mode 100644
index 0000000..91203d6
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ProjectPredicate.java
@@ -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.
+
+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;
+
+class ProjectPredicate extends OperatorPredicate<ChangeData> {
+  private final Provider<ReviewDb> dbProvider;
+
+  ProjectPredicate(Provider<ReviewDb> dbProvider, String id) {
+    super(ChangeQueryBuilder.FIELD_PROJECT, id);
+    this.dbProvider = dbProvider;
+  }
+
+  Project.NameKey getValueKey() {
+    return new Project.NameKey(getValue());
+  }
+
+  @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 p.equals(getValueKey());
+  }
+
+  @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
new file mode 100644
index 0000000..3433fa9
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
@@ -0,0 +1,330 @@
+// 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.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.events.ChangeAttribute;
+import com.google.gerrit.server.events.EventFactory;
+import com.google.gerrit.server.events.QueryStats;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gson.Gson;
+import com.google.gwtorm.client.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.util.io.DisabledOutputStream;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.lang.reflect.Field;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+
+public class QueryProcessor {
+  private static final Logger log =
+      LoggerFactory.getLogger(QueryProcessor.class);
+
+  public static enum OutputFormat {
+    TEXT, JSON;
+  }
+
+  private final Gson gson = new Gson();
+  private final SimpleDateFormat sdf =
+      new SimpleDateFormat("yyyy-MM-dd HH:mm:ss zzz");
+
+  private final EventFactory eventFactory;
+  private final ChangeQueryBuilder queryBuilder;
+  private final ChangeQueryRewriter queryRewriter;
+  private final Provider<ReviewDb> db;
+
+  private int defaultLimit = 500;
+  private OutputFormat outputFormat = OutputFormat.TEXT;
+  private boolean includePatchSets;
+  private boolean includeCurrentPatchSet;
+
+  private OutputStream outputStream = DisabledOutputStream.INSTANCE;
+  private PrintWriter out;
+
+  @Inject
+  QueryProcessor(EventFactory eventFactory,
+      ChangeQueryBuilder.Factory queryBuilder, CurrentUser currentUser,
+      ChangeQueryRewriter queryRewriter, Provider<ReviewDb> db) {
+    this.eventFactory = eventFactory;
+    this.queryBuilder = queryBuilder.create(currentUser);
+    this.queryRewriter = queryRewriter;
+    this.db = db;
+  }
+
+  public void setIncludePatchSets(boolean on) {
+    includePatchSets = on;
+  }
+
+  public void setIncludeCurrentPatchSet(boolean on) {
+    includeCurrentPatchSet = on;
+  }
+
+  public void setOutput(OutputStream out, OutputFormat fmt) {
+    this.outputStream = out;
+    this.outputFormat = fmt;
+  }
+
+  public void query(String queryString) throws IOException {
+    out = new PrintWriter( //
+        new BufferedWriter( //
+            new OutputStreamWriter(outputStream, "UTF-8")));
+    try {
+      try {
+        final QueryStats stats = new QueryStats();
+        stats.runTimeMilliseconds = System.currentTimeMillis();
+
+        final Predicate<ChangeData> visibleToMe = queryBuilder.is_visible();
+        Predicate<ChangeData> s = compileQuery(queryString, visibleToMe);
+        List<ChangeData> results = new ArrayList<ChangeData>();
+        HashSet<Change.Id> want = new HashSet<Change.Id>();
+        for (ChangeData d : ((ChangeDataSource) s).read()) {
+          if (d.hasChange()) {
+            // Checking visibleToMe here should be unnecessary, the
+            // query should have already performed it. But we don't
+            // want to trust the query rewriter that much yet.
+            //
+            if (visibleToMe.match(d)) {
+              results.add(d);
+            }
+          } else {
+            want.add(d.getId());
+          }
+        }
+
+        if (!want.isEmpty()) {
+          for (Change c : db.get().changes().get(want)) {
+            ChangeData d = new ChangeData(c);
+            if (visibleToMe.match(d)) {
+              results.add(d);
+            }
+          }
+        }
+
+        Collections.sort(results, new Comparator<ChangeData>() {
+          @Override
+          public int compare(ChangeData a, ChangeData b) {
+            return b.getChange().getSortKey().compareTo(
+                a.getChange().getSortKey());
+          }
+        });
+
+        int limit = limit(s);
+        if (limit < results.size()) {
+          results = results.subList(0, limit);
+        }
+
+        for (ChangeData d : results) {
+          ChangeAttribute c = eventFactory.asChangeAttribute(d.getChange());
+          eventFactory.extend(c, d.getChange());
+          eventFactory.addTrackingIds(c, d.trackingIds(db));
+
+          if (includePatchSets) {
+            eventFactory.addPatchSets(c, d.patches(db));
+          }
+
+          if (includeCurrentPatchSet) {
+            PatchSet current = d.currentPatchSet(db);
+            if (current != null) {
+              c.currentPatchSet = eventFactory.asPatchSetAttribute(current);
+              eventFactory.addApprovals(c.currentPatchSet, //
+                  d.approvalsFor(db, current.getId()));
+            }
+          }
+
+          show(c);
+        }
+
+        stats.rowCount = results.size();
+        stats.runTimeMilliseconds =
+            System.currentTimeMillis() - stats.runTimeMilliseconds;
+        show(stats);
+      } catch (OrmException err) {
+        log.error("Cannot execute query: " + queryString, err);
+
+        ErrorMessage m = new ErrorMessage();
+        m.message = "cannot query database";
+        show(m);
+
+      } catch (QueryParseException e) {
+        ErrorMessage m = new ErrorMessage();
+        m.message = e.getMessage();
+        show(m);
+      }
+    } finally {
+      try {
+        out.flush();
+      } finally {
+        out = null;
+      }
+    }
+  }
+
+  private int limit(Predicate<ChangeData> s) {
+    return queryBuilder.hasLimit(s) ? queryBuilder.getLimit(s) : defaultLimit;
+  }
+
+  @SuppressWarnings("unchecked")
+  private Predicate<ChangeData> compileQuery(String queryString,
+      final Predicate<ChangeData> visibleToMe) throws QueryParseException {
+
+    Predicate<ChangeData> q = queryBuilder.parse(queryString);
+    if (!queryBuilder.hasLimit(q)) {
+      q = Predicate.and(q, queryBuilder.limit(defaultLimit));
+    }
+    if (!queryBuilder.hasSortKey(q)) {
+      q = Predicate.and(q, queryBuilder.sortkey_before("z"));
+    }
+    q = Predicate.and(q, visibleToMe);
+
+    Predicate<ChangeData> s = queryRewriter.rewrite(q);
+    if (!(s instanceof ChangeDataSource)) {
+      s = queryRewriter.rewrite(Predicate.and(queryBuilder.status_open(), q));
+    }
+
+    if (!(s instanceof ChangeDataSource)) {
+      throw new QueryParseException("cannot execute query: " + s);
+    }
+
+    return s;
+  }
+
+  private void show(Object data) {
+    switch (outputFormat) {
+      default:
+      case TEXT:
+        if (data instanceof ChangeAttribute) {
+          out.print("change ");
+          out.print(((ChangeAttribute) data).id);
+          out.print("\n");
+          showText(data, 1);
+        } else {
+          showText(data, 0);
+        }
+        out.print('\n');
+        break;
+
+      case JSON:
+        out.print(gson.toJson(data));
+        out.print('\n');
+        break;
+    }
+  }
+
+  private void showText(Object data, int depth) {
+    for (Field f : fieldsOf(data.getClass())) {
+      Object val;
+      try {
+        val = f.get(data);
+      } catch (IllegalArgumentException err) {
+        continue;
+      } catch (IllegalAccessException err) {
+        continue;
+      }
+      if (val == null) {
+        continue;
+      }
+
+      indent(depth);
+      out.print(f.getName());
+      out.print(":");
+
+      if (val instanceof Long && isDateField(f.getName())) {
+        out.print(' ');
+        out.print(sdf.format(new Date(((Long) val) * 1000L)));
+        out.print('\n');
+      } else {
+        showTextValue(val, depth);
+      }
+    }
+  }
+
+  private void indent(int depth) {
+    for (int i = 0; i < depth; i++) {
+      out.print("  ");
+    }
+  }
+
+  @SuppressWarnings( {"cast", "unchecked"})
+  private void showTextValue(Object value, int depth) {
+    if (isPrimitive(value)) {
+      out.print(' ');
+      out.print(value);
+      out.print('\n');
+
+    } else if (value instanceof Collection) {
+      out.print('\n');
+      for (Object thing : ((Collection) value)) {
+        if (isPrimitive(thing)) {
+          out.print(' ');
+          out.print(value);
+          out.print('\n');
+        } else {
+          showText(thing, depth + 1);
+          out.print('\n');
+        }
+      }
+    } else {
+      out.print('\n');
+      showText(value, depth + 1);
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  private static boolean isPrimitive(Object value) {
+    return value instanceof String //
+        || value instanceof Number //
+        || value instanceof Boolean //
+        || value instanceof Enum;
+  }
+
+  private static boolean isDateField(String name) {
+    return "lastUpdated".equals(name) //
+        || "grantedOn".equals(name);
+  }
+
+  private List<Field> fieldsOf(Class<?> type) {
+    List<Field> r = new ArrayList<Field>();
+    if (type.getSuperclass() != null) {
+      r.addAll(fieldsOf(type.getSuperclass()));
+    }
+    r.addAll(Arrays.asList(type.getDeclaredFields()));
+    return r;
+  }
+
+  static class ErrorMessage {
+    public final String type = "error";
+    public String message;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RefPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RefPredicate.java
new file mode 100644
index 0000000..f5f83e2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RefPredicate.java
@@ -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.
+
+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;
+
+class RefPredicate extends OperatorPredicate<ChangeData> {
+  private final Provider<ReviewDb> dbProvider;
+
+  RefPredicate(Provider<ReviewDb> dbProvider, String ref) {
+    super(ChangeQueryBuilder.FIELD_REF, ref);
+    this.dbProvider = dbProvider;
+  }
+
+  @Override
+  public boolean match(final ChangeData object) throws OrmException {
+    Change change = object.change(dbProvider);
+    if (change == null) {
+      return false;
+    }
+    return getValue().equals(change.getDest().get());
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexFilePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexFilePredicate.java
new file mode 100644
index 0000000..6f159b8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexFilePredicate.java
@@ -0,0 +1,55 @@
+// Copyright 2010 Google Inc. All Rights Reserved.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gwtorm.client.OrmException;
+import com.google.inject.Provider;
+
+import java.util.Collection;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+class RegexFilePredicate extends OperatorPredicate<ChangeData> {
+  private final Provider<ReviewDb> db;
+  private final PatchListCache cache;
+  private final Pattern pattern;
+
+  RegexFilePredicate(Provider<ReviewDb> db, PatchListCache plc, String re) {
+    super(ChangeQueryBuilder.FIELD_FILE, re);
+    this.db = db;
+    this.cache = plc;
+    try {
+      this.pattern = Pattern.compile(re);
+    } catch (PatternSyntaxException e) {
+      throw new IllegalArgumentException(e.getMessage());
+    }
+  }
+
+  @Override
+  public boolean match(ChangeData object) throws OrmException {
+    Collection<String> files = object.currentFilePaths(db, cache);
+    if (files != null) {
+      for (String path : files) {
+        if (pattern.matcher(path).find()) {
+          return true;
+        }
+      }
+      return false;
+
+    } else {
+      // The ChangeData can't do expensive lookups right now. Bypass
+      // them and include the result anyway. We might be able to do
+      // a narrow later on to a smaller set.
+      //
+      return true;
+    }
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
new file mode 100644
index 0000000..bcece94
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
@@ -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.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.PatchSetApproval;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gwtorm.client.OrmException;
+import com.google.inject.Provider;
+
+class ReviewerPredicate extends OperatorPredicate<ChangeData> {
+  private final Provider<ReviewDb> dbProvider;
+  private final Account.Id id;
+
+  ReviewerPredicate(Provider<ReviewDb> dbProvider, Account.Id id) {
+    super(ChangeQueryBuilder.FIELD_REVIEWER, id.toString());
+    this.dbProvider = dbProvider;
+    this.id = id;
+  }
+
+  Account.Id getAccountId() {
+    return id;
+  }
+
+  @Override
+  public boolean match(final ChangeData object) throws OrmException {
+    for (PatchSetApproval p : object.approvals(dbProvider)) {
+      if (id.equals(p.getAccountId())) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public int getCost() {
+    return 2;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java
new file mode 100644
index 0000000..f8ab33f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.AccountProjectWatch;
+import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.server.AccessPath;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.AuthConfig;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+
+final class SingleGroupUser extends CurrentUser {
+  private final Set<AccountGroup.Id> groups;
+
+  SingleGroupUser(AuthConfig authConfig, AccountGroup.Id groupId) {
+    this(authConfig, Collections.singleton(groupId));
+  }
+
+  SingleGroupUser(AuthConfig authConfig, Set<AccountGroup.Id> groups) {
+    super(AccessPath.UNKNOWN, authConfig);
+    this.groups = groups;
+  }
+
+  @Override
+  public Set<AccountGroup.Id> getEffectiveGroups() {
+    return groups;
+  }
+
+  @Override
+  public Set<Change.Id> getStarredChanges() {
+    return Collections.emptySet();
+  }
+
+  @Override
+  public Collection<AccountProjectWatch> getNotificationFilters() {
+    return Collections.emptySet();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SortKeyPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SortKeyPredicate.java
new file mode 100644
index 0000000..3ecc596
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SortKeyPredicate.java
@@ -0,0 +1,47 @@
+// Copyright 2010 Google Inc. All Rights Reserved.
+
+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;
+
+abstract class SortKeyPredicate extends OperatorPredicate<ChangeData> {
+  protected final Provider<ReviewDb> dbProvider;
+
+  SortKeyPredicate(Provider<ReviewDb> dbProvider, String name, String value) {
+    super(name, value);
+    this.dbProvider = dbProvider;
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+
+  static class Before extends SortKeyPredicate {
+    Before(Provider<ReviewDb> dbProvider, String value) {
+      super(dbProvider, "sortkey_before", value);
+    }
+
+    @Override
+    public boolean match(ChangeData cd) throws OrmException {
+      Change change = cd.change(dbProvider);
+      return change != null && change.getSortKey().compareTo(getValue()) < 0;
+    }
+  }
+
+  static class After extends SortKeyPredicate {
+    After(Provider<ReviewDb> dbProvider, String value) {
+      super(dbProvider, "sortkey_after", value);
+    }
+
+    @Override
+    public boolean match(ChangeData cd) throws OrmException {
+      Change change = cd.change(dbProvider);
+      return change != null && change.getSortKey().compareTo(getValue()) > 0;
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TopicPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TopicPredicate.java
new file mode 100644
index 0000000..7bc972d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TopicPredicate.java
@@ -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.
+
+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;
+
+class TopicPredicate extends OperatorPredicate<ChangeData> {
+  private final Provider<ReviewDb> dbProvider;
+
+  TopicPredicate(Provider<ReviewDb> dbProvider, String topic) {
+    super(ChangeQueryBuilder.FIELD_TOPIC, topic);
+    this.dbProvider = dbProvider;
+  }
+
+  @Override
+  public boolean match(final ChangeData object) throws OrmException {
+    Change change = object.change(dbProvider);
+    if (change == null) {
+      return false;
+    }
+    return getValue().equals(change.getTopic());
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java
new file mode 100644
index 0000000..eef568d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java
@@ -0,0 +1,77 @@
+// 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.reviewdb.TrackingId;
+import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.client.ResultSet;
+import com.google.gwtorm.client.impl.ListResultSet;
+import com.google.inject.Provider;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+
+class TrackingIdPredicate extends OperatorPredicate<ChangeData> implements
+    ChangeDataSource {
+  private final Provider<ReviewDb> db;
+
+  TrackingIdPredicate(Provider<ReviewDb> db, String trackingId) {
+    super(ChangeQueryBuilder.FIELD_TR, trackingId);
+    this.db = db;
+  }
+
+  @Override
+  public boolean match(final ChangeData object) throws OrmException {
+    for (TrackingId c : object.trackingIds(db)) {
+      if (getValue().equals(c.getTrackingId())) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public ResultSet<ChangeData> read() throws OrmException {
+    HashSet<Change.Id> ids = new HashSet<Change.Id>();
+    for (TrackingId sc : db.get().trackingIds() //
+        .byTrackingId(new TrackingId.Id(getValue()))) {
+      ids.add(sc.getChangeId());
+    }
+
+    ArrayList<ChangeData> r = new ArrayList<ChangeData>(ids.size());
+    for (Change.Id id : ids) {
+      r.add(new ChangeData(id));
+    }
+    return new ListResultSet<ChangeData>(r);
+  }
+
+  @Override
+  public boolean hasChange() {
+    return false;
+  }
+
+  @Override
+  public int getCardinality() {
+    return ChangeCosts.CARD_TRACKING_IDS;
+  }
+
+  @Override
+  public int getCost() {
+    return ChangeCosts.cost(ChangeCosts.TR_SCAN, getCardinality());
+  }
+}
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 da7d389..2f8d4ca 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_35.class;
+  private static final Class<? extends SchemaVersion> C = Schema_40.class;
 
   public static class Module extends AbstractModule {
     @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_34.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_34.java
index 2f06f70..fa94146 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_34.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_34.java
@@ -19,19 +19,33 @@
 import com.google.gerrit.reviewdb.RefRight;
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.reviewdb.RefRight.RefPattern;
-import com.google.gerrit.server.project.RefControl;
 import com.google.gerrit.server.project.RefControl.RefRightsForPattern;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
 import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.TreeMap;
 
 public class Schema_34 extends SchemaVersion {
+  private static final Comparator<String> DESCENDING_SORT =
+      new Comparator<String>() {
+
+        @Override
+        public int compare(String a, String b) {
+          int aLength = a.length();
+          int bLength = b.length();
+          if (bLength == aLength) {
+            return a.compareTo(b);
+          }
+          return bLength - aLength;
+        }
+      };
+
   @Inject
   Schema_34(Provider<Schema_33> prior) {
     super(prior);
@@ -54,7 +68,7 @@
         ApprovalCategory.Id cat = right.getApprovalCategoryId();
         if (r.get(cat) == null) {
           Map<String, RefRightsForPattern> m =
-            new TreeMap<String, RefRightsForPattern>(RefControl.DESCENDING_SORT);
+            new TreeMap<String, RefRightsForPattern>(DESCENDING_SORT);
           r.put(cat, m);
         }
         if (r.get(cat).get(right.getRefPattern()) == null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_36.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_36.java
new file mode 100644
index 0000000..ba6b841
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_36.java
@@ -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.
+
+package com.google.gerrit.server.schema;
+
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.schema.sql.DialectMySQL;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.sql.SQLException;
+import java.sql.Statement;
+
+public class Schema_36 extends SchemaVersion {
+  @Inject
+  Schema_36(Provider<Schema_35> prior) {
+    super(prior);
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui) throws SQLException {
+    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+    try {
+      if (((JdbcSchema) db).getDialect() instanceof DialectMySQL) {
+        stmt.execute("DROP INDEX account_project_watches_ntNew ON account_project_watches");
+        stmt.execute("DROP INDEX account_project_watches_ntCmt ON account_project_watches");
+        stmt.execute("DROP INDEX account_project_watches_ntSub ON account_project_watches");
+      } else {
+        stmt.execute("DROP INDEX account_project_watches_ntNew");
+        stmt.execute("DROP INDEX account_project_watches_ntCmt");
+        stmt.execute("DROP INDEX account_project_watches_ntSub");
+      }
+      stmt.execute("CREATE INDEX account_project_watches_byProject"
+          + " ON account_project_watches (project_name)");
+    } finally {
+      stmt.close();
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_37.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_37.java
new file mode 100644
index 0000000..871f2e9
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_37.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_37 extends SchemaVersion {
+  @Inject
+  Schema_37(Provider<Schema_36> prior) {
+    super(prior);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_38.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_38.java
new file mode 100644
index 0000000..59d6fa2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_38.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.AccountDiffPreference;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.List;
+
+public class Schema_38 extends SchemaVersion {
+  @Inject
+  Schema_38(Provider<Schema_37> prior) {
+    super(prior);
+  }
+
+  /**
+   * Migrate the account.default_context column to account_diff_preferences.context column.
+   * <p>
+   * Other fields in account_diff_preferences will be filled in with their defaults as
+   * defined in the {@link AccountDiffPreference#createDefault(com.google.gerrit.reviewdb.Account.Id)}
+   */
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException,
+      SQLException {
+    List<AccountDiffPreference> newPrefs =
+        new ArrayList<AccountDiffPreference>();
+
+    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+    try {
+      ResultSet result =
+          stmt.executeQuery("SELECT account_id, default_context"
+              + " FROM accounts WHERE default_context <> 10");
+      while (result.next()) {
+        int accountId = result.getInt(1);
+        short defaultContext = result.getShort(2);
+        AccountDiffPreference diffPref = AccountDiffPreference.createDefault(new Account.Id(accountId));
+        diffPref.setContext(defaultContext);
+        newPrefs.add(diffPref);
+      }
+    } finally {
+      stmt.close();
+    }
+
+    db.accountDiffPreferences().insert(newPrefs);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_39.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_39.java
new file mode 100644
index 0000000..39ae226
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_39.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_39 extends SchemaVersion {
+  @Inject
+  Schema_39(Provider<Schema_38> prior) {
+    super(prior);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_40.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_40.java
new file mode 100644
index 0000000..7d3e4f5
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_40.java
@@ -0,0 +1,70 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import com.google.gerrit.reviewdb.AccountProjectWatch;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.schema.sql.DialectH2;
+import com.google.gwtorm.schema.sql.DialectMySQL;
+import com.google.gwtorm.schema.sql.DialectPostgreSQL;
+import com.google.gwtorm.schema.sql.SqlDialect;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.sql.SQLException;
+import java.sql.Statement;
+
+public class Schema_40 extends SchemaVersion {
+  @Inject
+  Schema_40(Provider<Schema_39> prior) {
+    super(prior);
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui) throws SQLException,
+      OrmException {
+    // Set to "*" the filter field of the previously watched projects
+    //
+    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+    try {
+      stmt.execute("UPDATE account_project_watches" //
+          + " SET filter = '" + AccountProjectWatch.FILTER_ALL + "'" //
+          + " WHERE filter IS NULL OR filter = ''");
+
+      // Set the new primary key
+      //
+      final SqlDialect dialect = ((JdbcSchema) db).getDialect();
+      if (dialect instanceof DialectPostgreSQL) {
+        stmt.execute("ALTER TABLE account_project_watches "
+            + "DROP CONSTRAINT account_project_watches_pkey");
+        stmt.execute("ALTER TABLE account_project_watches "
+            + "ADD PRIMARY KEY (account_id, project_name, filter)");
+
+      } else if ((dialect instanceof DialectH2)
+          || (dialect instanceof DialectMySQL)) {
+        stmt.execute("ALTER TABLE account_project_watches DROP PRIMARY KEY");
+        stmt.execute("ALTER TABLE account_project_watches "
+            + "ADD PRIMARY KEY (account_id, project_name, filter)");
+
+      } else {
+        throw new OrmException("Unsupported dialect " + dialect);
+      }
+    } finally {
+      stmt.close();
+    }
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/AndPredicateTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/AndPredicateTest.java
new file mode 100644
index 0000000..589440d
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/AndPredicateTest.java
@@ -0,0 +1,138 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query;
+
+import static com.google.gerrit.server.query.Predicate.and;
+
+import junit.framework.TestCase;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+public class AndPredicateTest extends TestCase {
+  private static final class TestPredicate extends OperatorPredicate<String> {
+    private TestPredicate(String name, String value) {
+      super(name, value);
+    }
+
+    @Override
+    public boolean match(String object) {
+      return false;
+    }
+
+    @Override
+    public int getCost() {
+      return 0;
+    }
+  }
+
+  private static TestPredicate f(final String name, final String value) {
+    return new TestPredicate(name, value);
+  }
+
+  public void testChildren() {
+    final TestPredicate a = f("author", "alice");
+    final TestPredicate b = f("author", "bob");
+    final Predicate<String> n = and(a, b);
+    assertEquals(2, n.getChildCount());
+    assertSame(a, n.getChild(0));
+    assertSame(b, n.getChild(1));
+  }
+
+  public void testChildrenUnmodifiable() {
+    final TestPredicate a = f("author", "alice");
+    final TestPredicate b = f("author", "bob");
+    final Predicate<String> n = and(a, b);
+
+    try {
+      n.getChildren().clear();
+    } catch (RuntimeException e) {
+    }
+    assertChildren("clear", n, list(a, b));
+
+    try {
+      n.getChildren().remove(0);
+    } catch (RuntimeException e) {
+    }
+    assertChildren("remove(0)", n, list(a, b));
+
+    try {
+      n.getChildren().iterator().remove();
+    } catch (RuntimeException e) {
+    }
+    assertChildren("remove(0)", n, list(a, b));
+  }
+
+  private static void assertChildren(String o, Predicate<String> p,
+      final List<Predicate<String>> l) {
+    assertEquals(o + " did not affect child", l, p.getChildren());
+  }
+
+  public void testToString() {
+    final TestPredicate a = f("q", "alice");
+    final TestPredicate b = f("q", "bob");
+    final TestPredicate c = f("q", "charlie");
+    assertEquals("(q:alice q:bob)", and(a, b).toString());
+    assertEquals("(q:alice q:bob q:charlie)", and(a, b, c).toString());
+  }
+
+  public void testEquals() {
+    final TestPredicate a = f("author", "alice");
+    final TestPredicate b = f("author", "bob");
+    final TestPredicate c = f("author", "charlie");
+
+    assertTrue(and(a, b).equals(and(a, b)));
+    assertTrue(and(a, b, c).equals(and(a, b, c)));
+
+    assertFalse(and(a, b).equals(and(b, a)));
+    assertFalse(and(a, c).equals(and(a, b)));
+
+    assertFalse(and(a, c).equals(a));
+  }
+
+  public void testHashCode() {
+    final TestPredicate a = f("author", "alice");
+    final TestPredicate b = f("author", "bob");
+    final TestPredicate c = f("author", "charlie");
+
+    assertTrue(and(a, b).hashCode() == and(a, b).hashCode());
+    assertTrue(and(a, b, c).hashCode() == and(a, b, c).hashCode());
+    assertFalse(and(a, c).hashCode() == and(a, b).hashCode());
+  }
+
+  public void testCopy() {
+    final TestPredicate a = f("author", "alice");
+    final TestPredicate b = f("author", "bob");
+    final TestPredicate c = f("author", "charlie");
+    final List<Predicate<String>> s2 = list(a, b);
+    final List<Predicate<String>> s3 = list(a, b, c);
+    final Predicate<String> n2 = and(a, b);
+
+    assertNotSame(n2, n2.copy(s2));
+    assertEquals(s2, n2.copy(s2).getChildren());
+    assertEquals(s3, n2.copy(s3).getChildren());
+
+    try {
+      n2.copy(Collections.<Predicate<String>> emptyList());
+    } catch (IllegalArgumentException e) {
+      assertEquals("Need at least two predicates", e.getMessage());
+    }
+  }
+
+  private static List<Predicate<String>> list(final Predicate<String>... predicates) {
+    return Arrays.asList(predicates);
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/ChangeQueryBuilderTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/ChangeQueryBuilderTest.java
deleted file mode 100644
index 4dc8ba5..0000000
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/ChangeQueryBuilderTest.java
+++ /dev/null
@@ -1,210 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.query;
-
-import static com.google.gerrit.server.query.ChangeQueryBuilder.FIELD_CHANGE;
-import static com.google.gerrit.server.query.ChangeQueryBuilder.FIELD_COMMIT;
-import static com.google.gerrit.server.query.ChangeQueryBuilder.FIELD_OWNER;
-import static com.google.gerrit.server.query.ChangeQueryBuilder.FIELD_REVIEWER;
-import static com.google.gerrit.server.query.Predicate.and;
-import static com.google.gerrit.server.query.Predicate.not;
-import static com.google.gerrit.server.query.Predicate.or;
-
-import junit.framework.TestCase;
-
-import org.eclipse.jgit.lib.AbbreviatedObjectId;
-
-public class ChangeQueryBuilderTest extends TestCase {
-  private static OperatorPredicate f(final String name, final String value) {
-    return new OperatorPredicate(name, value);
-  }
-
-  private static Predicate owner(final String who) {
-    return f(FIELD_OWNER, who);
-  }
-
-  private static Predicate reviewer(final String who) {
-    return f(FIELD_REVIEWER, who);
-  }
-
-  private static Predicate commit(final String idstr) {
-    final AbbreviatedObjectId id = AbbreviatedObjectId.fromString(idstr);
-    return new ObjectIdPredicate(FIELD_COMMIT, id);
-  }
-
-  private static Predicate p(final String str) throws QueryParseException {
-    return new ChangeQueryBuilder().parse(str);
-  }
-
-  public void testEmptyQuery() {
-    try {
-      p("");
-      fail("expected exception");
-    } catch (QueryParseException e) {
-      assertEquals("line 0:-1 no viable alternative at input '<EOF>'", e
-          .getMessage());
-    }
-  }
-
-  public void testFailInvalidOperator() {
-    final String op = "thiswillneverbeaqueryoperatoritistoolongtotype";
-    final String val = "true";
-    try {
-      p(op + ":" + val);
-      fail("expected exception");
-    } catch (QueryParseException e) {
-      assertEquals("Unsupported operator " + op + ":" + val, e.getMessage());
-    }
-  }
-
-  public void testFailNestedOperator() {
-    try {
-      p("commit:(foo:bar whiz:bang)");
-      fail("expected exception");
-    } catch (QueryParseException e) {
-      assertEquals("Nested operator not expected: foo", e.getMessage());
-    }
-  }
-
-  // commit:
-
-  public void testDefaultSHA1() throws QueryParseException {
-    assertEquals(commit("6ea15"), p("6ea15"));
-    assertEquals(commit("6ea15"), p("6EA15"));
-    assertEquals(commit("6ea15b73668073fd9f70b2635efcb8cf8aabda22"),
-        p("6ea15b73668073fd9f70b2635efcb8cf8aabda22"));
-  }
-
-  public void testCommitSHA1() throws QueryParseException {
-    assertEquals(commit("6ea15"), p("commit:6ea15"));
-    assertEquals(commit("6ea15"), p("commit:6EA15")); // note: forces lowercase
-    assertEquals(commit("6ea15b73668073fd9f70b2635efcb8cf8aabda22"),
-        p("commit:6ea15b73668073fd9f70b2635efcb8cf8aabda22"));
-
-    try {
-      p("commit:yonothash");
-    } catch (QueryParseException e) {
-      assertEquals("Error in operator commit:yonothash", e.getMessage());
-    }
-  }
-
-  // change:
-
-  public void testDefaultChangeID() throws QueryParseException {
-    assertEquals(f(FIELD_CHANGE, "1234"), p("1234"));
-  }
-
-  public void testChangeID() throws QueryParseException {
-    assertEquals(f(FIELD_CHANGE, "1234"), p("change:1234"));
-  }
-
-  // owner:
-
-  public void testOwnerBare() throws QueryParseException {
-    assertEquals(owner("bob"), p("owner:bob"));
-    assertEquals(owner("Bob"), p("owner:Bob"));
-    assertEquals(owner("bob@example.com"), p("owner:bob@example.com"));
-
-    assertEquals(owner("bob"), p("owner: bob"));
-    assertEquals(owner("Bob"), p("owner: Bob"));
-    assertEquals(owner("bob@example.com"), p("owner: bob@example.com"));
-
-    assertEquals(owner("bob"), p("owner:\tbob"));
-    assertEquals(owner("Bob"), p("owner:\tBob"));
-    assertEquals(owner("bob@example.com"), p("owner:\tbob@example.com"));
-  }
-
-  public void testOwnerQuoted() throws QueryParseException {
-    assertEquals(owner("bob"), p("owner:\"bob\""));
-    assertEquals(owner("bob@example.com"), p("owner:\"bob@example.com\""));
-    assertEquals(owner("<bob@example.com>"), p("owner:\"<bob@example.com>\""));
-    assertEquals(owner("A U Thor"), p("owner:\"A U Thor\""));
-
-    assertEquals(owner("bob"), p("owner: \"bob\""));
-    assertEquals(owner("bob@example.com"), p("owner: \"bob@example.com\""));
-    assertEquals(owner("<bob@example.com>"), p("owner: \"<bob@example.com>\""));
-    assertEquals(owner("A U Thor"), p("owner: \"A U Thor\""));
-
-    assertEquals(owner("bob"), p("owner:\t\"bob\""));
-    assertEquals(owner("bob@example.com"), p("owner:\t\"bob@example.com\""));
-    assertEquals(owner("<bob@example.com>"), p("owner:\t\"<bob@example.com>\""));
-    assertEquals(owner("A U Thor"), p("owner:\t\"A U Thor\""));
-  }
-
-  public void testOwner_NOT() throws QueryParseException {
-    assertEquals(not(owner("bob")), p("-owner:bob"));
-    assertEquals(not(owner("Bob")), p("-owner:Bob"));
-    assertEquals(not(owner("bob@example.com")), p("-owner:bob@example.com"));
-
-    assertEquals(not(owner("bob")), p("NOT owner:bob"));
-    assertEquals(not(owner("Bob")), p("NOT owner:Bob"));
-    assertEquals(not(owner("bob@example.com")), p("NOT owner:bob@example.com"));
-  }
-
-  // AND
-
-  public void testAND_Styles2() throws QueryParseException {
-    final Predicate exp = and(commit("6ea15"), owner("bob"));
-    assertEquals(exp, p("6ea15 owner:bob"));
-    assertEquals(exp, p("6ea15 AND owner:bob"));
-  }
-
-  public void testAND_Styles3() throws QueryParseException {
-    final Predicate exp = and(commit("6ea15"), owner("bob"), reviewer("alice"));
-    assertEquals(exp, p("6ea15 owner:bob reviewer:alice"));
-    assertEquals(exp, p("6ea15 AND owner:bob reviewer:alice"));
-    assertEquals(exp, p("6ea15 owner:bob AND reviewer:alice"));
-    assertEquals(exp, p("6ea15 AND owner:bob AND reviewer:alice"));
-  }
-
-  public void testAND_ManyValuesOneOperator() throws QueryParseException {
-    final Predicate exp =
-        and(reviewer("alice"), reviewer("bob"), reviewer("charlie"));
-    assertEquals(exp, p("reviewer:(alice bob charlie)"));
-    assertEquals(exp, p("reviewer:(alice AND bob charlie)"));
-    assertEquals(exp, p("reviewer:(alice bob AND charlie)"));
-    assertEquals(exp, p("reviewer:(alice AND bob AND charlie)"));
-  }
-
-  public void testAND_FlattensOperators() throws QueryParseException {
-    final Predicate exp =
-        and(reviewer("alice"), reviewer("bob"), reviewer("charlie"));
-    assertEquals(exp, p("reviewer:alice reviewer:(bob charlie)"));
-  }
-
-  // OR
-
-  public void testOR_2() throws QueryParseException {
-    final Predicate exp = or(commit("6ea15"), owner("bob"));
-    assertEquals(exp, p("6ea15 OR owner:bob"));
-  }
-
-  public void testOR_3() throws QueryParseException {
-    final Predicate exp = or(commit("6ea15"), owner("bob"), reviewer("alice"));
-    assertEquals(exp, p("6ea15 OR owner:bob OR reviewer:alice"));
-  }
-
-  public void testOR_ManyValuesOneOperator() throws QueryParseException {
-    final Predicate exp =
-        or(reviewer("alice"), reviewer("bob"), reviewer("charlie"));
-    assertEquals(exp, p("reviewer:(alice OR bob OR charlie)"));
-  }
-
-  public void testOR_FlattensOperators() throws QueryParseException {
-    final Predicate exp =
-        or(reviewer("alice"), reviewer("bob"), reviewer("charlie"));
-    assertEquals(exp, p("reviewer:alice OR reviewer:(bob OR charlie)"));
-  }
-}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/FieldPredicateTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/FieldPredicateTest.java
index 63cf166..a37a336 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/FieldPredicateTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/FieldPredicateTest.java
@@ -16,9 +16,27 @@
 
 import junit.framework.TestCase;
 
+import java.util.Collections;
+
 public class FieldPredicateTest extends TestCase {
-  private static OperatorPredicate f(final String name, final String value) {
-    return new OperatorPredicate(name, value);
+  private static final class TestPredicate extends OperatorPredicate<String> {
+    private TestPredicate(String name, String value) {
+      super(name, value);
+    }
+
+    @Override
+    public boolean match(String object) {
+      return false;
+    }
+
+    @Override
+    public int getCost() {
+      return 0;
+    }
+  }
+
+  private static TestPredicate f(final String name, final String value) {
+    return new TestPredicate(name, value);
   }
 
   public void testToString() {
@@ -42,9 +60,21 @@
   public void testNameValue() {
     final String name = "author";
     final String value = "alice";
-    final OperatorPredicate f = f(name, value);
+    final OperatorPredicate<String> f = f(name, value);
     assertSame(name, f.getOperator());
     assertSame(value, f.getValue());
     assertEquals(0, f.getChildren().size());
   }
+
+  public void testCopy() {
+    final OperatorPredicate<String> f = f("author", "alice");
+    assertSame(f, f.copy(Collections.<Predicate<String>> emptyList()));
+    assertSame(f, f.copy(f.getChildren()));
+
+    try {
+      f.copy(Collections.singleton(f("owner", "bob")));
+    } catch (IllegalArgumentException e) {
+      assertEquals("Expected 0 children", e.getMessage());
+    }
+  }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/NotPredicateTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/NotPredicateTest.java
index 23ba24e..8e86e95 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/NotPredicateTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/NotPredicateTest.java
@@ -14,33 +14,53 @@
 
 package com.google.gerrit.server.query;
 
+import static com.google.gerrit.server.query.Predicate.and;
 import static com.google.gerrit.server.query.Predicate.not;
 
 import junit.framework.TestCase;
 
+import java.util.Collections;
+import java.util.List;
+
 public class NotPredicateTest extends TestCase {
-  private static OperatorPredicate f(final String name, final String value) {
-    return new OperatorPredicate(name, value);
+  private static final class TestPredicate extends OperatorPredicate<String> {
+    private TestPredicate(String name, String value) {
+      super(name, value);
+    }
+
+    @Override
+    public boolean match(String object) {
+      return false;
+    }
+
+    @Override
+    public int getCost() {
+      return 0;
+    }
+  }
+
+  private static TestPredicate f(final String name, final String value) {
+    return new TestPredicate(name, value);
   }
 
   public void testNotNot() {
-    final OperatorPredicate p = f("author", "bob");
-    final Predicate n = p.not();
+    final TestPredicate p = f("author", "bob");
+    final Predicate n = not(p);
     assertTrue(n instanceof NotPredicate);
     assertNotSame(p, n);
-    assertSame(p, n.not());
+    assertSame(p, not(n));
   }
 
   public void testChildren() {
-    final OperatorPredicate p = f("author", "bob");
-    final Predicate n = p.not();
+    final TestPredicate p = f("author", "bob");
+    final Predicate n = not(p);
     assertEquals(1, n.getChildCount());
     assertSame(p, n.getChild(0));
   }
 
   public void testChildrenUnmodifiable() {
-    final OperatorPredicate p = f("author", "bob");
-    final Predicate n = p.not();
+    final TestPredicate p = f("author", "bob");
+    final Predicate n = not(p);
 
     try {
       n.getChildren().clear();
@@ -81,4 +101,30 @@
     assertTrue(not(f("a", "b")).hashCode() == not(f("a", "b")).hashCode());
     assertFalse(not(f("a", "b")).hashCode() == not(f("a", "a")).hashCode());
   }
+
+  public void testCopy() {
+    final TestPredicate a = f("author", "alice");
+    final TestPredicate b = f("author", "bob");
+    final List<TestPredicate> sa = Collections.singletonList(a);
+    final List<TestPredicate> sb = Collections.singletonList(b);
+    final Predicate n = not(a);
+
+    assertNotSame(n, n.copy(sa));
+    assertEquals(sa, n.copy(sa).getChildren());
+
+    assertNotSame(n, n.copy(sb));
+    assertEquals(sb, n.copy(sb).getChildren());
+
+    try {
+      n.copy(Collections.<Predicate> emptyList());
+    } catch (IllegalArgumentException e) {
+      assertEquals("Expected exactly one child", e.getMessage());
+    }
+
+    try {
+      n.copy(and(a, b).getChildren());
+    } catch (IllegalArgumentException e) {
+      assertEquals("Expected exactly one child", e.getMessage());
+    }
+  }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/OrPredicateTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/OrPredicateTest.java
new file mode 100644
index 0000000..be820b9
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/OrPredicateTest.java
@@ -0,0 +1,138 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query;
+
+import static com.google.gerrit.server.query.Predicate.or;
+
+import junit.framework.TestCase;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+public class OrPredicateTest extends TestCase {
+  private static final class TestPredicate extends OperatorPredicate<String> {
+    private TestPredicate(String name, String value) {
+      super(name, value);
+    }
+
+    @Override
+    public boolean match(String object) {
+      return false;
+    }
+
+    @Override
+    public int getCost() {
+      return 0;
+    }
+  }
+
+  private static TestPredicate f(final String name, final String value) {
+    return new TestPredicate(name, value);
+  }
+
+  public void testChildren() {
+    final TestPredicate a = f("author", "alice");
+    final TestPredicate b = f("author", "bob");
+    final Predicate n = or(a, b);
+    assertEquals(2, n.getChildCount());
+    assertSame(a, n.getChild(0));
+    assertSame(b, n.getChild(1));
+  }
+
+  public void testChildrenUnmodifiable() {
+    final TestPredicate a = f("author", "alice");
+    final TestPredicate b = f("author", "bob");
+    final Predicate n = or(a, b);
+
+    try {
+      n.getChildren().clear();
+    } catch (RuntimeException e) {
+    }
+    assertChildren("clear", n, list(a, b));
+
+    try {
+      n.getChildren().remove(0);
+    } catch (RuntimeException e) {
+    }
+    assertChildren("remove(0)", n, list(a, b));
+
+    try {
+      n.getChildren().iterator().remove();
+    } catch (RuntimeException e) {
+    }
+    assertChildren("remove(0)", n, list(a, b));
+  }
+
+  private static void assertChildren(String o, Predicate p,
+      final List<Predicate> l) {
+    assertEquals(o + " did not affect child", l, p.getChildren());
+  }
+
+  public void testToString() {
+    final TestPredicate a = f("q", "alice");
+    final TestPredicate b = f("q", "bob");
+    final TestPredicate c = f("q", "charlie");
+    assertEquals("(q:alice OR q:bob)", or(a, b).toString());
+    assertEquals("(q:alice OR q:bob OR q:charlie)", or(a, b, c).toString());
+  }
+
+  public void testEquals() {
+    final TestPredicate a = f("author", "alice");
+    final TestPredicate b = f("author", "bob");
+    final TestPredicate c = f("author", "charlie");
+
+    assertTrue(or(a, b).equals(or(a, b)));
+    assertTrue(or(a, b, c).equals(or(a, b, c)));
+
+    assertFalse(or(a, b).equals(or(b, a)));
+    assertFalse(or(a, c).equals(or(a, b)));
+
+    assertFalse(or(a, c).equals(a));
+  }
+
+  public void testHashCode() {
+    final TestPredicate a = f("author", "alice");
+    final TestPredicate b = f("author", "bob");
+    final TestPredicate c = f("author", "charlie");
+
+    assertTrue(or(a, b).hashCode() == or(a, b).hashCode());
+    assertTrue(or(a, b, c).hashCode() == or(a, b, c).hashCode());
+    assertFalse(or(a, c).hashCode() == or(a, b).hashCode());
+  }
+
+  public void testCopy() {
+    final TestPredicate a = f("author", "alice");
+    final TestPredicate b = f("author", "bob");
+    final TestPredicate c = f("author", "charlie");
+    final List<Predicate> s2 = list(a, b);
+    final List<Predicate> s3 = list(a, b, c);
+    final Predicate n2 = or(a, b);
+
+    assertNotSame(n2, n2.copy(s2));
+    assertEquals(s2, n2.copy(s2).getChildren());
+    assertEquals(s3, n2.copy(s3).getChildren());
+
+    try {
+      n2.copy(Collections.<Predicate> emptyList());
+    } catch (IllegalArgumentException e) {
+      assertEquals("Need at least two predicates", e.getMessage());
+    }
+  }
+
+  private static List<Predicate> list(final Predicate... predicates) {
+    return Arrays.asList(predicates);
+  }
+}
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 41a34af..1644d22e 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
@@ -23,7 +23,7 @@
 import com.google.gerrit.reviewdb.SystemConfig;
 import com.google.gerrit.server.workflow.NoOpFunction;
 import com.google.gerrit.server.workflow.SubmitFunction;
-import com.google.gerrit.testutil.TestDatabase;
+import com.google.gerrit.testutil.InMemoryDatabase;
 import com.google.gwtorm.client.OrmException;
 import com.google.gwtorm.jdbc.JdbcSchema;
 
@@ -36,17 +36,17 @@
 
 public class SchemaCreatorTest extends TestCase {
   private ApprovalCategory.Id codeReview = new ApprovalCategory.Id("CRVW");
-  private TestDatabase db;
+  private InMemoryDatabase db;
 
   @Override
   protected void setUp() throws Exception {
     super.setUp();
-    db = new TestDatabase();
+    db = new InMemoryDatabase();
   }
 
   @Override
   protected void tearDown() throws Exception {
-    TestDatabase.drop(db);
+    InMemoryDatabase.drop(db);
     super.tearDown();
   }
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java
index 1e01674..a009f24 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java
@@ -17,7 +17,7 @@
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.reviewdb.SystemConfig;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.testutil.TestDatabase;
+import com.google.gerrit.testutil.InMemoryDatabase;
 import com.google.gwtorm.client.OrmException;
 import com.google.gwtorm.client.SchemaFactory;
 import com.google.gwtorm.client.StatementExecutor;
@@ -33,17 +33,17 @@
 import java.util.UUID;
 
 public class SchemaUpdaterTest extends TestCase {
-  private TestDatabase db;
+  private InMemoryDatabase db;
 
   @Override
   protected void setUp() throws Exception {
     super.setUp();
-    db = new TestDatabase();
+    db = new InMemoryDatabase();
   }
 
   @Override
   protected void tearDown() throws Exception {
-    TestDatabase.drop(db);
+    InMemoryDatabase.drop(db);
     super.tearDown();
   }
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/util/SocketUtilTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/util/SocketUtilTest.java
index e6aecc8..9e66046 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/util/SocketUtilTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/util/SocketUtilTest.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.util;
 
-import static com.google.gerrit.server.util.SocketUtil.format;
 import static com.google.gerrit.server.util.SocketUtil.hostname;
 import static com.google.gerrit.server.util.SocketUtil.isIPv6;
 import static com.google.gerrit.server.util.SocketUtil.parse;
@@ -48,21 +47,21 @@
   }
 
   public void testFormat() throws UnknownHostException {
-    assertEquals("*:1234", format(new InetSocketAddress(1234), 80));
-    assertEquals("*", format(new InetSocketAddress(80), 80));
+    assertEquals("*:1234", SocketUtil.format(new InetSocketAddress(1234), 80));
+    assertEquals("*", SocketUtil.format(new InetSocketAddress(80), 80));
 
-    assertEquals("foo:1234", format(createUnresolved("foo", 1234), 80));
-    assertEquals("foo", format(createUnresolved("foo", 80), 80));
+    assertEquals("foo:1234", SocketUtil.format(createUnresolved("foo", 1234), 80));
+    assertEquals("foo", SocketUtil.format(createUnresolved("foo", 80), 80));
 
     assertEquals("[1:2:3:4:5:6:7:8]:1234",//
-        format(new InetSocketAddress(getByName("1:2:3:4:5:6:7:8"), 1234), 80));
+        SocketUtil.format(new InetSocketAddress(getByName("1:2:3:4:5:6:7:8"), 1234), 80));
     assertEquals("[1:2:3:4:5:6:7:8]",//
-        format(new InetSocketAddress(getByName("1:2:3:4:5:6:7:8"), 80), 80));
+        SocketUtil.format(new InetSocketAddress(getByName("1:2:3:4:5:6:7:8"), 80), 80));
 
     assertEquals("localhost:1234",//
-        format(new InetSocketAddress("localhost", 1234), 80));
+        SocketUtil.format(new InetSocketAddress("localhost", 1234), 80));
     assertEquals("localhost",//
-        format(new InetSocketAddress("localhost", 80), 80));
+        SocketUtil. format(new InetSocketAddress("localhost", 80), 80));
   }
 
   public void testParse() {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestDatabase.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java
similarity index 93%
rename from gerrit-server/src/test/java/com/google/gerrit/testutil/TestDatabase.java
rename to gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java
index 1f71a91..fe138c6 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestDatabase.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java
@@ -43,10 +43,10 @@
  * <p>
  * Test classes should create one instance of this class for each unique test
  * database they want to use. When the tests needing this instance are complete,
- * ensure that {@link #drop(TestDatabase)} is called to free the resources so
+ * ensure that {@link #drop(InMemoryDatabase)} is called to free the resources so
  * the JVM running the unit tests doesn't run out of heap space.
  */
-public class TestDatabase implements SchemaFactory<ReviewDb> {
+public class InMemoryDatabase implements SchemaFactory<ReviewDb> {
   private static int dbCnt;
 
   private static synchronized DataSource newDataSource() throws SQLException {
@@ -58,7 +58,7 @@
   }
 
   /** Drop the database from memory; does nothing if the instance was null. */
-  public static void drop(final TestDatabase db) {
+  public static void drop(final InMemoryDatabase db) {
     if (db != null) {
       db.drop();
     }
@@ -69,7 +69,7 @@
   private boolean created;
   private SchemaVersion schemaVersion;
 
-  public TestDatabase() throws OrmException {
+  public InMemoryDatabase() throws OrmException {
     try {
       final DataSource dataSource = newDataSource();
 
@@ -101,7 +101,7 @@
   }
 
   /** Ensure the database schema has been created and initialized. */
-  public TestDatabase create() throws OrmException {
+  public InMemoryDatabase create() throws OrmException {
     if (!created) {
       created = true;
       final ReviewDb c = open();
diff --git a/gerrit-sshd/pom.xml b/gerrit-sshd/pom.xml
index 004ad83..2bb4713 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.3</version>
+    <version>2.1.4-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-sshd</artifactId>
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePasswordAuth.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePasswordAuth.java
deleted file mode 100644
index a346ab6..0000000
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePasswordAuth.java
+++ /dev/null
@@ -1,116 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.sshd;
-
-import com.google.gerrit.reviewdb.AccountExternalId;
-import com.google.gerrit.server.AccessPath;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.sshd.SshScope.Context;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.Singleton;
-
-import org.apache.mina.core.future.IoFuture;
-import org.apache.mina.core.future.IoFutureListener;
-import org.apache.sshd.server.PasswordAuthenticator;
-import org.apache.sshd.server.session.ServerSession;
-
-import java.net.SocketAddress;
-
-/**
- * Authenticates by password through {@link AccountExternalId} entities.
- */
-@Singleton
-class DatabasePasswordAuth implements PasswordAuthenticator {
-  private final AccountCache accountCache;
-  private final SshLog log;
-  private final IdentifiedUser.GenericFactory userFactory;
-
-  @Inject
-  DatabasePasswordAuth(final AccountCache ac, final SshLog l,
-      final IdentifiedUser.GenericFactory uf) {
-    accountCache = ac;
-    log = l;
-    userFactory = uf;
-  }
-
-  @Override
-  public boolean authenticate(final String username, final String password,
-      final ServerSession session) {
-    final SshSession sd = session.getAttribute(SshSession.KEY);
-
-    AccountState state = accountCache.getByUsername(username);
-    if (state == null) {
-      sd.authenticationError(username, "user-not-found");
-      return false;
-    }
-
-    final String p = state.getPassword(username);
-    if (p == null) {
-      sd.authenticationError(username, "no-password");
-      return false;
-    }
-
-    if (!p.equals(password)) {
-      sd.authenticationError(username, "incorrect-password");
-      return false;
-    }
-
-    if (sd.getCurrentUser() == null) {
-      sd.authenticationSuccess(username, createUser(sd, state));
-
-      // If this is the first time we've authenticated this
-      // session, record a login event in the log and add
-      // a close listener to record a logout event.
-      //
-      Context ctx = new Context(sd, null);
-      Context old = SshScope.set(ctx);
-      try {
-        log.onLogin();
-      } finally {
-        SshScope.set(old);
-      }
-
-      session.getIoSession().getCloseFuture().addListener(
-          new IoFutureListener<IoFuture>() {
-            @Override
-            public void operationComplete(IoFuture future) {
-              final Context ctx = new Context(sd, null);
-              final Context old = SshScope.set(ctx);
-              try {
-                log.onLogout();
-              } finally {
-                SshScope.set(old);
-              }
-            }
-          });
-    }
-
-    return true;
-  }
-
-  private IdentifiedUser createUser(final SshSession sd,
-      final AccountState state) {
-    return userFactory.create(AccessPath.SSH_COMMAND,
-        new Provider<SocketAddress>() {
-          @Override
-          public SocketAddress get() {
-            return sd.getRemoteAddress();
-          }
-        }, state.getAccount().getId());
-  }
-}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshCurrentUserProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshCurrentUserProvider.java
index e2797ef..5839cf1 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshCurrentUserProvider.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshCurrentUserProvider.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.sshd;
 
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -22,14 +23,20 @@
 @Singleton
 class SshCurrentUserProvider implements Provider<CurrentUser> {
   private final Provider<SshSession> session;
+  private final Provider<IdentifiedUser> identifiedProvider;
 
   @Inject
-  SshCurrentUserProvider(final Provider<SshSession> s) {
+  SshCurrentUserProvider(Provider<SshSession> s, Provider<IdentifiedUser> p) {
     session = s;
+    identifiedProvider = p;
   }
 
   @Override
   public CurrentUser get() {
+    final CurrentUser user = session.get().getCurrentUser();
+    if (user instanceof IdentifiedUser) {
+      return identifiedProvider.get();
+    }
     return session.get().getCurrentUser();
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
index 35fd229..2636ff2 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
@@ -59,10 +59,8 @@
 import org.apache.sshd.server.Command;
 import org.apache.sshd.server.CommandFactory;
 import org.apache.sshd.server.ForwardingFilter;
-import org.apache.sshd.server.PasswordAuthenticator;
 import org.apache.sshd.server.PublickeyAuthenticator;
 import org.apache.sshd.server.UserAuth;
-import org.apache.sshd.server.auth.UserAuthPassword;
 import org.apache.sshd.server.auth.UserAuthPublicKey;
 import org.apache.sshd.server.channel.ChannelDirectTcpip;
 import org.apache.sshd.server.channel.ChannelSession;
@@ -119,7 +117,6 @@
 
   @Inject
   SshDaemon(final CommandFactory commandFactory,
-      final PasswordAuthenticator passAuth,
       final PublickeyAuthenticator userAuth,
       final KeyPairProvider hostKeyProvider, final IdGenerator idGenerator,
       @GerritServerConfig final Config cfg, final SshLog sshLog) {
@@ -141,7 +138,7 @@
     initForwardingFilter();
     initSubsystems();
     initCompression();
-    initUserAuth(passAuth, userAuth);
+    initUserAuth(userAuth);
     setKeyPairProvider(hostKeyProvider);
     setCommandFactory(commandFactory);
     setShellFactory(new NoShell());
@@ -459,11 +456,9 @@
   }
 
   @SuppressWarnings("unchecked")
-  private void initUserAuth(final PasswordAuthenticator pass,
-      final PublickeyAuthenticator pubkey) {
-    setUserAuthFactories(Arrays.<NamedFactory<UserAuth>> asList(
-        new UserAuthPublicKey.Factory(), new UserAuthPassword.Factory()));
-    setPasswordAuthenticator(pass);
+  private void initUserAuth(final PublickeyAuthenticator pubkey) {
+    setUserAuthFactories(Arrays
+        .<NamedFactory<UserAuth>> asList(new UserAuthPublicKey.Factory()));
     setPublickeyAuthenticator(pubkey);
   }
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshIdentifiedUserProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshIdentifiedUserProvider.java
index f3da92c..516b59c 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshIdentifiedUserProvider.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshIdentifiedUserProvider.java
@@ -25,17 +25,21 @@
 @Singleton
 class SshIdentifiedUserProvider implements Provider<IdentifiedUser> {
   private final Provider<SshSession> session;
+  private final IdentifiedUser.RequestFactory factory;
 
   @Inject
-  SshIdentifiedUserProvider(final Provider<SshSession> s) {
+  SshIdentifiedUserProvider(Provider<SshSession> s,
+      IdentifiedUser.RequestFactory f) {
     session = s;
+    factory = f;
   }
 
   @Override
   public IdentifiedUser get() {
     final CurrentUser user = session.get().getCurrentUser();
     if (user instanceof IdentifiedUser) {
-      return (IdentifiedUser) user;
+      return factory.create(user.getAccessPath(), //
+          ((IdentifiedUser) user).getAccountId());
     }
     throw new ProvisionException(NotSignedInException.MESSAGE,
         new NotSignedInException());
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
index 7417cfc..c5f64f7 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
@@ -22,7 +22,7 @@
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.SelfPopulatingCache;
+import com.google.gerrit.server.cache.EntryCreator;
 import com.google.gerrit.server.ssh.SshKeyCache;
 import com.google.gwtorm.client.OrmException;
 import com.google.gwtorm.client.SchemaFactory;
@@ -59,7 +59,7 @@
       protected void configure() {
         final TypeLiteral<Cache<String, Iterable<SshKeyCacheEntry>>> type =
             new TypeLiteral<Cache<String, Iterable<SshKeyCacheEntry>>>() {};
-        core(type, CACHE_NAME);
+        core(type, CACHE_NAME).populateWith(Loader.class);
         bind(SshKeyCacheImpl.class);
         bind(SshKeyCache.class).to(SshKeyCacheImpl.class);
       }
@@ -71,33 +71,20 @@
         .asList(new SshKeyCacheEntry[0]));
   }
 
-  private final SchemaFactory<ReviewDb> schema;
-  private final SelfPopulatingCache<String, Iterable<SshKeyCacheEntry>> self;
+  private final Cache<String, Iterable<SshKeyCacheEntry>> cache;
 
   @Inject
-  SshKeyCacheImpl(final SchemaFactory<ReviewDb> schema,
-      @Named(CACHE_NAME) final Cache<String, Iterable<SshKeyCacheEntry>> raw) {
-    this.schema = schema;
-    self = new SelfPopulatingCache<String, Iterable<SshKeyCacheEntry>>(raw) {
-      @Override
-      protected Iterable<SshKeyCacheEntry> createEntry(final String username)
-          throws Exception {
-        return lookup(username);
-      }
-
-      @Override
-      protected Iterable<SshKeyCacheEntry> missing(final String username) {
-        return Collections.emptyList();
-      }
-    };
+  SshKeyCacheImpl(
+      @Named(CACHE_NAME) final Cache<String, Iterable<SshKeyCacheEntry>> cache) {
+    this.cache = cache;
   }
 
   public Iterable<SshKeyCacheEntry> get(String username) {
-    return self.get(username);
+    return cache.get(username);
   }
 
   public void evict(String username) {
-    self.remove(username);
+    cache.remove(username);
   }
 
   @Override
@@ -120,52 +107,68 @@
     }
   }
 
-  private Iterable<SshKeyCacheEntry> lookup(final String username)
-      throws Exception {
-    final ReviewDb db = schema.open();
-    try {
-      final AccountExternalId.Key key =
-          new AccountExternalId.Key(SCHEME_USERNAME, username);
-      final AccountExternalId user = db.accountExternalIds().get(key);
-      if (user == null) {
-        return NO_SUCH_USER;
-      }
+  static class Loader extends EntryCreator<String, Iterable<SshKeyCacheEntry>> {
+    private final SchemaFactory<ReviewDb> schema;
 
-      final List<SshKeyCacheEntry> kl = new ArrayList<SshKeyCacheEntry>(4);
-      for (AccountSshKey k : db.accountSshKeys().byAccount(user.getAccountId())) {
-        if (k.isValid()) {
-          add(db, kl, k);
+    @Inject
+    Loader(SchemaFactory<ReviewDb> schema) {
+      this.schema = schema;
+    }
+
+    @Override
+    public Iterable<SshKeyCacheEntry> createEntry(String username)
+        throws Exception {
+      final ReviewDb db = schema.open();
+      try {
+        final AccountExternalId.Key key =
+            new AccountExternalId.Key(SCHEME_USERNAME, username);
+        final AccountExternalId user = db.accountExternalIds().get(key);
+        if (user == null) {
+          return NO_SUCH_USER;
         }
-      }
-      if (kl.isEmpty()) {
-        return NO_KEYS;
-      }
-      return Collections.unmodifiableList(kl);
-    } finally {
-      db.close();
-    }
-  }
 
-  private void add(ReviewDb db, List<SshKeyCacheEntry> kl, AccountSshKey k) {
-    try {
-      kl.add(new SshKeyCacheEntry(k.getKey(), SshUtil.parse(k)));
-    } catch (OutOfMemoryError e) {
-      // This is the only case where we assume the problem has nothing
-      // to do with the key object, and instead we must abort this load.
-      //
-      throw e;
-    } catch (Throwable e) {
-      markInvalid(db, k);
+        final List<SshKeyCacheEntry> kl = new ArrayList<SshKeyCacheEntry>(4);
+        for (AccountSshKey k : db.accountSshKeys().byAccount(
+            user.getAccountId())) {
+          if (k.isValid()) {
+            add(db, kl, k);
+          }
+        }
+        if (kl.isEmpty()) {
+          return NO_KEYS;
+        }
+        return Collections.unmodifiableList(kl);
+      } finally {
+        db.close();
+      }
     }
-  }
 
-  private void markInvalid(final ReviewDb db, final AccountSshKey k) {
-    try {
-      log.info("Flagging SSH key " + k.getKey() + " invalid");
-      k.setInvalid();
-      db.accountSshKeys().update(Collections.singleton(k));
-    } catch (OrmException e) {
-      log.error("Failed to mark SSH key" + k.getKey() + " invalid", e);
+    @Override
+    public Iterable<SshKeyCacheEntry> missing(String username) {
+      return Collections.emptyList();
+    }
+
+    private void add(ReviewDb db, List<SshKeyCacheEntry> kl, AccountSshKey k) {
+      try {
+        kl.add(new SshKeyCacheEntry(k.getKey(), SshUtil.parse(k)));
+      } catch (OutOfMemoryError e) {
+        // This is the only case where we assume the problem has nothing
+        // to do with the key object, and instead we must abort this load.
+        //
+        throw e;
+      } catch (Throwable e) {
+        markInvalid(db, k);
+      }
+    }
+
+    private void markInvalid(final ReviewDb db, final AccountSshKey k) {
+      try {
+        log.info("Flagging SSH key " + k.getKey() + " invalid");
+        k.setInvalid();
+        db.accountSshKeys().update(Collections.singleton(k));
+      } catch (OrmException e) {
+        log.error("Failed to mark SSH key" + k.getKey() + " invalid", e);
+      }
     }
   }
 }
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 15fc093..40d271b 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
@@ -40,14 +40,12 @@
 import com.google.gerrit.util.cli.OptionHandlerFactory;
 import com.google.gerrit.util.cli.OptionHandlerUtil;
 import com.google.inject.Key;
-import com.google.inject.Scopes;
 import com.google.inject.TypeLiteral;
 import com.google.inject.assistedinject.FactoryProvider;
 import com.google.inject.servlet.RequestScoped;
 
 import org.apache.sshd.common.KeyPairProvider;
 import org.apache.sshd.server.CommandFactory;
-import org.apache.sshd.server.PasswordAuthenticator;
 import org.apache.sshd.server.PublickeyAuthenticator;
 import org.kohsuke.args4j.spi.OptionHandler;
 
@@ -78,7 +76,6 @@
     bind(QueueProvider.class).to(CommandExecutorQueueProvider.class).in(SINGLETON);
 
     bind(PublickeyAuthenticator.class).to(DatabasePubKeyAuth.class);
-    bind(PasswordAuthenticator.class).to(DatabasePasswordAuth.class);
     bind(KeyPairProvider.class).toProvider(HostKeyProvider.class).in(SINGLETON);
     bind(TransferConfig.class);
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
index a04ba15..cdcaf56 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
@@ -36,6 +36,7 @@
     command(gerrit).toProvider(new DispatchCommandProvider(gerrit));
     command(gerrit, "flush-caches").to(AdminFlushCaches.class);
     command(gerrit, "ls-projects").to(ListProjects.class);
+    command(gerrit, "query").to(Query.class);
     command(gerrit, "show-caches").to(AdminShowCaches.class);
     command(gerrit, "show-connections").to(AdminShowConnections.class);
     command(gerrit, "show-queue").to(ShowQueue.class);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java
new file mode 100644
index 0000000..d2bf26c
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java
@@ -0,0 +1,71 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd.commands;
+
+import com.google.gerrit.server.query.change.QueryProcessor;
+import com.google.gerrit.sshd.BaseCommand;
+import com.google.inject.Inject;
+
+import org.apache.sshd.server.Environment;
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
+
+import java.util.List;
+
+class Query extends BaseCommand {
+  @Inject
+  private QueryProcessor processor;
+
+  @Option(name = "--format", metaVar = "FMT", usage = "Output display format")
+  void setFormat(QueryProcessor.OutputFormat format) {
+    processor.setOutput(out, format);
+  }
+
+  @Option(name = "--current-patch-set", usage = "Include information about current patch set")
+  void setCurrentPatchSet(boolean on) {
+    processor.setIncludeCurrentPatchSet(on);
+  }
+
+  @Option(name = "--patch-sets", usage = "Include information about all patch sets")
+  void setPatchSets(boolean on) {
+    processor.setIncludePatchSets(on);
+  }
+
+  @Argument(index = 0, required = true, multiValued = true, metaVar = "QUERY", usage = "Query to execute")
+  private List<String> query;
+
+  @Override
+  public void start(Environment env) {
+    startThread(new CommandRunnable() {
+      @Override
+      public void run() throws Exception {
+        processor.setOutput(out, QueryProcessor.OutputFormat.TEXT);
+        parseCommandLine();
+        processor.query(join(query, " "));
+      }
+    });
+  }
+
+  private static String join(List<String> list, String sep) {
+    StringBuilder r = new StringBuilder();
+    for (int i = 0; i < list.size(); i++) {
+      if (i > 0) {
+        r.append(sep);
+      }
+      r.append(list.get(i));
+    }
+    return r.toString();
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
index 4bc3686..a0b9d25 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.MergeOp;
 import com.google.gerrit.server.git.MergeQueue;
 import com.google.gerrit.server.patch.PublishComments;
 import com.google.gerrit.server.project.CanSubmitResult;
@@ -96,6 +97,9 @@
   private MergeQueue merger;
 
   @Inject
+  private MergeOp.Factory opFactory;
+
+  @Inject
   private ApprovalTypes approvalTypes;
 
   @Inject
@@ -166,7 +170,7 @@
           changeControl.canSubmit(patchSetId, db, approvalTypes,
               functionStateFactory);
       if (result == CanSubmitResult.OK) {
-        ChangeUtil.submit(patchSetId, currentUser, db, merger);
+        ChangeUtil.submit(opFactory, patchSetId, currentUser, db, merger);
       } else {
         throw error(result.getMessage());
       }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
index 6e6e600..e9e6015 100755
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
@@ -16,8 +16,8 @@
 
 import com.google.gerrit.common.ChangeHookRunner;
 import com.google.gerrit.common.ChangeListener;
-import com.google.gerrit.common.ChangeHookRunner.ChangeEvent;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.events.ChangeEvent;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.git.WorkQueue.CancelableRunnable;
 import com.google.gerrit.sshd.BaseCommand;
diff --git a/gerrit-util-cli/pom.xml b/gerrit-util-cli/pom.xml
index a6d5cce..20f28e1 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.3</version>
+    <version>2.1.4-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-util-cli</artifactId>
diff --git a/gerrit-util-ssl/pom.xml b/gerrit-util-ssl/pom.xml
index 35f6188..220ae47 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.3</version>
+    <version>2.1.4-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-util-ssl</artifactId>
diff --git a/gerrit-war/pom.xml b/gerrit-war/pom.xml
index f629085..4bfa17e 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.3</version>
+    <version>2.1.4-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-war</artifactId>
@@ -155,6 +155,7 @@
         <configuration>
           <warName>gerrit-${project.version}</warName>
           <archiveClasses>true</archiveClasses>
+          <attachClasses>true</attachClasses>
           <archive>
             <addMavenDescriptor>false</addMavenDescriptor>
             <manifestEntries>
diff --git a/pom.xml b/pom.xml
index 6123b50..0ef6d2d 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.3</version>
+  <version>2.1.4-SNAPSHOT</version>
 
   <name>Gerrit Code Review - Parent</name>
   <url>http://code.google.com/p/gerrit/</url>
@@ -46,12 +46,12 @@
   </issueManagement>
 
   <properties>
-    <jgitVersion>0.8.4</jgitVersion>
+    <jgitVersion>0.8.4.87-g395d236</jgitVersion>
     <gwtormVersion>1.1.4</gwtormVersion>
     <gwtjsonrpcVersion>1.2.2</gwtjsonrpcVersion>
     <gwtexpuiVersion>1.2.1</gwtexpuiVersion>
-    <gwtVersion>2.0.3</gwtVersion>
-    <slf4jVersion>1.5.8</slf4jVersion>
+    <gwtVersion>2.0.4</gwtVersion>
+    <slf4jVersion>1.6.1</slf4jVersion>
     <guiceVersion>2.0</guiceVersion>
     <jettyVersion>7.0.2.v20100331</jettyVersion>
     <keyappletVersion>1.0</keyappletVersion>
@@ -82,6 +82,7 @@
     <module>gerrit-reviewdb</module>
     <module>gerrit-server</module>
     <module>gerrit-sshd</module>
+    <module>gerrit-gwtdebug</module>
     <module>gerrit-war</module>
 
     <module>gerrit-gwtui</module>
@@ -402,12 +403,6 @@
       <artifactId>easymock</artifactId>
       <scope>test</scope>
     </dependency>
-
-    <dependency>
-      <groupId>org.easymock</groupId>
-      <artifactId>easymockclassextension</artifactId>
-      <scope>test</scope>
-    </dependency>
   </dependencies>
 
   <dependencyManagement>
@@ -554,6 +549,12 @@
       </dependency>
 
       <dependency>
+        <groupId>commons-lang</groupId>
+        <artifactId>commons-lang</artifactId>
+        <version>2.5</version>
+      </dependency>
+
+      <dependency>
         <groupId>eu.medsea.mimeutil</groupId>
         <artifactId>mime-util</artifactId>
         <version>2.1.3</version>
@@ -606,7 +607,7 @@
       <dependency>
         <groupId>log4j</groupId>
         <artifactId>log4j</artifactId>
-        <version>1.2.15</version>
+        <version>1.2.16</version>
         <exclusions>
           <exclusion>
             <groupId>javax.mail</groupId>
@@ -661,7 +662,7 @@
       <dependency>
         <groupId>junit</groupId>
         <artifactId>junit</artifactId>
-        <version>3.8.2</version>
+        <version>4.8.1</version>
       </dependency>
 
       <dependency>
@@ -692,13 +693,7 @@
       <dependency>
         <groupId>org.easymock</groupId>
         <artifactId>easymock</artifactId>
-        <version>2.5.1</version>
-      </dependency>
-
-      <dependency>
-        <groupId>org.easymock</groupId>
-        <artifactId>easymockclassextension</artifactId>
-        <version>2.4</version>
+        <version>3.0</version>
       </dependency>
 
       <dependency>
@@ -736,6 +731,12 @@
         <artifactId>juniversalchardet</artifactId>
         <version>1.0.3</version>
       </dependency>
+
+      <dependency>
+        <groupId>dk.brics.automaton</groupId>
+        <artifactId>automaton</artifactId>
+        <version>1.11.2</version>
+      </dependency>
     </dependencies>
   </dependencyManagement>
 
@@ -769,5 +770,10 @@
       <id>objectweb-repository</id>
       <url>http://maven.objectweb.org/maven2/</url>
     </repository>
+
+    <repository>
+      <id>clojars-repo</id>
+      <url>http://clojars.org/repo</url>
+    </repository>
   </repositories>
 </project>
diff --git a/tools/gwtui_dbg.launch b/tools/gwtui_dbg.launch
index e1b6716..58f5ec8 100644
--- a/tools/gwtui_dbg.launch
+++ b/tools/gwtui_dbg.launch
@@ -2,19 +2,19 @@
 <launchConfiguration type="org.eclipse.jdt.launching.localJavaApplication">
 <stringAttribute key="bad_container_name" value="/gerrit-appja"/>
 <listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
-<listEntry value="/gerrit-gwtdbug"/>
+<listEntry value="/gerrit-gwtdebug"/>
 </listAttribute>
 <listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
 <listEntry value="4"/>
 </listAttribute>
 <booleanAttribute key="org.eclipse.debug.core.appendEnvironmentVariables" value="true"/>
 <stringAttribute key="org.eclipse.debug.core.source_locator_id" value="org.eclipse.jdt.launching.sourceLocator.JavaSourceLookupDirector"/>
-<stringAttribute key="org.eclipse.debug.core.source_locator_memento" value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;sourceLookupDirector&gt;&#10;&lt;sourceContainers duplicates=&quot;false&quot;&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-common&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-httpd&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-patch-commonsnet&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-prettify&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-patch-jgit&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-pgm&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-server&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-sshd&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-util-cli&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-util-ssl&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-reviewdb&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-gwtui&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-gwtdbug&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;default/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.debug.core.containerType.default&quot;/&gt;&#10;&lt;/sourceContainers&gt;&#10;&lt;/sourceLookupDirector&gt;&#10;"/>
+<stringAttribute key="org.eclipse.debug.core.source_locator_memento" value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;sourceLookupDirector&gt;&#10;&lt;sourceContainers duplicates=&quot;false&quot;&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-common&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-httpd&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-patch-commonsnet&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-prettify&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-patch-jgit&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-pgm&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-server&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-sshd&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-util-cli&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-util-ssl&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-reviewdb&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-gwtui&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-gwtdebug&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;default/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.debug.core.containerType.default&quot;/&gt;&#10;&lt;/sourceContainers&gt;&#10;&lt;/sourceLookupDirector&gt;&#10;"/>
 <booleanAttribute key="org.eclipse.jdt.debug.ui.CONSIDER_INHERITED_MAIN" value="true"/>
 <listAttribute key="org.eclipse.jdt.launching.CLASSPATH">
 <listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry containerPath=&quot;org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6&quot; path=&quot;1&quot; type=&quot;4&quot;/&gt;&#10;"/>
-<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry path=&quot;3&quot; projectName=&quot;gerrit-gwtui&quot; type=&quot;1&quot;/&gt;&#10;"/>
-<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry path=&quot;3&quot; projectName=&quot;gerrit-gwtdbug&quot; type=&quot;1&quot;/&gt;&#10;"/>
+<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry path=&quot;3&quot; projectName=&quot;gerrit-gwtdebug&quot; type=&quot;1&quot;/&gt;&#10;"/>
+<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry path=&quot;3&quot; projectName=&quot;gerrit-war&quot; type=&quot;1&quot;/&gt;&#10;"/>
 <listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry internalArchive=&quot;/gerrit-prettify/src/main/java&quot; path=&quot;3&quot; type=&quot;2&quot;/&gt;&#10;"/>
 <listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry internalArchive=&quot;/gerrit-patch-jgit/src/main/java&quot; path=&quot;3&quot; type=&quot;2&quot;/&gt;&#10;"/>
 <listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry internalArchive=&quot;/gerrit-reviewdb/src/main/java&quot; path=&quot;3&quot; type=&quot;2&quot;/&gt;&#10;"/>
@@ -24,12 +24,13 @@
 <listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry internalArchive=&quot;/gwtexpui/src/main/java&quot; path=&quot;3&quot; type=&quot;2&quot;/&gt;&#10;"/>
 <listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry internalArchive=&quot;/gwtjsonrpc/src/main/java&quot; path=&quot;3&quot; type=&quot;2&quot;/&gt;&#10;"/>
 <listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry internalArchive=&quot;/gwtorm/src/main/java&quot; path=&quot;3&quot; type=&quot;2&quot;/&gt;&#10;"/>
+<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry internalArchive=&quot;/gerrit-gwtui/target/classes&quot; path=&quot;3&quot; type=&quot;2&quot;/&gt;&#10;"/>
 </listAttribute>
 <stringAttribute key="org.eclipse.jdt.launching.CLASSPATH_PROVIDER" value="org.maven.ide.eclipse.launchconfig.classpathProvider"/>
 <booleanAttribute key="org.eclipse.jdt.launching.DEFAULT_CLASSPATH" value="false"/>
 <stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="com.google.gwt.dev.DevMode"/>
 <stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="-startupUrl /&#10;-war ${resource_loc:/gerrit-gwtui/target}/gwt-hosted-mode&#10;-server com.google.gerrit.gwtdebug.GerritDebugLauncher&#10;com.google.gerrit.GerritGwtUI"/>
-<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="gerrit-gwtdbug"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="gerrit-gwtdebug"/>
 <stringAttribute key="org.eclipse.jdt.launching.SOURCE_PATH_PROVIDER" value="org.maven.ide.eclipse.launchconfig.sourcepathProvider"/>
 <stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-Xmx256M&#10;&#10;-Dgerrit.site_path=${resource_loc:/gerrit-parent}/../test_site"/>
 </launchConfiguration>