Merge branch 'stable-2.6' into stable-2.7

* stable-2.6:
  Use rev-parse to find gitdir when generating commit-msg hook hint

Change-Id: Ib373c4f0004ba8ab635e1995e6af2049d5f03ca3
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index 100e473..ab32d78 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -775,6 +775,10 @@
 above) must enable submit, and also must not block it.  See above for
 details on each label.
 
+To link:user-upload.html#auto_merge[immediately submit a change on push]
+the caller needs to have the Submit permission on `refs/for/<ref>`
+(e.g. on `refs/for/refs/heads/master`).
+
 
 [[category_view_drafts]]
 View Drafts
@@ -1312,6 +1316,15 @@
 replication plugin is installed on the server.
 
 
+[[capability_streamEvents]]
+Stream Events
+~~~~~~~~~~~~~
+
+Allow performing streaming of Gerrit events. This capability
+allows the granted group to
+link:cmd-stream-events.html[stream Gerrit events via ssh].
+
+
 [[capability_viewCaches]]
 View Caches
 ~~~~~~~~~~~
diff --git a/Documentation/cmd-stream-events.txt b/Documentation/cmd-stream-events.txt
index ce23da6..6da0ef0 100644
--- a/Documentation/cmd-stream-events.txt
+++ b/Documentation/cmd-stream-events.txt
@@ -23,7 +23,9 @@
 
 ACCESS
 ------
-Any user who has configured an SSH key.
+Caller must be a member of the privileged 'Administrators' group,
+or have been granted
+link:access-control.html#capability_streamEvents[the 'Stream Events' global capability].
 
 SCRIPTING
 ---------
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index dd3a6ea..93492ba 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -744,6 +744,11 @@
   html = $1<a href=\"http://trak.example.com/$2\">$2</a>
 ----
 
+Comment links can also be specified in `project.config` and sections in
+children override those in parents. The only restriction is that to
+avoid injecting arbitrary user-supplied HTML in the page, comment links
+defined in `project.config` may only supply `link`, not `html`.
+
 [[commentlink.name.match]]commentlink.<name>.match::
 +
 A JavaScript regular expression to match positions to be replaced
@@ -779,6 +784,21 @@
 The configuration file eats double quotes, so escaping them as
 `\"` is necessary to protect them from the parser.
 
+[[commentlink.name.enabled]]commentlink.<name>.enabled::
++
+Whether the comment link is enabled. A child project may override a
+section in a parent or the site-wide config that is disabled by
+specifying `enabled = true`.
++
+Disabling sections in `gerrit.config` can be used by site administrators
+to create a library of comment links with `html` set that are not
+user-supplied and thus can be verified to be XSS-free, but are only
+enabled for a subset of projects.
++
+Note that the names and contents of disabled sections are visible even
+to anonymous users via the
+link:rest-api-projects.html#get-config[REST API].
+
 
 [[contactstore]]Section contactstore
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1322,6 +1342,16 @@
 +
 Valid values are the characters '*', '(' and ')'.
 
+[[gitweb.linkDrafts]]gitweb.linkDrafts::
++
+Whether or not Gerrit should provide links to gitweb on draft patch sets.
++
+By default, Gerrit will show links to gitweb on all patch sets. If gitweb
+only allows publicly viewable references, set this to false to remove
+the links to draft patch sets from the change review screen.
++
+Valid values are "true" and "false," default is "true."
+
 [[groups]]Section groups
 ~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -2440,6 +2470,34 @@
 +
 By default, all supported MACs are available.
 
+[[sshd.kerberosKeytab]]sshd.kerberosKeytab::
++
+Enable kerberos authentication for SSH connections.  To permit
+kerberos authentication, the server must have a host principal
+(see `sshd.kerberosPrincipal`) which is acquired from a keytab.
+This must be provisioned by the kerberos administrators, and is
+typically installed into `/etc/krb5.keytab` on host machines.
++
+The keytab must contain at least one `host/` principal, typically
+using the host's canonical name. If it does not use the
+canonical name, the `sshd.kerberosPrincipal` should be configured
+with the correct name.
++
+By default, not set and so kerberos authentication is not enabled.
+
+[[sshd.kerberosPrincipal]]sshd.kerberosPrincipal::
++
+If kerberos authentication is enabled with `sshd.kerberosKeytab`,
+instead use the given principal name instead of the default.
+If the principal does not begin with `host/` a warning message is
+printed and may prevent successful authentication.
++
+This may be useful if the host is behind an IP load balancer or
+other SSH forwarding systems, since the principal name is constructed
+by the client and must match for kerberos authentication to work.
++
+By default, `host/canonical.host.name`
+
 [[suggest]] Section suggest
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -2735,7 +2793,7 @@
 +
 Other files support site customization.
 +
-* link:config-headerfooter.html[Site Header/Footer]
+* link:config-themes.html[Themes]
 
 GERRIT
 ------
diff --git a/Documentation/config-labels.txt b/Documentation/config-labels.txt
index 2fd7a95..1ff7e24 100644
--- a/Documentation/config-labels.txt
+++ b/Documentation/config-labels.txt
@@ -221,6 +221,14 @@
 If true, the lowest possible negative value for the label is copied
 forward when a new patch set is uploaded.
 
+[[label_copyMaxScore]]
+`label.Label-Name.copyMaxScore`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If true, the highest possible positive value for the label is copied
+forward when a new patch set is uploaded. This can be used to enable
+sticky approvals, reducing turn-around for trivial cleanups prior to
+submitting a change.
 
 [[label_canOverride]]
 `label.Label-Name.canOverride`
diff --git a/Documentation/config-headerfooter.txt b/Documentation/config-themes.txt
similarity index 87%
rename from Documentation/config-headerfooter.txt
rename to Documentation/config-themes.txt
index ae5d8f7..c102381 100644
--- a/Documentation/config-headerfooter.txt
+++ b/Documentation/config-themes.txt
@@ -1,29 +1,39 @@
-Gerrit Code Review - Site Customization
-=======================================
+Gerrit Code Review - Themes
+===========================
 
 Gerrit supports some customization of the HTML it sends to
 the browser, allowing organizations to alter the look and
 feel of the application to fit with their general scheme.
 
+Configuration can either be sitewide or per-project. Projects without a
+specified theme inherit from their parents, or from the sitewide theme
+for `All-Projects`.
+
+Sitewide themes are stored in `'$site_path'/etc`, and per-project
+themes are stored in `'$site_path'/themes/{project-name}`. Files are
+only served from a single theme directory; if you want to modify or
+extend an inherited theme, you must copy it into the appropriate
+per-project directory.
+
 HTML Header/Footer
 ------------------
 
 At startup Gerrit reads the following files (if they exist) and
 uses them to customize the HTML page it sends to clients:
 
-* `'$site_path'/etc/GerritSiteHeader.html`
+* `<theme-dir>/GerritSiteHeader.html`
 +
 HTML is inserted below the menu bar, but above any page content.
 This is a good location for an organizational logo, or links to
 other systems like bug tracking.
 
-* `'$site_path'/etc/GerritSiteFooter.html`
+* `<theme-dir>/GerritSiteFooter.html`
 +
 HTML is inserted at the bottom of the page, below all other content,
 but just above the footer rule and the "Powered by Gerrit Code
 Review (v....)" message shown at the extreme bottom.
 
-* `'$site_path'/etc/GerritSite.css`
+* `<theme-dir>/GerritSite.css`
 +
 The CSS rules are inlined into the top of the HTML page, inside
 of a `<style>` tag.  These rules can be used to support styling
diff --git a/Documentation/dev-eclipse.txt b/Documentation/dev-eclipse.txt
index 64e5935..019c78f 100644
--- a/Documentation/dev-eclipse.txt
+++ b/Documentation/dev-eclipse.txt
@@ -79,10 +79,9 @@
 Running Hosted Mode
 ~~~~~~~~~~~~~~~~~~~
 
-To debug the GWT code executing in the web browser, three additional Git
+To debug the GWT code executing in the web browser, two additional Git
 repositories need to be cloned.
 
-* https://gerrit.googlesource.com/gwtexpui
 * https://gerrit.googlesource.com/gwtjsonrpc
 * https://gerrit.googlesource.com/gwtorm
 
@@ -121,6 +120,18 @@
 to `DEVELOPMENT_BECOME_ANY_ACCOUNT` to disable OpenID and allow you to
 impersonate whatever account you otherwise would've used.
 
+* Gerrit site doesn't appear, only directory listing is shown. Web toolkit
+developer browser plugin is missing. If there is no warning, that browser
+plugin is missing with the suggestion to install it, you can install the
+right extension for your browser from the following locations:
++
+https://dl.google.com/dl/gwt/plugins/chrome/gwt-dev-plugin.crx[Chrome]
++
+link:https://dl.google.com/dl/gwt/plugins/firefox/gwt-dev-plugin.xpi[Firefox]
++
+link:http://dl.google.com/dl/gwt/plugins/ie/1.0.7263.20091208111100/gwt-dev-plugin.msi[IE]
++
+https://dl.google.com/dl/gwt/plugins/safari/gwt-dev-plugin.dmg[Safari]
 
 GERRIT
 ------
diff --git a/Documentation/dev-release-deploy-config.txt b/Documentation/dev-release-deploy-config.txt
index bc52d50..ffdb6ea 100644
--- a/Documentation/dev-release-deploy-config.txt
+++ b/Documentation/dev-release-deploy-config.txt
@@ -14,8 +14,7 @@
 
 * `gerrit-maven`:
 +
-Bucket to store Gerrit Subproject Artifacts (e.g. `gwtexpui`,
-`gwtjsonrpc` etc.).
+Bucket to store Gerrit Subproject Artifacts (e.g. `gwtjsonrpc` etc.).
 
 * `gerrit-plugins`:
 +
diff --git a/Documentation/dev-release.txt b/Documentation/dev-release.txt
index ae2aed6..cd7cd34 100644
--- a/Documentation/dev-release.txt
+++ b/Documentation/dev-release.txt
@@ -111,7 +111,6 @@
 
 The subprojects to be released are:
 
-* `gwtexpui`
 * `gwtjsonrpc`
 * `gwtorm`
 * `prolog-cafe`
@@ -139,16 +138,8 @@
 * Build the Gerrit WAR
 +
 ====
- rm -f ~/.m2/settings.xml
  ./tools/release.sh
 ====
-+
-[WARNING]
-========================================================================
-Make sure you are compiling the release for all browsers. Check in your
-Maven `~/.m2/settings.xml` file that no Maven profile is active that
-limits the compilation to a certain browser.
-========================================================================
 
 * Sanity check WAR
 * Test the new Gerrit version
diff --git a/Documentation/index.txt b/Documentation/index.txt
index 9b2d8eb..88b50fa 100644
--- a/Documentation/index.txt
+++ b/Documentation/index.txt
@@ -48,7 +48,7 @@
 * link:config-gerrit.html[System Settings]
 * link:config-contact.html[User Contact Information]
 * link:config-gitweb.html[Gitweb Integration]
-* link:config-headerfooter.html[Site Header/Footer]
+* link:config-themes.html[Themes]
 * link:config-sso.html[Single Sign-On Systems]
 * link:config-reverseproxy.html[Reverse Proxy]
 * link:config-hooks.html[Hooks]
diff --git a/Documentation/install.txt b/Documentation/install.txt
index 4b14ff0..a18a506 100644
--- a/Documentation/install.txt
+++ b/Documentation/install.txt
@@ -143,7 +143,7 @@
 
 * link:config-reverseproxy.html[Reverse Proxy]
 * link:config-sso.html[Single Sign-On Systems]
-* link:config-headerfooter.html[Site Header/Footer]
+* link:config-themes.html[Themes]
 * link:config-gitweb.html[Gitweb Integration]
 * link:config-gerrit.html[Other System Settings]
 
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
index 69a12c3..fbdd9cd 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -12,7 +12,6 @@
 |======================================================================
 |Included Package           | License
 |Gerrit Code Review         | <<apache2,Apache License 2.0>>
-|gwtexpui                   | <<apache2,Apache License 2.0>>
 |gwtjsonrpc                 | <<apache2,Apache License 2.0>>
 |gwtorm                     | <<apache2,Apache License 2.0>>
 |Google Gson                | <<apache2,Apache License 2.0>>
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index 2ccd39a..f17a741 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -257,6 +257,115 @@
   Location: https://profiles/avatar/john_doe.jpeg?s=20x20
 ----
 
+[[get-avatar-change-url]]
+Get Avatar Change URL
+~~~~~~~~~~~~~~~~~~~~~
+[verse]
+'GET /accounts/link:#account-id[\{account-id\}]/avatar.change.url'
+
+Retrieves the URL where the user can change the avatar image.
+
+.Request
+----
+  GET /a/accounts/self/avatar.change.url HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: text/plain;charset=UTF-8
+
+  https://profiles/pictures/john.doe
+----
+
+[[get-diff-preferences]]
+Get Diff Preferences
+~~~~~~~~~~~~~~~~~~~~
+[verse]
+'GET /accounts/link:#account-id[\{account-id\}]/preferences.diff'
+
+Retrieves the diff preferences of a user.
+
+.Request
+----
+  GET /a/accounts/self/preferences.diff HTTP/1.0
+----
+
+As result the diff preferences of the user are returned as a
+link:#diff-preferences-info[DiffPreferencesInfo] entity.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "context": 10,
+    "ignore_whitespace": "IGNORE_ALL_SPACE",
+    "intraline_difference": true,
+    "line_length": 100,
+    "show_tabs": true,
+    "show_whitespace_errors": true,
+    "syntax_highlighting": true,
+    "tab_size": 8
+  }
+----
+
+[[set-diff-preferences]]
+Set Diff Preferences
+~~~~~~~~~~~~~~~~~~~~
+[verse]
+'PUT /accounts/link:#account-id[\{account-id\}]/preferences.diff'
+
+Sets the diff preferences of a user.
+
+The new diff preferences must be provided in the request body as a
+link:#diff-preferences-input[DiffPreferencesInput] entity.
+
+.Request
+----
+  GET /a/accounts/self/preferences.diff HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "context": 10,
+    "ignore_whitespace": "IGNORE_ALL_SPACE",
+    "intraline_difference": true,
+    "line_length": 100,
+    "show_line_endings": true,
+    "show_tabs": true,
+    "show_whitespace_errors": true,
+    "syntax_highlighting": true,
+    "tab_size": 8
+  }
+----
+
+As result the new diff preferences of the user are returned as a
+link:#diff-preferences-info[DiffPreferencesInfo] entity.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "context": 10,
+    "ignore_whitespace": "IGNORE_ALL_SPACE",
+    "intraline_difference": true,
+    "line_length": 100,
+    "show_line_endings": true,
+    "show_tabs": true,
+    "show_whitespace_errors": true,
+    "syntax_highlighting": true,
+    "tab_size": 8
+  }
+----
+
 
 [[ids]]
 IDs
@@ -349,6 +458,99 @@
 capability.
 |=================================
 
+[[diff-preferences-info]]
+DiffPreferencesInfo
+~~~~~~~~~~~~~~~~~~~
+The `DiffPreferencesInfo` entity contains information about the diff
+preferences of a user.
+
+[options="header",width="50%",cols="1,^1,5"]
+|=====================================
+|Field Name              ||Description
+|`context`               ||
+The number of lines of context when viewing a patch.
+|`expand_all_comments`   |not set if `false`|
+Whether all inline comments should be automatically expanded.
+|`ignore_whitespace`     ||
+Whether whitespace changes should be ignored and if yes, which
+whitespace changes should be ignored. +
+Allowed values are `IGNORE_NONE`, `IGNORE_SPACE_AT_EOL`,
+`IGNORE_SPACE_CHANGE`, `IGNORE_ALL_SPACE`.
+|`intraline_difference`  |not set if `false`|
+Whether intraline differences should be highlighted.
+|`line_length`           ||
+Number of characters that should be displayed in one line.
+|`manual_review`         |not set if `false`|
+Whether the 'Reviewed' flag should not be set automatically on a patch
+when it is viewed.
+|`retain_header`         |not set if `false`|
+Whether the header that is displayed above the patch (that either shows
+the commit message, the diff preferences, the patch sets or the files)
+should be retained on file switch.
+|`show_line_endings`     |not set if `false`|
+Whether Windows EOL/Cr-Lf should be displayed as '\r' in a dotted-line
+box.
+|`show_tabs`             |not set if `false`|
+Whether tabs should be shown.
+|`show_whitespace_errors`|not set if `false`|
+Whether whitespace errors should be shown.
+|`skip_deleted`          |not set if `false`|
+Whether deleted files should be skipped on file switch.
+|`skip_uncommented`      |not set if `false`|
+Whether uncommented files should be skipped on file switch.
+|`syntax_highlighting`   |not set if `false`|
+Whether syntax highlighting should be enabled.
+|`tab_size`              ||
+Number of spaces that should be used to display one tab.
+|=====================================
+
+[[diff-preferences-input]]
+DiffPreferencesInput
+~~~~~~~~~~~~~~~~~~~~
+The `DiffPreferencesInput` entity contains information for setting the
+diff preferences of a user. Fields which are not set will not be
+updated.
+
+[options="header",width="50%",cols="1,^1,5"]
+|=====================================
+|Field Name              ||Description
+|`context`               |optional|
+The number of lines of context when viewing a patch.
+|`expand_all_comments`   |optional|
+Whether all inline comments should be automatically expanded.
+|`ignore_whitespace`     |optional|
+Whether whitespace changes should be ignored and if yes, which
+whitespace changes should be ignored. +
+Allowed values are `IGNORE_NONE`, `IGNORE_SPACE_AT_EOL`,
+`IGNORE_SPACE_CHANGE`, `IGNORE_ALL_SPACE`.
+|`intraline_difference`  |optional|
+Whether intraline differences should be highlighted.
+|`line_length`           |optional|
+Number of characters that should be displayed in one line.
+|`manual_review`         |optional|
+Whether the 'Reviewed' flag should not be set automatically on a patch
+when it is viewed.
+|`retain_header`         |optional|
+Whether the header that is displayed above the patch (that either shows
+the commit message, the diff preferences, the patch sets or the files)
+should be retained on file switch.
+|`show_line_endings`     |optional|
+Whether Windows EOL/Cr-Lf should be displayed as '\r' in a dotted-line
+box.
+|`show_tabs`             |optional|
+Whether tabs should be shown.
+|`show_whitespace_errors`|optional|
+Whether whitespace errors should be shown.
+|`skip_deleted`          |optional|
+Whether deleted files should be skipped on file switch.
+|`skip_uncommented`      |optional|
+Whether uncommented files should be skipped on file switch.
+|`syntax_highlighting`   |optional|
+Whether syntax highlighting should be enabled.
+|`tab_size`              |optional|
+Number of spaces that should be used to display one tab.
+|=====================================
+
 [[query-limit-info]]
 QueryLimitInfo
 ~~~~~~~~~~~~~~
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 696a174..09c7e11 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -208,6 +208,11 @@
   referencing accounts.
 --
 
+[[messages]]
+--
+* `MESSAGES`: include messages associated with the change.
+--
+
 .Request
 ----
   GET /changes/?q=97&o=CURRENT_REVISION&o=CURRENT_COMMIT&o=CURRENT_FILES HTTP/1.0
@@ -356,7 +361,8 @@
 'GET /changes/link:#change-id[\{change-id\}]/detail'
 
 Retrieves a change with link:#labels[labels], link:#detailed-labels[
-detailed labels] and link:#detailed-accounts[detailed accounts].
+detailed labels], link:#detailed-accounts[detailed accounts], and
+link:#messages[messages].
 
 .Request
 ----
@@ -473,6 +479,30 @@
         "name": "Jane Roe",
         "email": "jane.roe@example.com"
       }
+    ],
+    "messages": [
+      {
+        "id": "YH-egE",
+        "author": {
+          "_account_id": 1000096,
+          "name": "John Doe",
+          "email": "john.doe@example.com"
+        },
+        "updated": "2013-03-23 21:34:02.419000000",
+        "message": "Patch Set 1:\n\nThis is the first message.",
+        "revision_number": 1
+      },
+      {
+        "id": "WEEdhU",
+        "author": {
+          "_account_id": 1000097,
+          "name": "Jane Roe",
+          "email": "jane.roe@example.com"
+        },
+        "updated": "2013-03-23 21:36:52.332000000",
+        "message": "Patch Set 1:\n\nThis is the second message.\n\nWith a line break.",
+        "revision_number": 1
+      }
     ]
   }
 ----
@@ -1518,6 +1548,99 @@
   HTTP/1.1 204 No Content
 ----
 
+[[list-comments]]
+List Comments
+~~~~~~~~~~~~~
+[verse]
+'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/comments/'
+
+Lists the published comments of a revision.
+
+As result a map is returned that maps the file path to a list of
+link:#comment-info[CommentInfo] entries. The entries in the map are
+sorted by file path.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/comments/ HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java": [
+      {
+        "kind": "gerritcodereview#comment",
+        "id": "TvcXrmjM",
+        "line": 23,
+        "message": "[nit] trailing whitespace",
+        "updated": "2013-02-26 15:40:43.986000000",
+        "author": {
+          "_account_id": 1000096,
+          "name": "John Doe",
+          "email": "john.doe@example.com"
+        }
+      },
+      {
+        "kind": "gerritcodereview#comment",
+        "id": "TveXwFiA",
+        "line": 49,
+        "in_reply_to": "TfYX-Iuo",
+        "message": "Done",
+        "updated": "2013-02-26 15:40:45.328000000",
+        "author": {
+          "_account_id": 1000097,
+          "name": "Jane Roe",
+          "email": "jane.roe@example.com"
+        }
+      }
+    ]
+  }
+----
+
+[[get-comment]]
+Get Comment
+~~~~~~~~~~~
+[verse]
+'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/comments/link:#comment-id[\{comment-id\}]'
+
+Retrieves a published comment of a revision.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/comments/TvcXrmjM HTTP/1.0
+----
+
+As response a link:#comment-info[CommentInfo] entity is returned that
+describes the published comment.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "kind": "gerritcodereview#comment",
+    "id": "TvcXrmjM",
+    "path": "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java",
+    "line": 23,
+    "message": "[nit] trailing whitespace",
+    "updated": "2013-02-26 15:40:43.986000000",
+    "author": {
+      "_account_id": 1000096,
+      "name": "John Doe",
+      "email": "john.doe@example.com"
+    }
+  }
+----
+
 [[set-reviewed]]
 Set Reviewed
 ~~~~~~~~~~~~
@@ -1582,6 +1705,11 @@
   ("I8473b95934b5732ac55d26311a706c9c2bde9940")
 * a legacy numeric change ID ("4247")
 
+[[comment-id]]
+\{comment-id\}
+~~~~~~~~~~~~~~
+UUID of a published comment.
+
 [[draft-id]]
 \{draft-id\}
 ~~~~~~~~~~~~
@@ -1717,6 +1845,10 @@
 The reviewers that can be removed by the calling user as a list of
 link:rest-api-accounts.html#account-info[AccountInfo] entities. +
 Only set if link:#detailed-labels[detailed labels] are requested.
+|`messages`|optional|
+Messages associated with the change as a list of 
+link:#change-message-info[ChangeMessageInfo] entities. +
+Only set if link:#messages[messages] are requested.
 |`current_revision`   |optional|
 The commit ID of the current patch set of this change. +
 Only set if link:#current-revision[the current revision] is requested
@@ -1730,6 +1862,27 @@
 Only set on either the last or the first change that is returned.
 |==================================
 
+[[change-message-info]]
+ChangeMessageInfo
+~~~~~~~~~~~~~~~~~
+The `ChangeMessageInfo` entity contains information about a message
+attached to a change.
+
+[options="header",width="50%",cols="1,^1,5"]
+|==================================
+|Field Name           ||Description
+|`id`                 ||The ID of the message.
+|`author`             |optional|
+Author of the message as an 
+link:rest-api-accounts.html#account-info[AccountInfo] entity. +
+Unset if written by the Gerrit system.
+|`date`            ||
+The link:rest-api.html#timestamp[timestamp] this message was posted.
+|`message`            ||The text left by the user.
+|`_revision_number`    |optional|
+Which patchset (if any) generated this message.
+|==================================
+
 [[comment-info]]
 CommentInfo
 ~~~~~~~~~~~
@@ -1739,7 +1892,7 @@
 |===========================
 |Field Name    ||Description
 |`kind`        ||`gerritcodereview#comment`
-|`id`          ||The URL encoded UUID of the draft comment.
+|`id`          ||The URL encoded UUID of the comment.
 |`path`        |optional|
 The path of the file for which the inline comment was done. +
 Not set if returned in a map where the key is the file path.
@@ -1756,6 +1909,10 @@
 |`updated`     ||
 The link:rest-api.html#timestamp[timestamp] of when this comment was
 written.
+|`author`      |optional|
+The author of the message as an +
+link:rest-api-accounts.html#account-info[AccountInfo] entity. +
+Unset for draft comments, assumed to be the calling user.
 |===========================
 
 [[comment-input]]
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 9aba0e9..35caac4 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -408,6 +408,58 @@
   }
 ----
 
+[[get-config]]
+Get Config
+~~~~~~~~~~
+[verse]
+'GET /projects/link:#project-name[\{project-name\}]/config'
+
+Gets some configuration information about a project. Note that this
+config info is not simply the contents of `project.config`; it generally
+contains fields that may have been inherited from parent projects.
+
+.Request
+----
+  GET /projects/myproject/config
+----
+
+A link:#config-info[ConfigInfo] entity is returned that describes the
+project configuration. Some fields are only visible to users that have
+read access to `refs/meta/config`.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "kind": "gerritcodereview#project_config",
+    "use_contributor_agreements": {
+      "value": true,
+      "configured_value": "TRUE",
+      "inherited_value": false
+    },
+    "use_content_merge": {
+      "value": true,
+      "configured_value": "INHERIT",
+      "inherited_value": true
+    },
+    "use_signed_off_by": {
+      "value": false,
+      "configured_value": "INHERIT",
+      "inherited_value": false
+    },
+    "require_change_id": {
+      "value": false,
+      "configured_value": "FALSE",
+      "inherited_value": true
+    }
+    "commentlinks": {}
+  }
+----
+
 [[run-gc]]
 Run GC
 ~~~~~~
@@ -693,6 +745,43 @@
 JSON Entities
 -------------
 
+[[config-info]]
+ConfigInfo
+~~~~~~~~~~
+The `ConfigInfo` entity contains information about the effective project
+configuration.
+
+Fields marked with * are only visible to users who have read access to
+`refs/meta/config`.
+
+[options="header",width="50%",cols="1,6"]
+|======================================
+|Field Name                   |Description
+|`use_contributor_agreements*`|
+link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
+authors must complete a contributor agreement on the site before
+pushing any commits or changes to this project.
+|`use_content_merge*`|
+link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
+Gerrit will try to perform a 3-way merge of text file content when a
+file has been modified by both the destination branch and the change
+being submitted. This option only takes effect if submit type is not
+FAST_FORWARD_ONLY.
+|`use_signed_off_by*`|
+link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
+each change must contain a Signed-off-by line from either the author or
+the uploader in the commit message.
+|`require_change_id*`|
+link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether a
+valid link:user-changeid.html[Change-Id] footer in any commit uploaded
+for review is required. This does not apply to commits pushed directly
+to a branch or tag.
+|`commentlinks`|
+Comment link configuration for the project. Has the same format as the
+link:config-gerrit.html#_a_id_commentlink_a_section_commentlink[commentlink section]
+of `gerrit.config`.
+|======================================
+
 [[dashboard-info]]
 DashboardInfo
 ~~~~~~~~~~~~~
@@ -775,6 +864,23 @@
 omitted.
 |============================
 
+[[inherited-boolean-info]]
+InheritedBooleanInfo
+~~~~~~~~~~~~~~~~~~~~
+A boolean value that can also be inherited.
+
+[options="header",width="50%",cols="1,^2,4"]
+|================================
+|Field Name         ||Description
+|`value`            ||
+The effective boolean value.
+|`configured_value` ||
+The configured value, can be `TRUE`, `FALSE` or `INHERITED`.
+|`inherited_value`  |optional|
+The boolean value inherited from the parent. +
+Not set if there is no parent.
+|================================
+
 [[project-description-input]]
 ProjectDescriptionInput
 ~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/Documentation/user-upload.txt b/Documentation/user-upload.txt
index 55ab895..cbb152c3 100644
--- a/Documentation/user-upload.txt
+++ b/Documentation/user-upload.txt
@@ -333,6 +333,40 @@
 make undesired changes to the public repository.
 
 
+[[auto_merge]]
+Auto-Merge during Push
+~~~~~~~~~~~~~~~~~~~~~~
+
+Changes can be directly submitted on push.  This is primarily useful
+for teams that don't want to do code review but want to use Gerrit's
+submit strategies to handle contention on busy branches.  Using
+`%submit` creates a change and submits it immediately, if the caller
+has link:access-control.html#category_submit[Submit] permission on
+`refs/for/<ref>` (e.g. on `refs/for/refs/heads/master`).
+
+====
+  git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/master%submit
+====
+
+On auto-merge of a change neither labels nor submit rules are checked.
+If the merge fails the change stays open, but when pushing a new patch
+set the merge can be reattempted by using `%submit` again.
+
+
+[[base]]
+Selecting Merge Base
+~~~~~~~~~~~~~~~~~~~~
+
+By default new changes are opened only for new unique commits
+that have never before been seen by the Gerrit server. Clients
+may override that behavior and force new changes to be created
+by setting the merge base SHA-1 using the '%base' argument:
+
+====
+  git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/master%base=$(git rev-parse origin/master)
+====
+
+
 repo upload
 -----------
 
diff --git a/ReleaseNotes/ReleaseNotes-2.7.txt b/ReleaseNotes/ReleaseNotes-2.7.txt
new file mode 100644
index 0000000..3a2ebf9
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.7.txt
@@ -0,0 +1,337 @@
+Release notes for Gerrit 2.7
+============================
+
+
+Gerrit 2.7 is now available:
+
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.7.war[
+https://gerrit-releases.storage.googleapis.com/gerrit-2.7.war]
+
+Gerrit 2.7 includes the bug fixes done with link:ReleaseNotes-2.6.1.html[Gerrit 2.6.1].
+These bug fixes are *not* listed in these release notes.
+
+Schema Change
+-------------
+
+
+*WARNING:* This release contains schema changes.  To upgrade:
+----
+  java -jar gerrit.war init -d site_path
+----
+
+*WARNING:* Upgrading to 2.7.x requires the server be first upgraded to 2.1.7 (or
+a later 2.1.x version), and then to 2.7.x.  If you are upgrading from 2.2.x.x or
+newer, you may ignore this warning and upgrade directly to 2.7.x.
+
+
+
+Gerrit Trigger Plugin in Jenkins
+--------------------------------
+
+
+*WARNING:* Upgrading to 2.7 may cause the Gerrit Trigger Plugin in Jenkins to
+stop working.  Please see the "New 'Stream Events' global capability" section
+below.
+
+
+Release Highlights
+------------------
+
+
+* New `copyMaxScore` setting for labels.
+* Comment links configurable per project.
+* Themes configurable per project.
+* Better support for binary files and images in diff screens.
+* User avatars in more places.
+* Several new REST APIs.
+
+
+New Features
+------------
+
+
+General
+~~~~~~~
+
+* New `copyMaxScore` setting for labels.
++
+Labels can be link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.7/config-labels.html#label_copyMaxScore[
+configured] to copy approvals forward to the next patch set.
+
+* Comment links can be link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.7/config-gerrit.html#commentlink[
+defined per project in the project configuration].
+
+* Gerrit administrators can define project-specific themes.
++
+Themes can be link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.7/config-themes.html[
+configured site-wide or per project].
+
+* New '/a/tools' URL.
++
+This allows users to download the `commit-msg` hook via the command line if the
+Gerrit server requires authentication globally.
+
+* New 'Stream Events' global capability.
++
+The link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.7/access-control.txt#capability_streamEvents[
+Stream Events capability] controls access to the `stream-events` ssh command.
++
+Only administrators and users having this capability are allowed to use `stream-events`.
++
+If you are using the Gerrit Trigger Plugin in Jenkins, you must make sure that the
+'Non-Interactive Users' group, or whichever group the Jenkins user belongs to, is
+given the 'Stream Events' capability.
+
+* Allow opening new changes on existing commits.
++
+The `%base` argument can be used with `refs/for/` to identify a specific revision the server should
+start to look for new commits at. Any commits in the range `$base..$tip` will be opened as a new
+change, even if the commit already has another change on a different branch.
+
+* New setting `gitweb.linkDrafts` to control if gitweb links are shown on drafts.
++
+By default, Gerrit will show links to gitweb on all patch sets.  If the
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.7/config-gerrit.html#gitweb.linkDrafts[
+gitweb.linkDrafts setting] is set to 'false', links will not be shown on
+draft patch sets.
+
+* Allow changes to be automatically submitted on push.
++
+Teams that want to use Gerrit's submit strategies to handle contention on busy
+branches can use `%submit` to create a change and have it
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.7/user-upload.html#auto_merge[
+immediately submitted], if the caller has Submit permission on `refs/for/<ref>`.
+
+* Allow administrators to see all groups.
+
+
+Web UI
+~~~~~~
+
+
+Global
+^^^^^^
+
+* User avatars are displayed in more places in the Web UI.
+
+* 'Diffy' is used as avatar for the Gerrit server itself.
+
+* A popup with user profile information is shown when hovering the
+mouse over avatar images.
+
+
+Change Screens
+^^^^^^^^^^^^^^
+
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=667[Issue 667]:
+Highlight patch sets that have drafts.
++
+Patch sets having unpublished draft comments are highlighted with an icon.
+
+* Option to show relative times in change tables.
++
+A new preference setting allows the user to decide if absolute or relative dates
+should be shown in change tables.
+
+* Option to set default visibility of change comments.
++
+A new preference setting allows the user to set the default visibility of
+change comments.
+
+
+Diff Screens
+^^^^^^^^^^^^
+
+* Show images in side-by-side and unified diffs.
+
+* Show diffed images above/below each other in unified diffs.
+
+* Harmonize unified diff's styling of images with that of text.
+
+
+REST API
+~~~~~~~~
+
+
+Several new link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.7/rest-api.html[
+REST API endpoints] are added.
+
+Accounts
+^^^^^^^^
+
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.7/rest-api-accounts.html#get-diff-preferences[
+Get account diff preferences]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.7/rest-api-accounts.html#set-diff-preferences[
+Set account diff preferences]
+
+
+Changes
+^^^^^^^
+
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1820[Issue 1820]:
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.7/rest-api-changes.html#list-comments[
+List comments]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.7/rest-api-changes.html#get-comment[
+Get comment]
+
+
+
+Projects
+^^^^^^^^
+
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.7/rest-api-projects.html#get-config[
+Get project configuration]
+
+
+ssh
+~~~
+
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1088[Issue 1088]:
+Support link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.7/config-gerrit.html#sshd.kerberosKeytab[
+Kerberos authentication for ssh interaction].
+
+
+Bug Fixes
+---------
+
+General
+~~~~~~~
+
+* Postpone check for first account until adding an account.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1848[Issue 1848]:
+Mark `ALREADY_MERGED` changes as merged in the database.
++
+If a change was marked `ALREADY_MERGED`, likely due to a bug in
+merge code, it does not end up in the list of changes to be submitted
+and never gets marked as merged despite the branch head already
+having advanced.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=600[Issue 600]:
+Fix change stuck in SUBMITTED state but actually merged.
++
+When submitting a commit that has a tag, it could not be merged.
+
+* Fix null-pointer exception when dashboard title is not specified.
++
+If the title is not specified, the path of the dashboard config file
+is used as title.
+
+* Allow label values to be configured with no text.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1966[Issue 1966]:
+Fix Gerrit plugins under Tomcat by avoiding Guice static filter.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2054[Issue 2054]:
+Expand capabilities of `ldap.groupMemberPattern`.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2098[Issue 2098]:
+Fix re-enabling of disabled plugins.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2128[Issue 2128]:
+Fix null-pointer exception when deleting draft patch set when previous
+draft was already deleted.
+
+
+Web UI
+~~~~~~
+
+
+* Properly handle double-click on external group in GroupTable.
++
+Double-clicking on an external group opens the group's URL (if it
+is provided).
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1848[Issue 1848]:
+Don't discard inline comments when escape key is pressed.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1863[Issue 1863]:
+Drop Arial Unicode MS font and request only sans-serif.
++
+Arial Unicode MS does not have a bold version. Selecting this font prevents
+correct display of bold text on Mac OS X. Simplify the selector to sans-serif
+and allow the browser to use the user's preferred font in this family.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1872[Issue 1872]:
+Fix tab expansion in diff screens when syntax coloring is on.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1904[Issue 1904]:
+Fix diff screens for files with CRLF line endings.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2056[Issue 2056]:
+Display custom NoOp label score for open changes.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2093[Issue 2093]:
+Fix incorrect title of "repo download" link on change screen.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2127[Issue 2127]:
+Remove hard-coded documentation links from the admin page.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2010[Issue 2010]:
+Fix null-pointer exception when searching for changes with the query
+`owner:self`.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2039[Issue 2039]:
+Fix browser null-pointer exception when ChangeCache is incomplete.
+
+
+REST API
+~~~~~~~~
+
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1819[Issue 1819]:
+Include change-level messages to the payload returned from
+the link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.7/rest-api-changes#get-change-detail[
+Get Change Detail REST API endpoint].
+
+* Correct URL encoding in 'GroupInfo'.
+
+
+Email
+~~~~~
+
+* Log failure to access reviewer list for notification emails.
+
+* Log when appropriate if email delivery is skipped.
+
+
+ssh
+~~~
+
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2016[Issue 2016]:
+Flush caches after adding or deleting ssh keys via the `set-account` ssh command.
+
+Tools
+~~~~~
+
+
+* The release build now builds for all browser configurations.
+
+
+Upgrades
+--------
+
+* `gwtexpui` is now built in the gerrit tree rather than linking a separate module.
+
+
+
+Documentation
+-------------
+
+
+* Update the access control documentation to clarify how to set
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.7/access-control.html#global_capabilities[
+global capabilities].
+
+* Clarify the
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.7/config-gerrit.html#cache_names[
+change cache configuration].
+
diff --git a/ReleaseNotes/index.txt b/ReleaseNotes/index.txt
index eb9fa15..b656be2 100644
--- a/ReleaseNotes/index.txt
+++ b/ReleaseNotes/index.txt
@@ -1,6 +1,11 @@
 Gerrit Code Review - Release Notes
 ==================================
 
+[[2_7]]
+Version 2.7.x
+-------------
+* link:ReleaseNotes-2.7.html[2.7]
+
 [[2_6]]
 Version 2.6.x
 -------------
diff --git a/contrib/trivial_rebase.py b/contrib/trivial_rebase.py
index 7764470..30e60af 100755
--- a/contrib/trivial_rebase.py
+++ b/contrib/trivial_rebase.py
@@ -131,7 +131,7 @@
     Returns a list of approval dicts.
 
     """
-    sql_query = ("\"SELECT value,account_id,category_id FROM patch_set_approvals "
+    sql_query = ("\"SELECT value,account_id,category_id AS label FROM patch_set_approvals "
                  "WHERE change_id = %s AND patch_set_id = %s AND value != 0\""
                  % (self.changeId, (self.patchset - 1)))
     gsql_out = self.GsqlQuery(sql_query)
@@ -221,19 +221,20 @@
       # Note: Sites with different 'copy_min_score' values in the
       # approval_categories DB table might want different behavior here.
       # Additional categories should also be added if desired.
-      if approval["category_id"] == "Code-Review" and approval['value'] != '-2':
-        self.AppendAcctApproval(approval['account_id'], '--code-review %s' % approval['value'])
-      elif approval["category_id"] == "Verified":
+      if approval["label"] == "Code-Review":
+        if approval['value'] != '-2':
+          self.AppendAcctApproval(approval['account_id'],
+                                  '--label Code-Review=%s' % approval['value'])
+      elif approval["label"] == "Verified":
         # Don't re-add verifies
-        # self.AppendAcctApproval(approval['account_id'], '--verified %s' % approval['value'])
+        # self.AppendAcctApproval(approval['account_id'], '--label Verified=%s' % approval['value'])
         continue
-      elif approval["category_id"] == "SUBM":
+      elif approval["label"] == "SUBM":
         # We don't care about previous submit attempts
         continue
       else:
-        self.AppendAcctApproval(approval['account_id'], '--%s %s' %
-                                (approval['category_id'].lower().replace(' ', '-'),
-                                 approval['value']))
+        self.AppendAcctApproval(approval['account_id'], '--label %s=%s' %
+                                (approval['label'], approval['value']))
 
     gerrit_review_msg = ("\'Automatically re-added by Gerrit trivial rebase "
                           "detection script.\'")
diff --git a/gerrit-acceptance-tests/pom.xml b/gerrit-acceptance-tests/pom.xml
index eb27301..63facba 100644
--- a/gerrit-acceptance-tests/pom.xml
+++ b/gerrit-acceptance-tests/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.6</version>
+    <version>2.7</version>
   </parent>
 
   <artifactId>gerrit-acceptance-tests</artifactId>
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/GitUtil.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/GitUtil.java
index 9faf32a..045207c 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/GitUtil.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/GitUtil.java
@@ -107,11 +107,17 @@
 
   public static String createCommit(Git git, PersonIdent i, String msg)
       throws GitAPIException, IOException {
-    return createCommit(git, i, msg, true);
+    return createCommit(git, i, msg, true, false);
   }
 
-  public static String createCommit(Git git, PersonIdent i, String msg,
-      boolean insertChangeId) throws GitAPIException, IOException {
+  public static void amendCommit(Git git, PersonIdent i, String msg, String changeId)
+      throws GitAPIException, IOException {
+    msg = ChangeIdUtil.insertId(msg, ObjectId.fromString(changeId.substring(1)));
+    createCommit(git, i, msg, false, true);
+  }
+
+  private static String createCommit(Git git, PersonIdent i, String msg,
+      boolean insertChangeId, boolean amend) throws GitAPIException, IOException {
     ObjectId changeId = null;
     if (insertChangeId) {
       changeId = computeChangeId(git, i, msg);
@@ -119,6 +125,7 @@
     }
 
     final CommitCommand commitCmd = git.commit();
+    commitCmd.setAmend(amend);
     commitCmd.setAuthor(i);
     commitCmd.setCommitter(i);
     commitCmd.setMessage(msg);
@@ -139,9 +146,13 @@
     }
   }
 
-  public static PushResult pushHead(Git git, String ref) throws GitAPIException {
+  public static PushResult pushHead(Git git, String ref, boolean pushTags)
+      throws GitAPIException {
     PushCommand pushCmd = git.push();
     pushCmd.setRefSpecs(new RefSpec("HEAD:" + ref));
+    if (pushTags) {
+      pushCmd.setPushTags();
+    }
     Iterable<PushResult> r = pushCmd.call();
     return Iterables.getOnlyElement(r);
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/PushForReviewIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/PushForReviewIT.java
index f905350..9799cc1 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/PushForReviewIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/PushForReviewIT.java
@@ -14,27 +14,15 @@
 
 package com.google.gerrit.acceptance.git;
 
-import static com.google.gerrit.acceptance.git.GitUtil.add;
 import static com.google.gerrit.acceptance.git.GitUtil.cloneProject;
-import static com.google.gerrit.acceptance.git.GitUtil.createCommit;
 import static com.google.gerrit.acceptance.git.GitUtil.createProject;
 import static com.google.gerrit.acceptance.git.GitUtil.initSsh;
-import static com.google.gerrit.acceptance.git.GitUtil.pushHead;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
 
-import com.google.common.base.Function;
-import com.google.common.base.Strings;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Sets;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.AccountCreator;
 import com.google.gerrit.acceptance.SshSession;
 import com.google.gerrit.acceptance.TestAccount;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gwtorm.server.OrmException;
@@ -45,37 +33,17 @@
 
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.errors.GitAPIException;
-import org.eclipse.jgit.transport.PushResult;
-import org.eclipse.jgit.transport.RemoteRefUpdate;
-import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
 
 import java.io.IOException;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Set;
 
-@RunWith(Parameterized.class)
 public class PushForReviewIT extends AbstractDaemonTest {
-
   private enum Protocol {
     SSH, HTTP
   }
 
-  @Parameters(name="{0}")
-  public static List<Object[]> getParam() {
-    List<Object[]> params = Lists.newArrayList();
-    for(Protocol p : Protocol.values()) {
-      params.add(new Object[] {p});
-    }
-    return params;
-  }
-
   @Inject
   private AccountCreator accounts;
 
@@ -86,11 +54,7 @@
   private Project.NameKey project;
   private Git git;
   private ReviewDb db;
-  private Protocol protocol;
-
-  public PushForReviewIT(Protocol p) {
-    this.protocol = p;
-  }
+  private String sshUrl;
 
   @Before
   public void setUp() throws Exception {
@@ -102,21 +66,25 @@
     initSsh(admin);
     SshSession sshSession = new SshSession(admin);
     createProject(sshSession, project.get());
+    sshUrl = sshSession.getUrl();
+    sshSession.close();
+
+    db = reviewDbProvider.open();
+  }
+
+  private void selectProtocol(Protocol p) throws GitAPIException, IOException {
     String url;
-    switch (protocol) {
+    switch (p) {
       case SSH:
-        url = sshSession.getUrl();
+        url = sshUrl;
         break;
       case HTTP:
         url = admin.getHttpUrl();
         break;
       default:
-        throw new IllegalStateException("unexpected protocol: " + protocol);
+        throw new IllegalArgumentException("unexpected protocol: " + p);
     }
     git = cloneProject(url + "/" + project.get());
-    sshSession.close();
-
-    db = reviewDbProvider.open();
   }
 
   @After
@@ -125,185 +93,176 @@
   }
 
   @Test
-  public void testPushForMaster() throws GitAPIException, OrmException,
+  public void testPushForMaster_HTTP() throws GitAPIException, OrmException,
       IOException {
-    PushOneCommit push = new PushOneCommit();
-    String ref = "refs/for/master";
-    PushResult r = push.to(ref);
-    assertOkStatus(r, ref);
-    assertChange(push.changeId, Change.Status.NEW, PushOneCommit.SUBJECT, null);
+    testPushForMaster(Protocol.HTTP);
   }
 
   @Test
-  public void testPushForMasterWithTopic() throws GitAPIException,
+  public void testPushForMaster_SSH() throws GitAPIException, OrmException,
+      IOException {
+    testPushForMaster(Protocol.SSH);
+  }
+
+  private void testPushForMaster(Protocol p) throws GitAPIException,
       OrmException, IOException {
+    selectProtocol(p);
+    PushOneCommit.Result r = pushTo("refs/for/master");
+    r.assertOkStatus();
+    r.assertChange(Change.Status.NEW, null);
+  }
+
+  @Test
+  public void testPushForMasterWithTopic_HTTP()
+      throws GitAPIException, OrmException, IOException {
+    testPushForMasterWithTopic(Protocol.HTTP);
+  }
+
+  @Test
+  public void testPushForMasterWithTopic_SSH()
+      throws GitAPIException, OrmException, IOException {
+    testPushForMasterWithTopic(Protocol.SSH);
+  }
+
+  private void testPushForMasterWithTopic(Protocol p) throws GitAPIException,
+      OrmException, IOException {
+    selectProtocol(p);
     // specify topic in ref
-    PushOneCommit push = new PushOneCommit();
     String topic = "my/topic";
-    String ref = "refs/for/master/" + topic;
-    PushResult r = push.to(ref);
-    assertOkStatus(r, ref);
-    assertChange(push.changeId, Change.Status.NEW, PushOneCommit.SUBJECT, topic);
+    PushOneCommit.Result r = pushTo("refs/for/master/" + topic);
+    r.assertOkStatus();
+    r.assertChange(Change.Status.NEW, topic);
 
     // specify topic as option
-    push = new PushOneCommit();
-    ref = "refs/for/master%topic=" + topic;
-    r = push.to(ref);
-    assertOkStatus(r, ref);
-    assertChange(push.changeId, Change.Status.NEW, PushOneCommit.SUBJECT, topic);
+    r = pushTo("refs/for/master%topic=" + topic);
+    r.assertOkStatus();
+    r.assertChange(Change.Status.NEW, topic);
   }
 
   @Test
-  public void testPushForMasterWithCc() throws GitAPIException, OrmException,
-      IOException, JSchException {
+  public void testPushForMasterWithCc_HTTP() throws GitAPIException,
+      OrmException, IOException, JSchException {
+    testPushForMasterWithCc(Protocol.HTTP);
+  }
+
+  @Test
+  public void testPushForMasterWithCc_SSH() throws GitAPIException,
+      OrmException, IOException, JSchException {
+    testPushForMasterWithCc(Protocol.SSH);
+  }
+
+  private void testPushForMasterWithCc(Protocol p) throws GitAPIException,
+      OrmException, IOException, JSchException {
+    selectProtocol(p);
     // cc one user
     TestAccount user = accounts.create("user", "user@example.com", "User");
-    PushOneCommit push = new PushOneCommit();
     String topic = "my/topic";
-    String ref = "refs/for/master/" + topic + "%cc=" + user.email;
-    PushResult r = push.to(ref);
-    assertOkStatus(r, ref);
-    assertChange(push.changeId, Change.Status.NEW, PushOneCommit.SUBJECT, topic);
+    PushOneCommit.Result r = pushTo("refs/for/master/" + topic + "%cc=" + user.email);
+    r.assertOkStatus();
+    r.assertChange(Change.Status.NEW, topic);
 
     // cc several users
     TestAccount user2 =
         accounts.create("another-user", "another.user@example.com", "Another User");
-    push = new PushOneCommit();
-    ref = "refs/for/master/" + topic + "%cc=" + admin.email + ",cc=" + user.email
-        + ",cc=" + user2.email;
-    r = push.to(ref);
-    assertOkStatus(r, ref);
-    assertChange(push.changeId, Change.Status.NEW, PushOneCommit.SUBJECT, topic);
+    r = pushTo("refs/for/master/" + topic + "%cc=" + admin.email + ",cc="
+        + user.email + ",cc=" + user2.email);
+    r.assertOkStatus();
+    r.assertChange(Change.Status.NEW, topic);
 
     // cc non-existing user
     String nonExistingEmail = "non.existing@example.com";
-    push = new PushOneCommit();
-    ref = "refs/for/master/" + topic + "%cc=" + admin.email + ",cc="
-        + nonExistingEmail + ",cc=" + user.email;
-    r = push.to(ref);
-    assertErrorStatus(r, "user \"" + nonExistingEmail + "\" not found", ref);
+    r = pushTo("refs/for/master/" + topic + "%cc=" + admin.email + ",cc="
+        + nonExistingEmail + ",cc=" + user.email);
+    r.assertErrorStatus("user \"" + nonExistingEmail + "\" not found");
   }
 
   @Test
-  public void testPushForMasterWithReviewer() throws GitAPIException,
+  public void testPushForMasterWithReviewer_HTTP() throws GitAPIException,
       OrmException, IOException, JSchException {
+    testPushForMasterWithReviewer(Protocol.HTTP);
+  }
+
+  @Test
+  public void testPushForMasterWithReviewer_SSH() throws GitAPIException,
+      OrmException, IOException, JSchException {
+    testPushForMasterWithReviewer(Protocol.SSH);
+  }
+
+  private void testPushForMasterWithReviewer(Protocol p)
+      throws GitAPIException, OrmException, IOException, JSchException {
+    selectProtocol(p);
     // add one reviewer
     TestAccount user = accounts.create("user", "user@example.com", "User");
-    PushOneCommit push = new PushOneCommit();
     String topic = "my/topic";
-    String ref = "refs/for/master/" + topic + "%r=" + user.email;
-    PushResult r = push.to(ref);
-    assertOkStatus(r, ref);
-    assertChange(push.changeId, Change.Status.NEW, PushOneCommit.SUBJECT,
-        topic, user);
+    PushOneCommit.Result r = pushTo("refs/for/master/" + topic + "%r=" + user.email);
+    r.assertOkStatus();
+    r.assertChange(Change.Status.NEW, topic, user);
 
     // add several reviewers
     TestAccount user2 =
         accounts.create("another-user", "another.user@example.com", "Another User");
-    push = new PushOneCommit();
-    ref = "refs/for/master/" + topic + "%r=" + admin.email + ",r=" + user.email
-        + ",r=" + user2.email;
-    r = push.to(ref);
-    assertOkStatus(r, ref);
+    r = pushTo("refs/for/master/" + topic + "%r=" + admin.email + ",r=" + user.email
+        + ",r=" + user2.email);
+    r.assertOkStatus();
     // admin is the owner of the change and should not appear as reviewer
-    assertChange(push.changeId, Change.Status.NEW, PushOneCommit.SUBJECT,
-        topic, user, user2);
+    r.assertChange(Change.Status.NEW, topic, user, user2);
 
     // add non-existing user as reviewer
     String nonExistingEmail = "non.existing@example.com";
-    push = new PushOneCommit();
-    ref = "refs/for/master/" + topic + "%r=" + admin.email + ",r="
-        + nonExistingEmail + ",r=" + user.email;
-    r = push.to(ref);
-    assertErrorStatus(r, "user \"" + nonExistingEmail + "\" not found", ref);
+    r = pushTo("refs/for/master/" + topic + "%r=" + admin.email + ",r="
+        + nonExistingEmail + ",r=" + user.email);
+    r.assertErrorStatus("user \"" + nonExistingEmail + "\" not found");
   }
 
   @Test
-  public void testPushForMasterAsDraft() throws GitAPIException, OrmException,
-      IOException {
+  public void testPushForMasterAsDraft_HTTP() throws GitAPIException,
+      OrmException, IOException {
+    testPushForMasterAsDraft(Protocol.HTTP);
+  }
+
+  @Test
+  public void testPushForMasterAsDraft_SSH() throws GitAPIException,
+      OrmException, IOException {
+    testPushForMasterAsDraft(Protocol.SSH);
+  }
+
+  private void testPushForMasterAsDraft(Protocol p) throws GitAPIException,
+      OrmException, IOException {
+    selectProtocol(p);
     // create draft by pushing to 'refs/drafts/'
-    PushOneCommit push = new PushOneCommit();
-    String ref = "refs/drafts/master";
-    PushResult r = push.to(ref);
-    assertOkStatus(r, ref);
-    assertChange(push.changeId, Change.Status.DRAFT, PushOneCommit.SUBJECT, null);
+    PushOneCommit.Result r = pushTo("refs/drafts/master");
+    r.assertOkStatus();
+    r.assertChange(Change.Status.DRAFT, null);
 
     // create draft by using 'draft' option
-    push = new PushOneCommit();
-    ref = "refs/for/master%draft";
-    r = push.to(ref);
-    assertOkStatus(r, ref);
-    assertChange(push.changeId, Change.Status.DRAFT, PushOneCommit.SUBJECT, null);
+    r = pushTo("refs/for/master%draft");
+    r.assertOkStatus();
+    r.assertChange(Change.Status.DRAFT, null);
   }
 
   @Test
-  public void testPushForNonExistingBranch() throws GitAPIException,
+  public void testPushForNonExistingBranch_HTTP() throws GitAPIException,
       OrmException, IOException {
-    PushOneCommit push = new PushOneCommit();
+    testPushForNonExistingBranch(Protocol.HTTP);
+  }
+
+  @Test
+  public void testPushForNonExistingBranch_SSH() throws GitAPIException,
+      OrmException, IOException {
+    testPushForNonExistingBranch(Protocol.SSH);
+  }
+
+  private void testPushForNonExistingBranch(Protocol p) throws GitAPIException,
+      OrmException, IOException {
+    selectProtocol(p);
     String branchName = "non-existing";
-    String ref = "refs/for/" + branchName;
-    PushResult r = push.to(ref);
-    assertErrorStatus(r, "branch " + branchName + " not found", ref);
+    PushOneCommit.Result r = pushTo("refs/for/" + branchName);
+    r.assertErrorStatus("branch " + branchName + " not found");
   }
 
-  private void assertChange(String changeId, Change.Status expectedStatus,
-      String expectedSubject, String expectedTopic,
-      TestAccount... expectedReviewers) throws OrmException {
-    Change c =
-        Iterables.getOnlyElement(db.changes().byKey(new Change.Key(changeId)).toList());
-    assertEquals(expectedSubject, c.getSubject());
-    assertEquals(expectedStatus, c.getStatus());
-    assertEquals(expectedTopic, Strings.emptyToNull(c.getTopic()));
-    assertReviewers(c, expectedReviewers);
-  }
-
-  private void assertReviewers(Change c, TestAccount... expectedReviewers)
-      throws OrmException {
-    Set<Account.Id> expectedReviewerIds =
-        Sets.newHashSet(Lists.transform(Arrays.asList(expectedReviewers),
-            new Function<TestAccount, Account.Id>() {
-              @Override
-              public Account.Id apply(TestAccount a) {
-                return a.id;
-              }
-            }));
-
-    for (PatchSetApproval psa : db.patchSetApprovals().byPatchSet(
-        c.currentPatchSetId())) {
-      assertTrue("unexpected reviewer " + psa.getAccountId(),
-          expectedReviewerIds.remove(psa.getAccountId()));
-    }
-    assertTrue("missing reviewers: " + expectedReviewerIds,
-        expectedReviewerIds.isEmpty());
-  }
-
-  private static void assertOkStatus(PushResult result, String ref) {
-    assertStatus(Status.OK, null, result, ref);
-  }
-
-  private static void assertErrorStatus(PushResult result,
-      String expectedMessage, String ref) {
-    assertStatus(Status.REJECTED_OTHER_REASON, expectedMessage, result, ref);
-  }
-
-  private static void assertStatus(Status expectedStatus,
-      String expectedMessage, PushResult result, String ref) {
-    RemoteRefUpdate refUpdate = result.getRemoteUpdate(ref);
-    assertEquals(refUpdate.getMessage() + "\n" + result.getMessages(),
-        expectedStatus, refUpdate.getStatus());
-    assertEquals(expectedMessage, refUpdate.getMessage());
-  }
-
-  private class PushOneCommit {
-    final static String FILE_NAME = "a.txt";
-    final static String FILE_CONTENT = "some content";
-    final static String SUBJECT = "test commit";
-    String changeId;
-
-    public PushResult to(String ref) throws GitAPIException, IOException {
-      add(git, FILE_NAME, FILE_CONTENT);
-      changeId = createCommit(git, admin.getIdent(), SUBJECT);
-      return pushHead(git, ref);
-    }
+  private PushOneCommit.Result pushTo(String ref) throws GitAPIException,
+      IOException {
+    PushOneCommit push = new PushOneCommit(db, admin.getIdent());
+    return push.to(git, ref);
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/PushOneCommit.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/PushOneCommit.java
new file mode 100644
index 0000000..4c32f2f
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/PushOneCommit.java
@@ -0,0 +1,187 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.git;
+
+import static com.google.gerrit.acceptance.git.GitUtil.add;
+import static com.google.gerrit.acceptance.git.GitUtil.amendCommit;
+import static com.google.gerrit.acceptance.git.GitUtil.createCommit;
+import static com.google.gerrit.acceptance.git.GitUtil.pushHead;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.base.Function;
+import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.server.OrmException;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.transport.PushResult;
+import org.eclipse.jgit.transport.RemoteRefUpdate;
+import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Set;
+
+public class PushOneCommit {
+  public final static String SUBJECT = "test commit";
+
+  private final static String FILE_NAME = "a.txt";
+  private final static String FILE_CONTENT = "some content";
+
+  private final ReviewDb db;
+  private final PersonIdent i;
+
+  private final String subject;
+  private final String fileName;
+  private final String content;
+  private String changeId;
+  private String tagName;
+
+  public PushOneCommit(ReviewDb db, PersonIdent i) {
+    this(db, i, SUBJECT, FILE_NAME, FILE_CONTENT);
+  }
+
+  public PushOneCommit(ReviewDb db, PersonIdent i, String subject,
+      String fileName, String content) {
+    this(db, i, subject, fileName, content, null);
+  }
+
+  public PushOneCommit(ReviewDb db, PersonIdent i, String subject,
+      String fileName, String content, String changeId) {
+    this.db = db;
+    this.i = i;
+    this.subject = subject;
+    this.fileName = fileName;
+    this.content = content;
+    this.changeId = changeId;
+  }
+
+  public Result to(Git git, String ref)
+      throws GitAPIException, IOException {
+    add(git, fileName, content);
+    if (changeId != null) {
+      amendCommit(git, i, subject, changeId);
+    } else {
+      changeId = createCommit(git, i, subject);
+    }
+    if (tagName != null) {
+      git.tag().setName(tagName).setAnnotated(false).call();
+    }
+    return new Result(db, ref, pushHead(git, ref, tagName != null), changeId, subject);
+  }
+
+  public void setTag(final String tagName) {
+    this.tagName = tagName;
+  }
+
+  public static class Result {
+    private final ReviewDb db;
+    private final String ref;
+    private final PushResult result;
+    private final String changeId;
+    private final String subject;
+
+    private Result(ReviewDb db, String ref, PushResult result, String changeId,
+        String subject) {
+      this.db = db;
+      this.ref = ref;
+      this.result = result;
+      this.changeId = changeId;
+      this.subject = subject;
+    }
+
+    public PatchSet.Id getPatchSetId() throws OrmException {
+      return Iterables.getOnlyElement(
+          db.changes().byKey(new Change.Key(changeId))).currentPatchSetId();
+    }
+
+    public String getChangeId() {
+      return changeId;
+    }
+
+    public void assertChange(Change.Status expectedStatus,
+        String expectedTopic, TestAccount... expectedReviewers)
+        throws OrmException {
+      Change c =
+          Iterables.getOnlyElement(db.changes().byKey(new Change.Key(changeId)).toList());
+      assertEquals(subject, c.getSubject());
+      assertEquals(expectedStatus, c.getStatus());
+      assertEquals(expectedTopic, Strings.emptyToNull(c.getTopic()));
+      assertReviewers(c, expectedReviewers);
+    }
+
+    private void assertReviewers(Change c, TestAccount... expectedReviewers)
+        throws OrmException {
+      Set<Account.Id> expectedReviewerIds =
+          Sets.newHashSet(Lists.transform(Arrays.asList(expectedReviewers),
+              new Function<TestAccount, Account.Id>() {
+                @Override
+                public Account.Id apply(TestAccount a) {
+                  return a.id;
+                }
+              }));
+
+      for (PatchSetApproval psa : db.patchSetApprovals().byPatchSet(
+          c.currentPatchSetId())) {
+        assertTrue("unexpected reviewer " + psa.getAccountId(),
+            expectedReviewerIds.remove(psa.getAccountId()));
+      }
+      assertTrue("missing reviewers: " + expectedReviewerIds,
+          expectedReviewerIds.isEmpty());
+    }
+
+    public void assertOkStatus() {
+      assertStatus(Status.OK, null);
+    }
+
+    public void assertErrorStatus(String expectedMessage) {
+      assertStatus(Status.REJECTED_OTHER_REASON, expectedMessage);
+    }
+
+    private void assertStatus(Status expectedStatus, String expectedMessage) {
+      RemoteRefUpdate refUpdate = result.getRemoteUpdate(ref);
+      assertEquals(message(refUpdate),
+          expectedStatus, refUpdate.getStatus());
+      assertEquals(expectedMessage, refUpdate.getMessage());
+    }
+
+    public void assertMessage(String expectedMessage) {
+      RemoteRefUpdate refUpdate = result.getRemoteUpdate(ref);
+      assertTrue(message(refUpdate), message(refUpdate).toLowerCase().contains(
+          expectedMessage.toLowerCase()));
+    }
+
+    private String message(RemoteRefUpdate refUpdate) {
+      StringBuilder b = new StringBuilder();
+      if (refUpdate.getMessage() != null) {
+        b.append(refUpdate.getMessage());
+        b.append("\n");
+      }
+      b.append(result.getMessages());
+      return b.toString();
+    }
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
new file mode 100644
index 0000000..ab97d19
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
@@ -0,0 +1,328 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.git;
+
+import static com.google.gerrit.acceptance.git.GitUtil.cloneProject;
+import static com.google.gerrit.acceptance.git.GitUtil.createProject;
+import static com.google.gerrit.acceptance.git.GitUtil.initSsh;
+import static org.junit.Assert.assertEquals;
+
+import com.google.common.collect.Iterables;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountCreator;
+import com.google.gerrit.acceptance.SshSession;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.git.CommitMergeStatus;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.RefSpec;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.List;
+
+public class SubmitOnPushIT extends AbstractDaemonTest {
+
+  @Inject
+  private AccountCreator accounts;
+
+  @Inject
+  private SchemaFactory<ReviewDb> reviewDbProvider;
+
+  @Inject
+  private GitRepositoryManager repoManager;
+
+  @Inject
+  private MetaDataUpdate.Server metaDataUpdateFactory;
+
+  @Inject
+  private ProjectCache projectCache;
+
+  @Inject
+  private GroupCache groupCache;
+
+  @Inject
+  private @GerritPersonIdent PersonIdent serverIdent;
+
+  private TestAccount admin;
+  private Project.NameKey project;
+  private Git git;
+  private ReviewDb db;
+
+  @Before
+  public void setUp() throws Exception {
+    admin =
+        accounts.create("admin", "admin@example.com", "Administrator",
+            "Administrators");
+
+    project = new Project.NameKey("p");
+    initSsh(admin);
+    SshSession sshSession = new SshSession(admin);
+    createProject(sshSession, project.get());
+    git = cloneProject(sshSession.getUrl() + "/" + project.get());
+    sshSession.close();
+
+    db = reviewDbProvider.open();
+  }
+
+  @After
+  public void cleanup() {
+    db.close();
+  }
+
+  @Test
+  public void submitOnPush() throws GitAPIException, OrmException,
+      IOException, ConfigInvalidException {
+    grant(Permission.SUBMIT, project, "refs/for/refs/heads/master");
+    PushOneCommit.Result r = pushTo("refs/for/master%submit");
+    r.assertOkStatus();
+    r.assertChange(Change.Status.MERGED, null, admin);
+    assertSubmitApproval(r.getPatchSetId());
+    assertCommit(project, "refs/heads/master");
+  }
+
+  @Test
+  public void submitOnPushWithTag() throws GitAPIException, OrmException,
+      IOException, ConfigInvalidException {
+    grant(Permission.SUBMIT, project, "refs/for/refs/heads/master");
+    grant(Permission.CREATE, project, "refs/tags/*");
+    final String tag = "v1.0";
+    PushOneCommit push = new PushOneCommit(db, admin.getIdent());
+    push.setTag(tag);
+    PushOneCommit.Result r = push.to(git, "refs/for/master%submit");
+    r.assertOkStatus();
+    r.assertChange(Change.Status.MERGED, null, admin);
+    assertSubmitApproval(r.getPatchSetId());
+    assertCommit(project, "refs/heads/master");
+    assertTag(project, "refs/heads/master", tag);
+  }
+
+  @Test
+  public void submitOnPushToRefsMetaConfig() throws GitAPIException,
+      OrmException, IOException, ConfigInvalidException {
+    grant(Permission.SUBMIT, project, "refs/for/refs/meta/config");
+
+    git.fetch().setRefSpecs(new RefSpec("refs/meta/config:refs/meta/config")).call();
+    ObjectId objectId = git.getRepository().getRef("refs/meta/config").getObjectId();
+    git.checkout().setName(objectId.getName()).call();
+
+    PushOneCommit.Result r = pushTo("refs/for/refs/meta/config%submit");
+    r.assertOkStatus();
+    r.assertChange(Change.Status.MERGED, null, admin);
+    assertSubmitApproval(r.getPatchSetId());
+    assertCommit(project, "refs/meta/config");
+  }
+
+  @Test
+  public void submitOnPushMergeConflict() throws GitAPIException, OrmException,
+      IOException, ConfigInvalidException {
+    String master = "refs/heads/master";
+    ObjectId objectId = git.getRepository().getRef(master).getObjectId();
+    push(master, "one change", "a.txt", "some content");
+    git.checkout().setName(objectId.getName()).call();
+
+    grant(Permission.SUBMIT, project, "refs/for/refs/heads/master");
+    PushOneCommit.Result r =
+        push("refs/for/master%submit", "other change", "a.txt", "other content");
+    r.assertOkStatus();
+    r.assertChange(Change.Status.NEW, null, admin);
+    r.assertMessage(CommitMergeStatus.PATH_CONFLICT.getMessage());
+  }
+
+  @Test
+  public void submitOnPushSuccessfulMerge() throws GitAPIException, OrmException,
+      IOException, ConfigInvalidException {
+    String master = "refs/heads/master";
+    ObjectId objectId = git.getRepository().getRef(master).getObjectId();
+    push(master, "one change", "a.txt", "some content");
+    git.checkout().setName(objectId.getName()).call();
+
+    grant(Permission.SUBMIT, project, "refs/for/refs/heads/master");
+    PushOneCommit.Result r =
+        push("refs/for/master%submit", "other change", "b.txt", "other content");
+    r.assertOkStatus();
+    r.assertChange(Change.Status.MERGED, null, admin);
+    assertMergeCommit(master, "other change");
+  }
+
+  @Test
+  public void submitOnPushNewPatchSet() throws GitAPIException,
+      OrmException, IOException, ConfigInvalidException {
+    PushOneCommit.Result r =
+        push("refs/for/master", PushOneCommit.SUBJECT, "a.txt", "some content");
+
+    grant(Permission.SUBMIT, project, "refs/for/refs/heads/master");
+    r = push("refs/for/master%submit", PushOneCommit.SUBJECT, "a.txt",
+        "other content", r.getChangeId());
+    r.assertOkStatus();
+    r.assertChange(Change.Status.MERGED, null, admin);
+    Change c = Iterables.getOnlyElement(db.changes().byKey(
+        new Change.Key(r.getChangeId())).toList());
+    assertEquals(2, db.patchSets().byChange(c.getId()).toList().size());
+    assertSubmitApproval(r.getPatchSetId());
+    assertCommit(project, "refs/heads/master");
+  }
+
+  @Test
+  public void submitOnPushNotAllowed_Error() throws GitAPIException,
+      OrmException, IOException {
+    PushOneCommit.Result r = pushTo("refs/for/master%submit");
+    r.assertErrorStatus("submit not allowed");
+  }
+
+  @Test
+  public void submitOnPushNewPatchSetNotAllowed_Error() throws GitAPIException,
+      OrmException, IOException, ConfigInvalidException {
+    PushOneCommit.Result r =
+        push("refs/for/master", PushOneCommit.SUBJECT, "a.txt", "some content");
+
+    r = push("refs/for/master%submit", PushOneCommit.SUBJECT, "a.txt",
+        "other content", r.getChangeId());
+    r.assertErrorStatus("submit not allowed");
+  }
+
+  @Test
+  public void submitOnPushingDraft_Error() throws GitAPIException,
+      OrmException, IOException {
+    PushOneCommit.Result r = pushTo("refs/for/master%draft,submit");
+    r.assertErrorStatus("cannot submit draft");
+  }
+
+  @Test
+  public void submitOnPushToNonExistingBranch_Error() throws GitAPIException,
+      OrmException, IOException {
+    String branchName = "non-existing";
+    PushOneCommit.Result r = pushTo("refs/for/" + branchName + "%submit");
+    r.assertErrorStatus("branch " + branchName + " not found");
+  }
+
+  private void grant(String permission, Project.NameKey project, String ref)
+      throws RepositoryNotFoundException, IOException, ConfigInvalidException {
+    MetaDataUpdate md = metaDataUpdateFactory.create(project);
+    md.setMessage(String.format("Grant %s on %s", permission, ref));
+    ProjectConfig config = ProjectConfig.read(md);
+    AccessSection s = config.getAccessSection(ref, true);
+    Permission p = s.getPermission(permission, true);
+    AccountGroup adminGroup = groupCache.get(new AccountGroup.NameKey("Administrators"));
+    p.add(new PermissionRule(config.resolve(adminGroup)));
+    config.commit(md);
+    projectCache.evict(config.getProject());
+  }
+
+  private void assertSubmitApproval(PatchSet.Id patchSetId) throws OrmException {
+    List<PatchSetApproval> approvals = db.patchSetApprovals().byPatchSet(patchSetId).toList();
+    assertEquals(1, approvals.size());
+    PatchSetApproval a = approvals.get(0);
+    assertEquals(PatchSetApproval.LabelId.SUBMIT.get(), a.getLabel());
+    assertEquals(1, a.getValue());
+    assertEquals(admin.id, a.getAccountId());
+  }
+
+  private void assertCommit(Project.NameKey project, String branch) throws IOException {
+    Repository r = repoManager.openRepository(project);
+    try {
+      RevWalk rw = new RevWalk(r);
+      try {
+        RevCommit c = rw.parseCommit(r.getRef(branch).getObjectId());
+        assertEquals(PushOneCommit.SUBJECT, c.getShortMessage());
+        assertEquals(admin.email, c.getAuthorIdent().getEmailAddress());
+        assertEquals(admin.email, c.getCommitterIdent().getEmailAddress());
+      } finally {
+        rw.release();
+      }
+    } finally {
+      r.close();
+    }
+  }
+
+  private void assertMergeCommit(String branch, String subject) throws IOException {
+    Repository r = repoManager.openRepository(project);
+    try {
+      RevWalk rw = new RevWalk(r);
+      try {
+        RevCommit c = rw.parseCommit(r.getRef(branch).getObjectId());
+        assertEquals(2, c.getParentCount());
+        assertEquals("Merge \"" + subject + "\"", c.getShortMessage());
+        assertEquals(admin.email, c.getAuthorIdent().getEmailAddress());
+        assertEquals(serverIdent.getEmailAddress(), c.getCommitterIdent().getEmailAddress());
+      } finally {
+        rw.release();
+      }
+    } finally {
+      r.close();
+    }
+  }
+
+  private void assertTag(Project.NameKey project, String branch, String tagName)
+      throws IOException {
+    Repository r = repoManager.openRepository(project);
+    try {
+      ObjectId headCommit = r.getRef(branch).getObjectId();
+      ObjectId taggedCommit = r.getRef(tagName).getObjectId();
+      assertEquals(headCommit, taggedCommit);
+    } finally {
+      r.close();
+    }
+  }
+
+  private PushOneCommit.Result pushTo(String ref) throws GitAPIException,
+      IOException {
+    PushOneCommit push = new PushOneCommit(db, admin.getIdent());
+    return push.to(git, ref);
+  }
+
+  private PushOneCommit.Result push(String ref, String subject,
+      String fileName, String content) throws GitAPIException, IOException {
+    PushOneCommit push =
+        new PushOneCommit(db, admin.getIdent(), subject, fileName, content);
+    return push.to(git, ref);
+  }
+
+  private PushOneCommit.Result push(String ref, String subject,
+      String fileName, String content, String changeId) throws GitAPIException,
+      IOException {
+    PushOneCommit push = new PushOneCommit(db, admin.getIdent(), subject,
+        fileName, content, changeId);
+    return push.to(git, ref);
+  }
+}
diff --git a/gerrit-antlr/pom.xml b/gerrit-antlr/pom.xml
index d88ed42..c17e01e 100644
--- a/gerrit-antlr/pom.xml
+++ b/gerrit-antlr/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.6</version>
+    <version>2.7</version>
   </parent>
 
   <artifactId>gerrit-antlr</artifactId>
diff --git a/gerrit-cache-h2/pom.xml b/gerrit-cache-h2/pom.xml
index a848aec..80ab03b 100644
--- a/gerrit-cache-h2/pom.xml
+++ b/gerrit-cache-h2/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.6</version>
+    <version>2.7</version>
   </parent>
 
   <artifactId>gerrit-cache-h2</artifactId>
diff --git a/gerrit-common/pom.xml b/gerrit-common/pom.xml
index 09e1d4e..efa7285 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.6</version>
+    <version>2.7</version>
   </parent>
 
   <artifactId>gerrit-common</artifactId>
@@ -40,8 +40,9 @@
     </dependency>
 
     <dependency>
-      <groupId>gwtexpui</groupId>
-      <artifactId>gwtexpui</artifactId>
+      <groupId>com.google.gerrit</groupId>
+      <artifactId>gerrit-gwtexpui</artifactId>
+      <version>${project.version}</version>
     </dependency>
 
     <dependency>
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/changes/ListChangesOption.java b/gerrit-common/src/main/java/com/google/gerrit/common/changes/ListChangesOption.java
index 3d08d06..f5ef9c5 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/changes/ListChangesOption.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/changes/ListChangesOption.java
@@ -34,7 +34,10 @@
   ALL_FILES(6),
 
   /** If accounts are included, include detailed account info. */
-  DETAILED_ACCOUNTS(7);
+  DETAILED_ACCOUNTS(7),
+
+  /** Include messages associated with the change. */
+  MESSAGES(9);
 
   private final int value;
 
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java
index 7ac21db..f6d5ea33 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.reviewdb.client.Project;
 
 import java.util.List;
+import java.util.Set;
 
 /** Detail necessary to display a change. */
 public class ChangeDetail {
@@ -37,6 +38,7 @@
   protected List<ChangeInfo> dependsOn;
   protected List<ChangeInfo> neededBy;
   protected List<PatchSet> patchSets;
+  protected Set<PatchSet.Id> patchSetsWithDraftComments;
   protected List<SubmitRecord> submitRecords;
   protected Project.SubmitType submitType;
   protected SubmitTypeRecord submitTypeRecord;
@@ -187,6 +189,14 @@
     patchSets = s;
   }
 
+  public void setPatchSetsWithDraftComments(Set<PatchSet.Id> pwdc) {
+    this.patchSetsWithDraftComments = pwdc;
+  }
+
+  public boolean hasDraftComments(PatchSet.Id id) {
+    return patchSetsWithDraftComments.contains(id);
+  }
+
   public void setSubmitRecords(List<SubmitRecord> all) {
     submitRecords = all;
   }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GarbageCollectionResult.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GarbageCollectionResult.java
index 93f283b..e4d7b80 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GarbageCollectionResult.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GarbageCollectionResult.java
@@ -14,16 +14,16 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.common.collect.Lists;
 import com.google.gerrit.reviewdb.client.Project;
 
+import java.util.ArrayList;
 import java.util.List;
 
 public class GarbageCollectionResult {
   protected List<Error> errors;
 
   public GarbageCollectionResult() {
-    errors = Lists.newArrayList();
+    errors = new ArrayList<Error>();
   }
 
   public void addError(Error e) {
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GerritConfig.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GerritConfig.java
index 16b99dc..7660a80 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GerritConfig.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GerritConfig.java
@@ -20,9 +20,7 @@
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwtexpui.safehtml.client.RegexFindReplace;
 
-import java.util.List;
 import java.util.Set;
 
 public class GerritConfig implements Cloneable {
@@ -44,7 +42,6 @@
   protected String editFullNameUrl;
   protected Project.NameKey wildProject;
   protected Set<Account.FieldName> editableAccountFields;
-  protected List<RegexFindReplace> commentLinks;
   protected boolean documentationAvailable;
   protected boolean testChangeMerge;
   protected String anonymousCowardName;
@@ -188,14 +185,6 @@
     editableAccountFields = af;
   }
 
-  public List<RegexFindReplace> getCommentLinks() {
-    return commentLinks;
-  }
-
-  public void setCommentLinks(final List<RegexFindReplace> cl) {
-    commentLinks = cl;
-  }
-
   public boolean isDocumentationAvailable() {
     return documentationAvailable;
   }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GitWebType.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GitWebType.java
index 3580774..8528c0f 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GitWebType.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GitWebType.java
@@ -75,6 +75,9 @@
     * project names */
   private char pathSeparator = '/';
 
+  /** Whether to include links to draft patch sets */
+  private boolean linkDrafts;
+
   /** Private default constructor for gson. */
   protected GitWebType() {
   }
@@ -125,6 +128,15 @@
   }
 
   /**
+   * Get whether to link to draft patch sets
+   *
+   * @return True to link
+   */
+  public boolean getLinkDrafts() {
+    return linkDrafts;
+  }
+
+  /**
    * Set the pattern for branch view.
    *
    * @param pattern The pattern for branch view
@@ -201,4 +213,8 @@
   public void setPathSeparator(char separator) {
     this.pathSeparator = separator;
   }
+
+  public void setLinkDrafts(boolean linkDrafts) {
+    this.linkDrafts = linkDrafts;
+  }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
index 7db691d..8c08feb 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
@@ -73,6 +73,9 @@
   /** Forcefully restart replication to any configured destination. */
   public static final String START_REPLICATION = "startReplication";
 
+  /** Can perform streaming of Gerrit events. */
+  public static final String STREAM_EVENTS = "streamEvents";
+
   /** Can view the server's current cache states. */
   public static final String VIEW_CACHES = "viewCaches";
 
@@ -99,6 +102,7 @@
     NAMES_ALL.add(QUERY_LIMIT);
     NAMES_ALL.add(RUN_GC);
     NAMES_ALL.add(START_REPLICATION);
+    NAMES_ALL.add(STREAM_EVENTS);
     NAMES_ALL.add(VIEW_CACHES);
     NAMES_ALL.add(VIEW_CONNECTIONS);
     NAMES_ALL.add(VIEW_QUEUE);
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java
index 37ae19e..db8bde9 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java
@@ -96,6 +96,7 @@
   protected String abbreviation;
   protected String functionName;
   protected boolean copyMinScore;
+  protected boolean copyMaxScore;
 
   protected List<LabelValue> values;
   protected short maxNegative;
@@ -187,6 +188,14 @@
     this.copyMinScore = copyMinScore;
   }
 
+  public boolean isCopyMaxScore() {
+    return copyMaxScore;
+  }
+
+  public void setCopyMaxScore(boolean copyMaxScore) {
+    this.copyMaxScore = copyMaxScore;
+  }
+
   public boolean isMaxNegative(PatchSetApproval ca) {
     return maxNegative == ca.getValue();
   }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetDetail.java
index a2debf2..39f5cb0 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetDetail.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetDetail.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetInfo;
+import com.google.gerrit.reviewdb.client.Project;
 
 import java.util.List;
 
@@ -24,6 +25,7 @@
   protected PatchSet patchSet;
   protected PatchSetInfo info;
   protected List<Patch> patches;
+  protected Project.NameKey project;
 
   public PatchSetDetail() {
   }
@@ -51,4 +53,12 @@
   public void setPatches(final List<Patch> p) {
     patches = p;
   }
+
+  public Project.NameKey getProject() {
+    return project;
+  }
+
+  public void setProject(final Project.NameKey p) {
+    project = p;
+  }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/SingleListChangeInfo.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SingleListChangeInfo.java
deleted file mode 100644
index e55375b..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/SingleListChangeInfo.java
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.common.data;
-
-import java.util.List;
-
-/** Summary information needed for screens showing a single list of changes}. */
-public class SingleListChangeInfo {
-  protected AccountInfoCache accounts;
-  protected List<ChangeInfo> changes;
-  protected boolean atEnd;
-
-  public SingleListChangeInfo() {
-  }
-
-  public AccountInfoCache getAccounts() {
-    return accounts;
-  }
-
-  public void setAccounts(final AccountInfoCache ac) {
-    accounts = ac;
-  }
-
-  public List<ChangeInfo> getChanges() {
-    return changes;
-  }
-
-  public boolean isAtEnd() {
-    return atEnd;
-  }
-
-  public void setChanges(List<ChangeInfo> c) {
-    setChanges(c, true);
-  }
-
-  public void setChanges(List<ChangeInfo> c, boolean end) {
-    changes = c;
-    atEnd = end;
-  }
-}
diff --git a/gerrit-extension-api/pom.xml b/gerrit-extension-api/pom.xml
index cdab3ab..31fccc0 100644
--- a/gerrit-extension-api/pom.xml
+++ b/gerrit-extension-api/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.6</version>
+    <version>2.7</version>
   </parent>
 
   <artifactId>gerrit-extension-api</artifactId>
diff --git a/gerrit-gwtdebug/pom.xml b/gerrit-gwtdebug/pom.xml
index da16eda..83c10eb 100644
--- a/gerrit-gwtdebug/pom.xml
+++ b/gerrit-gwtdebug/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.6</version>
+    <version>2.7</version>
   </parent>
 
   <artifactId>gerrit-gwtdebug</artifactId>
diff --git a/gerrit-gwtexpui/.gitignore b/gerrit-gwtexpui/.gitignore
new file mode 100644
index 0000000..406c4d5
--- /dev/null
+++ b/gerrit-gwtexpui/.gitignore
@@ -0,0 +1,6 @@
+/target
+/generated_classes
+/.classpath
+/.project
+/.settings/org.maven.ide.eclipse.prefs
+.settings/org.eclipse.m2e.core.prefs
diff --git a/gerrit-gwtexpui/.settings/org.eclipse.core.resources.prefs b/gerrit-gwtexpui/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000..f9fe345
--- /dev/null
+++ b/gerrit-gwtexpui/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,4 @@
+eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
+encoding//src/test/java=UTF-8
+encoding/<project>=UTF-8
diff --git a/gerrit-gwtexpui/.settings/org.eclipse.core.runtime.prefs b/gerrit-gwtexpui/.settings/org.eclipse.core.runtime.prefs
new file mode 100644
index 0000000..8667cfd
--- /dev/null
+++ b/gerrit-gwtexpui/.settings/org.eclipse.core.runtime.prefs
@@ -0,0 +1,3 @@
+#Tue Sep 02 16:59:24 PDT 2008
+eclipse.preferences.version=1
+line.separator=\n
diff --git a/gerrit-gwtexpui/.settings/org.eclipse.jdt.core.prefs b/gerrit-gwtexpui/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..21aa7e7
--- /dev/null
+++ b/gerrit-gwtexpui/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,285 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
+org.eclipse.jdt.core.compiler.source=1.6
+org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=0
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_assignment=16
+org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_compact_if=16
+org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_enum_constants=16
+org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16
+org.eclipse.jdt.core.formatter.alignment_for_method_declaration=0
+org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16
+org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_resources_in_try=80
+org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16
+org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16
+org.eclipse.jdt.core.formatter.blank_lines_after_imports=1
+org.eclipse.jdt.core.formatter.blank_lines_after_package=1
+org.eclipse.jdt.core.formatter.blank_lines_before_field=0
+org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0
+org.eclipse.jdt.core.formatter.blank_lines_before_imports=0
+org.eclipse.jdt.core.formatter.blank_lines_before_member_type=0
+org.eclipse.jdt.core.formatter.blank_lines_before_method=1
+org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1
+org.eclipse.jdt.core.formatter.blank_lines_before_package=0
+org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1
+org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=2
+org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false
+org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false
+org.eclipse.jdt.core.formatter.comment.format_block_comments=true
+org.eclipse.jdt.core.formatter.comment.format_header=true
+org.eclipse.jdt.core.formatter.comment.format_html=true
+org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true
+org.eclipse.jdt.core.formatter.comment.format_line_comments=true
+org.eclipse.jdt.core.formatter.comment.format_source_code=true
+org.eclipse.jdt.core.formatter.comment.indent_parameter_description=false
+org.eclipse.jdt.core.formatter.comment.indent_root_tags=true
+org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=insert
+org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=do not insert
+org.eclipse.jdt.core.formatter.comment.line_length=80
+org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries=true
+org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true
+org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=false
+org.eclipse.jdt.core.formatter.compact_else_if=true
+org.eclipse.jdt.core.formatter.continuation_indentation=2
+org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2
+org.eclipse.jdt.core.formatter.disabling_tag=@formatter\:off
+org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on
+org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false
+org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true
+org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true
+org.eclipse.jdt.core.formatter.indent_empty_lines=false
+org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true
+org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true
+org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true
+org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=true
+org.eclipse.jdt.core.formatter.indentation.size=4
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_label=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert
+org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_binary_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources=insert
+org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert
+org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_binary_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert
+org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert
+org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert
+org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.join_lines_in_comments=true
+org.eclipse.jdt.core.formatter.join_wrapped_lines=true
+org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false
+org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false
+org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=true
+org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false
+org.eclipse.jdt.core.formatter.lineSplit=80
+org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false
+org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false
+org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0
+org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=3
+org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=false
+org.eclipse.jdt.core.formatter.tabulation.char=space
+org.eclipse.jdt.core.formatter.tabulation.size=2
+org.eclipse.jdt.core.formatter.use_on_off_tags=false
+org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false
+org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true
+org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true
+org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true
diff --git a/gerrit-gwtexpui/.settings/org.eclipse.jdt.ui.prefs b/gerrit-gwtexpui/.settings/org.eclipse.jdt.ui.prefs
new file mode 100644
index 0000000..7d663fda
--- /dev/null
+++ b/gerrit-gwtexpui/.settings/org.eclipse.jdt.ui.prefs
@@ -0,0 +1,3 @@
+eclipse.preferences.version=1
+formatter_profile=_Google Format
+formatter_settings_version=12
diff --git a/gerrit-gwtexpui/COPYING b/gerrit-gwtexpui/COPYING
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/gerrit-gwtexpui/COPYING
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/gerrit-gwtexpui/pom.xml b/gerrit-gwtexpui/pom.xml
new file mode 100644
index 0000000..2c5ec62
--- /dev/null
+++ b/gerrit-gwtexpui/pom.xml
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2009 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>com.google.gerrit</groupId>
+    <artifactId>gerrit-parent</artifactId>
+    <version>2.7</version>
+  </parent>
+
+  <artifactId>gerrit-gwtexpui</artifactId>
+
+  <name>Gerrit Code Review - GWT expui</name>
+  <description>Extended UI tools for GWT</description>
+
+  <build>
+    <plugins>
+      <plugin>
+        <artifactId>maven-source-plugin</artifactId>
+        <executions>
+          <execution>
+            <goals>
+              <goal>jar</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+
+    <extensions>
+      <extension>
+        <groupId>com.googlesource.gerrit</groupId>
+        <artifactId>gs-maven-wagon</artifactId>
+        <version>3.3</version>
+      </extension>
+    </extensions>
+  </build>
+
+  <dependencies>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>com.google.gwt</groupId>
+      <artifactId>gwt-user</artifactId>
+      <scope>provided</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>com.google.gwt</groupId>
+      <artifactId>gwt-dev</artifactId>
+      <scope>provided</scope>
+    </dependency>
+  </dependencies>
+</project>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/Clippy.gwt.xml
similarity index 69%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml
rename to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/Clippy.gwt.xml
index d7e835f..0e9b072 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/Clippy.gwt.xml
@@ -1,5 +1,5 @@
 <!--
- Copyright (C) 2011 The Android Open Source Project
+ 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.
@@ -13,8 +13,8 @@
  See the License for the specific language governing permissions and
  limitations under the License.
 -->
-<module rename-to="gerrit_ui">
-  <inherits name='com.google.gerrit.GerritGwtUI'/>
-  <set-property name="user.agent" value="gecko1_8" />
-  <set-property name="locale" value="default" />
+<module>
+  <inherits name='com.google.gwt.resources.Resources'/>
+  <inherits name="com.google.gwtexpui.safehtml.SafeHtml"/>
+  <inherits name="com.google.gwtexpui.user.User"/>
 </module>
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/SubmitLabelAttribute.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyCss.java
similarity index 69%
copy from gerrit-server/src/main/java/com/google/gerrit/server/events/SubmitLabelAttribute.java
copy to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyCss.java
index 99d0350..68495e8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/SubmitLabelAttribute.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyCss.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 The Android Open Source Project
+// 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.
@@ -12,10 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.events;
+package com.google.gwtexpui.clippy.client;
 
-public class SubmitLabelAttribute {
-    public String label;
-    public String status;
-    public AccountAttribute by;
+import com.google.gwt.resources.client.CssResource;
+
+public interface ClippyCss extends CssResource {
+  String label();
+  String control();
 }
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyResources.java
similarity index 61%
copy from gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.java
copy to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyResources.java
index df60305..4c2b8981 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyResources.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 The Android Open Source Project
+// 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.
@@ -12,16 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.prettify.common;
+package com.google.gwtexpui.clippy.client;
 
 import com.google.gwt.core.client.GWT;
-import com.google.gwt.i18n.client.Constants;
+import com.google.gwt.resources.client.ClientBundle;
 
-public interface PrettifyConstants extends Constants {
-  static final PrettifyConstants C = GWT.create(PrettifyConstants.class);
+public interface ClippyResources extends ClientBundle {
+  public static final ClippyResources I = GWT.create(ClippyResources.class);
 
-  String wseTabAfterSpace();
-  String wseTrailingSpace();
-  String wseBareCR();
-  String leCR();
+  @Source("clippy.css")
+  ClippyCss css();
 }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabel.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabel.java
new file mode 100644
index 0000000..273318b
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabel.java
@@ -0,0 +1,230 @@
+// 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.gwtexpui.clippy.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.event.dom.client.BlurEvent;
+import com.google.gwt.event.dom.client.BlurHandler;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.http.client.URL;
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HasText;
+import com.google.gwt.user.client.ui.InlineLabel;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.TextBox;
+import com.google.gwtexpui.safehtml.client.SafeHtml;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+import com.google.gwtexpui.user.client.UserAgent;
+
+/**
+ * Label which permits the user to easily copy the complete content.
+ * <p>
+ * If the Flash plugin is available a "movie" is embedded that provides
+ * one-click copying of the content onto the system clipboard. The label (if
+ * visible) can also be clicked, switching from a label to an input box,
+ * allowing the user to copy the text with a keyboard shortcut.
+ */
+public class CopyableLabel extends Composite implements HasText {
+  private static final int SWF_WIDTH = 110;
+  private static final int SWF_HEIGHT = 14;
+  private static String swfUrl;
+  private static boolean flashEnabled = true;
+
+  static {
+    ClippyResources.I.css().ensureInjected();
+  }
+
+  public static boolean isFlashEnabled() {
+    return flashEnabled;
+  }
+
+  public static void setFlashEnabled(final boolean on) {
+    flashEnabled = on;
+  }
+
+  private static String swfUrl() {
+    if (swfUrl == null) {
+      swfUrl = GWT.getModuleBaseURL() + "gwtexpui_clippy1.cache.swf";
+    }
+    return swfUrl;
+  }
+
+  private final FlowPanel content;
+  private String text;
+  private int visibleLen;
+  private Label textLabel;
+  private TextBox textBox;
+  private Element swf;
+
+  /**
+   * Create a new label
+   *
+   * @param str initial content
+   */
+  public CopyableLabel(final String str) {
+    this(str, true);
+  }
+
+  /**
+   * Create a new label
+   *
+   * @param str initial content
+   * @param showLabel if true, the content is shown, if false it is hidden from
+   *        view and only the copy icon is displayed.
+   */
+  public CopyableLabel(final String str, final boolean showLabel) {
+    content = new FlowPanel();
+    initWidget(content);
+
+    text = str;
+    visibleLen = text.length();
+
+    if (showLabel) {
+      textLabel = new InlineLabel(getText());
+      textLabel.setStyleName(ClippyResources.I.css().label());
+      textLabel.addClickHandler(new ClickHandler() {
+        @Override
+        public void onClick(final ClickEvent event) {
+          showTextBox();
+        }
+      });
+      content.add(textLabel);
+    }
+    embedMovie();
+  }
+
+  /**
+   * Change the text which is displayed in the clickable label.
+   *
+   * @param text the new preview text, should be shorter than the original text
+   *        which would be copied to the clipboard.
+   */
+  public void setPreviewText(final String text) {
+    if (textLabel != null) {
+      textLabel.setText(text);
+      visibleLen = text.length();
+    }
+  }
+
+  private void embedMovie() {
+    if (flashEnabled && UserAgent.hasFlash) {
+      final String flashVars = "text=" + URL.encodeQueryString(getText());
+      final SafeHtmlBuilder h = new SafeHtmlBuilder();
+
+      h.openElement("div");
+      h.setStyleName(ClippyResources.I.css().control());
+
+      h.openElement("object");
+      h.setWidth(SWF_WIDTH);
+      h.setHeight(SWF_HEIGHT);
+      h.setAttribute("classid", "clsid:d27cdb6e-ae6d-11cf-96b8-444553540000");
+      h.paramElement("movie", swfUrl());
+      h.paramElement("FlashVars", flashVars);
+
+      h.openElement("embed");
+      h.setWidth(SWF_WIDTH);
+      h.setHeight(SWF_HEIGHT);
+      h.setAttribute("wmode", "transparent");
+      h.setAttribute("type", "application/x-shockwave-flash");
+      h.setAttribute("src", swfUrl());
+      h.setAttribute("FlashVars", flashVars);
+      h.closeSelf();
+
+      h.closeElement("object");
+      h.closeElement("div");
+
+      if (swf != null) {
+        DOM.removeChild(getElement(), swf);
+      }
+      DOM.appendChild(getElement(), swf = SafeHtml.parse(h));
+    }
+  }
+
+  public String getText() {
+    return text;
+  }
+
+  public void setText(final String newText) {
+    text = newText;
+    visibleLen = newText.length();
+
+    if (textLabel != null) {
+      textLabel.setText(getText());
+    }
+    if (textBox != null) {
+      textBox.setText(getText());
+      textBox.selectAll();
+    }
+    embedMovie();
+  }
+
+  private void showTextBox() {
+    if (textBox == null) {
+      textBox = new TextBox();
+      textBox.setText(getText());
+      textBox.setVisibleLength(visibleLen);
+      textBox.addKeyPressHandler(new KeyPressHandler() {
+        @Override
+        public void onKeyPress(final KeyPressEvent event) {
+          if (event.isControlKeyDown() || event.isMetaKeyDown()) {
+            switch (event.getCharCode()) {
+              case 'c':
+              case 'x':
+                Scheduler.get().scheduleDeferred(new Command() {
+                  public void execute() {
+                    hideTextBox();
+                  }
+                });
+                break;
+            }
+          }
+        }
+      });
+      textBox.addBlurHandler(new BlurHandler() {
+        @Override
+        public void onBlur(final BlurEvent event) {
+          hideTextBox();
+        }
+      });
+      content.insert(textBox, 1);
+    }
+
+    textLabel.setVisible(false);
+    textBox.setVisible(true);
+    Scheduler.get().scheduleDeferred(new Command() {
+      @Override
+      public void execute() {
+        textBox.selectAll();
+        textBox.setFocus(true);
+      }
+    });
+  }
+
+  private void hideTextBox() {
+    if (textBox != null) {
+      textBox.removeFromParent();
+      textBox = null;
+    }
+    textLabel.setVisible(true);
+  }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/clippy.css b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/clippy.css
new file mode 100644
index 0000000..b962df3
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/clippy.css
@@ -0,0 +1,25 @@
+/* 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.
+ */
+
+.label {
+  vertical-align: top;
+}
+.control {
+  margin-left: 5px;
+  display: inline-block !important;
+  height: 14px;
+  width: 14px;
+  overflow: hidden;
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/public/gwtexpui_clippy1.cache.swf b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/public/gwtexpui_clippy1.cache.swf
new file mode 100644
index 0000000..e46886c
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/public/gwtexpui_clippy1.cache.swf
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/css/CSS.gwt.xml
similarity index 69%
copy from gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml
copy to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/css/CSS.gwt.xml
index d7e835f..b385987 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/css/CSS.gwt.xml
@@ -1,5 +1,5 @@
 <!--
- Copyright (C) 2011 The Android Open Source Project
+ 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.
@@ -13,8 +13,7 @@
  See the License for the specific language governing permissions and
  limitations under the License.
 -->
-<module rename-to="gerrit_ui">
-  <inherits name='com.google.gerrit.GerritGwtUI'/>
-  <set-property name="user.agent" value="gecko1_8" />
-  <set-property name="locale" value="default" />
+<module>
+  <define-linker name='cachecss' class='com.google.gwtexpui.css.rebind.CssLinker'/>
+  <add-linker name='cachecss'/>
 </module>
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/css/rebind/CssLinker.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/css/rebind/CssLinker.java
new file mode 100644
index 0000000..0f6992d
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/css/rebind/CssLinker.java
@@ -0,0 +1,130 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.css.rebind;
+
+import com.google.gwt.core.ext.LinkerContext;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.linker.AbstractLinker;
+import com.google.gwt.core.ext.linker.Artifact;
+import com.google.gwt.core.ext.linker.ArtifactSet;
+import com.google.gwt.core.ext.linker.LinkerOrder;
+import com.google.gwt.core.ext.linker.PublicResource;
+import com.google.gwt.core.ext.linker.impl.StandardLinkerContext;
+import com.google.gwt.core.ext.linker.impl.StandardStylesheetReference;
+import com.google.gwt.dev.util.Util;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+
+@LinkerOrder(LinkerOrder.Order.PRE)
+public class CssLinker extends AbstractLinker {
+  @Override
+  public String getDescription() {
+    return "CssLinker";
+  }
+
+  @Override
+  public ArtifactSet link(final TreeLogger logger, final LinkerContext context,
+      final ArtifactSet artifacts) throws UnableToCompleteException {
+    final ArtifactSet returnTo = new ArtifactSet();
+    int index = 0;
+
+    final HashMap<String, PublicResource> css =
+        new HashMap<String, PublicResource>();
+
+    for (final StandardStylesheetReference ssr : artifacts
+        .<StandardStylesheetReference> find(StandardStylesheetReference.class)) {
+      css.put(ssr.getSrc(), null);
+    }
+    for (final PublicResource pr : artifacts
+        .<PublicResource> find(PublicResource.class)) {
+      if (css.containsKey(pr.getPartialPath())) {
+        css.put(pr.getPartialPath(), new CssPubRsrc(name(logger, pr), pr));
+      }
+    }
+
+    for (Artifact<?> a : artifacts) {
+      if (a instanceof PublicResource) {
+        final PublicResource r = (PublicResource) a;
+        if (css.containsKey(r.getPartialPath())) {
+          a = css.get(r.getPartialPath());
+        }
+      } else if (a instanceof StandardStylesheetReference) {
+        final StandardStylesheetReference r = (StandardStylesheetReference) a;
+        final PublicResource p = css.get(r.getSrc());
+        a = new StandardStylesheetReference(p.getPartialPath(), index);
+      }
+
+      returnTo.add(a);
+      index++;
+    }
+    return returnTo;
+  }
+
+  private String name(final TreeLogger logger, final PublicResource r)
+      throws UnableToCompleteException {
+    final InputStream in = r.getContents(logger);
+    final ByteArrayOutputStream tmp = new ByteArrayOutputStream();
+    try {
+      try {
+        final byte[] buf = new byte[2048];
+        int n;
+        while ((n = in.read(buf)) >= 0) {
+          tmp.write(buf, 0, n);
+        }
+        tmp.close();
+      } finally {
+        in.close();
+      }
+    } catch (IOException e) {
+      final UnableToCompleteException ute = new UnableToCompleteException();
+      ute.initCause(e);
+      throw ute;
+    }
+
+    String base = r.getPartialPath();
+    final int s = base.lastIndexOf('/');
+    if (0 < s) {
+      base = base.substring(0, s + 1);
+    } else {
+      base = "";
+    }
+    return base + Util.computeStrongName(tmp.toByteArray()) + ".cache.css";
+  }
+
+  private static class CssPubRsrc extends PublicResource {
+    private static final long serialVersionUID = 1L;
+    private final PublicResource src;
+
+    CssPubRsrc(final String partialPath, final PublicResource r) {
+      super(StandardLinkerContext.class, partialPath);
+      src = r;
+    }
+
+    @Override
+    public InputStream getContents(final TreeLogger logger)
+        throws UnableToCompleteException {
+      return src.getContents(logger);
+    }
+
+    @Override
+    public long getLastModified() {
+      return src.getLastModified();
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/GlobalKey.gwt.xml
similarity index 69%
copy from gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml
copy to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/GlobalKey.gwt.xml
index d7e835f..771050f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/GlobalKey.gwt.xml
@@ -1,5 +1,5 @@
 <!--
- Copyright (C) 2011 The Android Open Source Project
+ 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.
@@ -13,8 +13,8 @@
  See the License for the specific language governing permissions and
  limitations under the License.
 -->
-<module rename-to="gerrit_ui">
-  <inherits name='com.google.gerrit.GerritGwtUI'/>
-  <set-property name="user.agent" value="gecko1_8" />
-  <set-property name="locale" value="default" />
+<module>
+  <inherits name='com.google.gwt.resources.Resources'/>
+  <inherits name='com.google.gwtexpui.user.User'/>
+  <inherits name='com.google.gwtexpui.safehtml.SafeHtml'/>
 </module>
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/CompoundKeyCommand.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/CompoundKeyCommand.java
new file mode 100644
index 0000000..304d56e
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/CompoundKeyCommand.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.gwtexpui.globalkey.client;
+
+import com.google.gwt.event.dom.client.KeyPressEvent;
+
+public final class CompoundKeyCommand extends KeyCommand {
+  final KeyCommandSet set;
+
+  public CompoundKeyCommand(int mask, char key, String help, KeyCommandSet s) {
+    super(mask, key, help);
+    set = s;
+  }
+
+  public CompoundKeyCommand(int mask, int key, String help, KeyCommandSet s) {
+    super(mask, key, help);
+    set = s;
+  }
+
+  public KeyCommandSet getSet() {
+    return set;
+  }
+
+  @Override
+  public void onKeyPress(final KeyPressEvent event) {
+    GlobalKey.temporaryWithTimeout(set);
+  }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/DocWidget.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/DocWidget.java
new file mode 100644
index 0000000..d680a72
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/DocWidget.java
@@ -0,0 +1,51 @@
+// 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.gwtexpui.globalkey.client;
+
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.Node;
+import com.google.gwt.event.dom.client.HasKeyPressHandlers;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.ui.RootPanel;
+import com.google.gwt.user.client.ui.Widget;
+
+class DocWidget extends Widget implements HasKeyPressHandlers {
+  private static DocWidget me;
+
+  static DocWidget get() {
+    if (me == null) {
+      me = new DocWidget();
+    }
+    return me;
+  }
+
+  private DocWidget() {
+    setElement((Element) docnode());
+    onAttach();
+    RootPanel.detachOnWindowClose(this);
+  }
+
+  @Override
+  public HandlerRegistration addKeyPressHandler(KeyPressHandler handler) {
+    return addDomHandler(handler, KeyPressEvent.getType());
+  }
+
+  private static Node docnode() {
+    return Document.get();
+  }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/GlobalKey.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/GlobalKey.java
new file mode 100644
index 0000000..1eaaa3c
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/GlobalKey.java
@@ -0,0 +1,183 @@
+// 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.gwtexpui.globalkey.client;
+
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.Timer;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.Widget;
+
+
+public class GlobalKey {
+  public static final KeyPressHandler STOP_PROPAGATION = new KeyPressHandler() {
+    @Override
+    public void onKeyPress(final KeyPressEvent event) {
+      event.stopPropagation();
+    }
+  };
+
+  private static State global;
+  static State active;
+  private static CloseHandler<PopupPanel> restoreGlobal;
+  private static Timer restoreTimer;
+
+  static {
+    KeyResources.I.css().ensureInjected();
+  }
+
+  private static void initEvents() {
+    if (active == null) {
+      DocWidget.get().addKeyPressHandler(new KeyPressHandler() {
+        @Override
+        public void onKeyPress(final KeyPressEvent event) {
+          final KeyCommandSet s = active.live;
+          if (s != active.all) {
+            active.live = active.all;
+            restoreTimer.cancel();
+          }
+          s.onKeyPress(event);
+        }
+      });
+
+      restoreTimer = new Timer() {
+        @Override
+        public void run() {
+          active.live = active.all;
+        }
+      };
+
+      global = new State(null);
+      active = global;
+    }
+  }
+
+  private static void initDialog() {
+    if (restoreGlobal == null) {
+      restoreGlobal = new CloseHandler<PopupPanel>() {
+        @Override
+        public void onClose(final CloseEvent<PopupPanel> event) {
+          active = global;
+        }
+      };
+    }
+  }
+
+  static void temporaryWithTimeout(final KeyCommandSet s) {
+    active.live = s;
+    restoreTimer.schedule(250);
+  }
+
+  public static void dialog(final PopupPanel panel) {
+    initEvents();
+    initDialog();
+    assert panel.isShowing();
+    assert active == global;
+    active = new State(panel);
+    active.add(new HidePopupPanelCommand(0, KeyCodes.KEY_ESCAPE, panel));
+    panel.addCloseHandler(restoreGlobal);
+  }
+
+  public static HandlerRegistration addApplication(final Widget widget,
+      final KeyCommand appKey) {
+    initEvents();
+    final State state = stateFor(widget);
+    state.add(appKey);
+    return new HandlerRegistration() {
+      @Override
+      public void removeHandler() {
+        state.remove(appKey);
+      }
+    };
+  }
+
+  public static HandlerRegistration add(final Widget widget,
+      final KeyCommandSet cmdSet) {
+    initEvents();
+    final State state = stateFor(widget);
+    state.add(cmdSet);
+    return new HandlerRegistration() {
+      @Override
+      public void removeHandler() {
+        state.remove(cmdSet);
+      }
+    };
+  }
+
+  private static State stateFor(Widget w) {
+    while (w != null) {
+      if (w == active.root) {
+        return active;
+      }
+      w = w.getParent();
+    }
+    return global;
+  }
+
+  public static void filter(final KeyCommandFilter filter) {
+    active.filter(filter);
+    if (active != global) {
+      global.filter(filter);
+    }
+  }
+
+  private GlobalKey() {
+  }
+
+  static class State {
+    final Widget root;
+    final KeyCommandSet app;
+    final KeyCommandSet all;
+    KeyCommandSet live;
+
+    State(final Widget r) {
+      root = r;
+
+      app = new KeyCommandSet(KeyConstants.I.applicationSection());
+      app.add(ShowHelpCommand.INSTANCE);
+
+      all = new KeyCommandSet();
+      all.add(app);
+
+      live = all;
+    }
+
+    void add(final KeyCommand k) {
+      app.add(k);
+      all.add(k);
+    }
+
+    void remove(final KeyCommand k) {
+      app.remove(k);
+      all.remove(k);
+    }
+
+    void add(final KeyCommandSet s) {
+      all.add(s);
+    }
+
+    void remove(final KeyCommandSet s) {
+      all.remove(s);
+    }
+
+    void filter(final KeyCommandFilter f) {
+      all.filter(f);
+    }
+  }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/HidePopupPanelCommand.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/HidePopupPanelCommand.java
new file mode 100644
index 0000000..0274b9d
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/HidePopupPanelCommand.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.globalkey.client;
+
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.user.client.ui.PopupPanel;
+
+/** Hides the given popup panel when invoked. */
+public class HidePopupPanelCommand extends KeyCommand {
+  private final PopupPanel panel;
+
+  public HidePopupPanelCommand(int mask, int key, PopupPanel panel) {
+    super(mask, key, KeyConstants.I.closeCurrentDialog());
+    this.panel = panel;
+  }
+
+  @Override
+  public void onKeyPress(final KeyPressEvent event) {
+    panel.hide();
+  }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommand.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommand.java
new file mode 100644
index 0000000..ba4f626
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommand.java
@@ -0,0 +1,94 @@
+// 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.gwtexpui.globalkey.client;
+
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwtexpui.safehtml.client.SafeHtml;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+
+
+public abstract class KeyCommand implements KeyPressHandler {
+  public static final int M_CTRL = 1 << 16;
+  public static final int M_ALT = 2 << 16;
+  public static final int M_META = 4 << 16;
+
+  public static boolean same(final KeyCommand a, final KeyCommand b) {
+    return a.getClass() == b.getClass() && a.helpText.equals(b.helpText);
+  }
+
+  final int keyMask;
+  private final String helpText;
+
+  public KeyCommand(final int mask, final int key, final String help) {
+    this(mask, (char) key, help);
+  }
+
+  public KeyCommand(final int mask, final char key, final String help) {
+    assert help != null;
+    keyMask = mask | key;
+    helpText = help;
+  }
+
+  public String getHelpText() {
+    return helpText;
+  }
+
+  SafeHtml describeKeyStroke() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+
+    if ((keyMask & M_CTRL) == M_CTRL) {
+      modifier(b, KeyConstants.I.keyCtrl());
+    }
+    if ((keyMask & M_ALT) == M_ALT) {
+      modifier(b, KeyConstants.I.keyAlt());
+    }
+    if ((keyMask & M_META) == M_META) {
+      modifier(b, KeyConstants.I.keyMeta());
+    }
+
+    final char c = (char) (keyMask & 0xffff);
+    switch (c) {
+      case KeyCodes.KEY_ENTER:
+        namedKey(b, KeyConstants.I.keyEnter());
+        break;
+      case KeyCodes.KEY_ESCAPE:
+        namedKey(b, KeyConstants.I.keyEsc());
+        break;
+      default:
+        b.openSpan();
+        b.setStyleName(KeyResources.I.css().helpKey());
+        b.append(String.valueOf(c));
+        b.closeSpan();
+        break;
+    }
+
+    return b;
+  }
+
+  private void modifier(final SafeHtmlBuilder b, final String name) {
+    namedKey(b, name);
+    b.append(" + ");
+  }
+
+  private void namedKey(final SafeHtmlBuilder b, final String name) {
+    b.append('<');
+    b.openSpan();
+    b.setStyleName(KeyResources.I.css().helpKey());
+    b.append(name);
+    b.closeSpan();
+    b.append(">");
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/TrackingIdAttribute.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandFilter.java
similarity index 75%
copy from gerrit-server/src/main/java/com/google/gerrit/server/events/TrackingIdAttribute.java
copy to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandFilter.java
index 7d55dd2..05f41d4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/TrackingIdAttribute.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandFilter.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 The Android Open Source Project
+// 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.
@@ -12,9 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.events;
+package com.google.gwtexpui.globalkey.client;
 
-public class TrackingIdAttribute {
-  public String system;
-  public String id;
+public interface KeyCommandFilter {
+  public boolean include(KeyCommand key);
 }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandSet.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandSet.java
new file mode 100644
index 0000000..4f3205a
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandSet.java
@@ -0,0 +1,136 @@
+// 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.gwtexpui.globalkey.client;
+
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+public class KeyCommandSet implements KeyPressHandler {
+  private final Map<Integer, KeyCommand> map;
+  private List<KeyCommandSet> sets;
+  private String name;
+
+  public KeyCommandSet() {
+    this("");
+  }
+
+  public KeyCommandSet(final String setName) {
+    map = new HashMap<Integer, KeyCommand>();
+    name = setName;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public void setName(final String setName) {
+    assert setName != null;
+    name = setName;
+  }
+
+  public boolean isEmpty() {
+    return map.isEmpty();
+  }
+
+  public void add(final KeyCommand k) {
+    assert !map.containsKey(k.keyMask)
+         : "Key " + k.describeKeyStroke().asString()
+         + " already registered";
+    if (!map.containsKey(k.keyMask)) {
+      map.put(k.keyMask, k);
+    }
+  }
+
+  public void remove(final KeyCommand k) {
+    assert map.get(k.keyMask) == k;
+    map.remove(k.keyMask);
+  }
+
+  public void add(final KeyCommandSet set) {
+    if (sets == null) {
+      sets = new ArrayList<KeyCommandSet>();
+    }
+    assert !sets.contains(set);
+    sets.add(set);
+    for (final KeyCommand k : set.map.values()) {
+      add(k);
+    }
+  }
+
+  public void remove(final KeyCommandSet set) {
+    assert sets != null;
+    assert sets.contains(set);
+    sets.remove(set);
+    for (final KeyCommand k : set.map.values()) {
+      remove(k);
+    }
+  }
+
+  public void filter(final KeyCommandFilter filter) {
+    if (sets != null) {
+      for (final KeyCommandSet s : sets) {
+        s.filter(filter);
+      }
+    }
+    for (final Iterator<KeyCommand> i = map.values().iterator(); i.hasNext();) {
+      final KeyCommand kc = i.next();
+      if (!filter.include(kc)) {
+        i.remove();
+      } else if (kc instanceof CompoundKeyCommand) {
+        ((CompoundKeyCommand) kc).set.filter(filter);
+      }
+    }
+  }
+
+  public Collection<KeyCommand> getKeys() {
+    return map.values();
+  }
+
+  public Collection<KeyCommandSet> getSets() {
+    return sets != null ? sets : Collections.<KeyCommandSet> emptyList();
+  }
+
+  @Override
+  public void onKeyPress(final KeyPressEvent event) {
+    final KeyCommand k = map.get(toMask(event));
+    if (k != null) {
+      event.preventDefault();
+      event.stopPropagation();
+      k.onKeyPress(event);
+    }
+  }
+
+  static int toMask(final KeyPressEvent event) {
+    int mask = event.getCharCode();
+    if (event.isAltKeyDown()) {
+      mask |= KeyCommand.M_ALT;
+    }
+    if (event.isControlKeyDown()) {
+      mask |= KeyCommand.M_CTRL;
+    }
+    if (event.isMetaKeyDown()) {
+      mask |= KeyCommand.M_META;
+    }
+    return mask;
+  }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyConstants.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyConstants.java
new file mode 100644
index 0000000..56fb85c
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyConstants.java
@@ -0,0 +1,37 @@
+// 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.gwtexpui.globalkey.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.i18n.client.Constants;
+
+public interface KeyConstants extends Constants {
+  public static final KeyConstants I = GWT.create(KeyConstants.class);
+
+  String applicationSection();
+  String showHelp();
+  String closeCurrentDialog();
+
+  String keyboardShortcuts();
+  String closeButton();
+  String orOtherKey();
+  String thenOtherKey();
+
+  String keyCtrl();
+  String keyAlt();
+  String keyMeta();
+  String keyEnter();
+  String keyEsc();
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyConstants.properties b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyConstants.properties
new file mode 100644
index 0000000..e21daf5
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyConstants.properties
@@ -0,0 +1,14 @@
+applicationSection = Application
+showHelp = Open shortcut help
+closeCurrentDialog = Close current dialog
+
+keyboardShortcuts = Keyboard Shortcuts
+closeButton = Close
+orOtherKey = or
+thenOtherKey = then
+
+keyCtrl = Ctrl
+keyAlt = Alt
+keyMeta = Meta
+keyEnter = Enter
+keyEsc = Esc
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCss.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCss.java
new file mode 100644
index 0000000..d19018d
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCss.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.globalkey.client;
+
+import com.google.gwt.resources.client.CssResource;
+
+public interface KeyCss extends CssResource {
+  String helpPopup();
+  String helpHeader();
+  String helpHeaderGlue();
+  String helpTable();
+  String helpTableGlue();
+  String helpGroup();
+  String helpKeyStroke();
+  String helpSeparator();
+  String helpKey();
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java
new file mode 100644
index 0000000..7bd0233
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java
@@ -0,0 +1,228 @@
+// 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.gwtexpui.globalkey.client;
+
+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.ui.Anchor;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.FocusPanel;
+import com.google.gwt.user.client.ui.Grid;
+import com.google.gwt.user.client.ui.HasHorizontalAlignment;
+import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
+import com.google.gwtexpui.safehtml.client.SafeHtml;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+import com.google.gwtexpui.user.client.PluginSafePopupPanel;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.List;
+
+
+public class KeyHelpPopup extends PluginSafePopupPanel implements
+    KeyPressHandler {
+  private final FocusPanel focus;
+
+  public KeyHelpPopup() {
+    super(true/* autohide */, true/* modal */);
+    setStyleName(KeyResources.I.css().helpPopup());
+
+    final Anchor closer = new Anchor(KeyConstants.I.closeButton());
+    closer.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(final ClickEvent event) {
+        hide();
+      }
+    });
+
+    final Grid header = new Grid(1, 3);
+    header.setStyleName(KeyResources.I.css().helpHeader());
+    header.setText(0, 0, KeyConstants.I.keyboardShortcuts());
+    header.setWidget(0, 2, closer);
+
+    final CellFormatter fmt = header.getCellFormatter();
+    fmt.addStyleName(0, 1, KeyResources.I.css().helpHeaderGlue());
+    fmt.setHorizontalAlignment(0, 2, HasHorizontalAlignment.ALIGN_RIGHT);
+
+    final Grid lists = new Grid(0, 7);
+    lists.setStyleName(KeyResources.I.css().helpTable());
+    populate(lists);
+    lists.getCellFormatter().addStyleName(0, 3,
+        KeyResources.I.css().helpTableGlue());
+
+    final FlowPanel body = new FlowPanel();
+    body.add(header);
+    DOM.appendChild(body.getElement(), DOM.createElement("hr"));
+    body.add(lists);
+
+    focus = new FocusPanel(body);
+    DOM.setStyleAttribute(focus.getElement(), "outline", "0px");
+    DOM.setElementAttribute(focus.getElement(), "hideFocus", "true");
+    focus.addKeyPressHandler(this);
+    add(focus);
+  }
+
+  @Override
+  public void setVisible(final boolean show) {
+    super.setVisible(show);
+    if (show) {
+      focus.setFocus(true);
+    }
+  }
+
+  @Override
+  public void onKeyPress(final KeyPressEvent event) {
+    if (KeyCommandSet.toMask(event) == ShowHelpCommand.INSTANCE.keyMask) {
+      // Block the '?' key from triggering us to show right after
+      // we just hide ourselves.
+      //
+      event.stopPropagation();
+      event.preventDefault();
+    }
+    hide();
+  }
+
+  private void populate(final Grid lists) {
+    int end[] = new int[5];
+    int column = 0;
+    for (final KeyCommandSet set : combinedSetsByName()) {
+      int row = end[column];
+      row = formatGroup(lists, row, column, set);
+      end[column] = row;
+      if (column == 0) {
+        column = 4;
+      } else {
+        column = 0;
+      }
+    }
+  }
+
+  /**
+   * @return an ordered collection of KeyCommandSet, combining sets which share
+   *         the same name, so that each set name appears at most once.
+   */
+  private static Collection<KeyCommandSet> combinedSetsByName() {
+    final LinkedHashMap<String, KeyCommandSet> byName =
+        new LinkedHashMap<String, KeyCommandSet>();
+    for (final KeyCommandSet set : GlobalKey.active.all.getSets()) {
+      KeyCommandSet v = byName.get(set.getName());
+      if (v == null) {
+        v = new KeyCommandSet(set.getName());
+        byName.put(v.getName(), v);
+      }
+      v.add(set);
+    }
+    return byName.values();
+  }
+
+  private int formatGroup(final Grid lists, int row, final int col,
+      final KeyCommandSet set) {
+    if (set.isEmpty()) {
+      return row;
+    }
+
+    if (lists.getRowCount() < row + 1) {
+      lists.resizeRows(row + 1);
+    }
+    lists.setText(row, col + 2, set.getName());
+    lists.getCellFormatter().addStyleName(row, col + 2,
+        KeyResources.I.css().helpGroup());
+    row++;
+
+    return formatKeys(lists, row, col, set, null);
+  }
+
+  private int formatKeys(final Grid lists, int row, final int col,
+      final KeyCommandSet set, final SafeHtml prefix) {
+    final CellFormatter fmt = lists.getCellFormatter();
+    final int initialRow = row;
+    final List<KeyCommand> keys = sort(set);
+    if (lists.getRowCount() < row + keys.size()) {
+      lists.resizeRows(row + keys.size());
+    }
+    FORMAT_KEYS: for (int i = 0; i < keys.size(); i++) {
+      final KeyCommand k = keys.get(i);
+
+      if (k instanceof CompoundKeyCommand) {
+        final SafeHtmlBuilder b = new SafeHtmlBuilder();
+        b.append(k.describeKeyStroke());
+        row = formatKeys(lists, row, col, ((CompoundKeyCommand) k).getSet(), b);
+        continue;
+      }
+
+      for (int prior = 0; prior < i; prior++) {
+        if (KeyCommand.same(keys.get(prior), k)) {
+          final int r = initialRow + prior;
+          final SafeHtmlBuilder b = new SafeHtmlBuilder();
+          b.append(SafeHtml.get(lists, r, col + 0));
+          b.append(" ");
+          b.append(KeyConstants.I.orOtherKey());
+          b.append(" ");
+          if (prefix != null) {
+            b.append(prefix);
+            b.append(" ");
+            b.append(KeyConstants.I.thenOtherKey());
+            b.append(" ");
+          }
+          b.append(k.describeKeyStroke());
+          SafeHtml.set(lists, r, col + 0, b);
+          continue FORMAT_KEYS;
+        }
+      }
+
+      if (prefix != null) {
+        final SafeHtmlBuilder b = new SafeHtmlBuilder();
+        b.append(prefix);
+        b.append(" ");
+        b.append(KeyConstants.I.thenOtherKey());
+        b.append(" ");
+        b.append(k.describeKeyStroke());
+        SafeHtml.set(lists, row, col + 0, b);
+      } else {
+        SafeHtml.set(lists, row, col + 0, k.describeKeyStroke());
+      }
+      lists.setText(row, col + 1, ":");
+      lists.setText(row, col + 2, k.getHelpText());
+
+      fmt.addStyleName(row, col + 0, KeyResources.I.css().helpKeyStroke());
+      fmt.addStyleName(row, col + 1, KeyResources.I.css().helpSeparator());
+      row++;
+    }
+
+    return row;
+  }
+
+  private List<KeyCommand> sort(final KeyCommandSet set) {
+    final List<KeyCommand> keys = new ArrayList<KeyCommand>(set.getKeys());
+    Collections.sort(keys, new Comparator<KeyCommand>() {
+      @Override
+      public int compare(KeyCommand arg0, KeyCommand arg1) {
+        if (arg0.keyMask < arg1.keyMask) {
+          return -1;
+        } else if (arg0.keyMask > arg1.keyMask) {
+          return 1;
+        }
+        return 0;
+      }
+    });
+    return keys;
+  }
+}
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyResources.java
similarity index 61%
copy from gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.java
copy to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyResources.java
index df60305..a52ca2a 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyResources.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 The Android Open Source Project
+// 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.
@@ -12,16 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.prettify.common;
+package com.google.gwtexpui.globalkey.client;
 
 import com.google.gwt.core.client.GWT;
-import com.google.gwt.i18n.client.Constants;
+import com.google.gwt.resources.client.ClientBundle;
 
-public interface PrettifyConstants extends Constants {
-  static final PrettifyConstants C = GWT.create(PrettifyConstants.class);
+public interface KeyResources extends ClientBundle {
+  public static final KeyResources I = GWT.create(KeyResources.class);
 
-  String wseTabAfterSpace();
-  String wseTrailingSpace();
-  String wseBareCR();
-  String leCR();
+  @Source("key.css")
+  KeyCss css();
 }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpTextArea.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpTextArea.java
new file mode 100644
index 0000000..c06d2c4
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpTextArea.java
@@ -0,0 +1,34 @@
+// 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.gwtexpui.globalkey.client;
+
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.ui.TextArea;
+
+public class NpTextArea extends TextArea {
+  public NpTextArea() {
+    addKeyPressHandler(GlobalKey.STOP_PROPAGATION);
+  }
+
+  public NpTextArea(final Element element) {
+    super(element);
+    addKeyPressHandler(GlobalKey.STOP_PROPAGATION);
+  }
+
+  public void setSpellCheck(boolean spell) {
+    DOM.setElementPropertyBoolean(getElement(), "spellcheck", spell);
+  }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpTextBox.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpTextBox.java
new file mode 100644
index 0000000..86402e1
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpTextBox.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.globalkey.client;
+
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.user.client.ui.TextBox;
+
+public class NpTextBox extends TextBox {
+  public NpTextBox() {
+    addKeyPressHandler(GlobalKey.STOP_PROPAGATION);
+  }
+
+  public NpTextBox(final Element element) {
+    super(element);
+    addKeyPressHandler(GlobalKey.STOP_PROPAGATION);
+  }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/ShowHelpCommand.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/ShowHelpCommand.java
new file mode 100644
index 0000000..50a4a86
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/ShowHelpCommand.java
@@ -0,0 +1,61 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.globalkey.client;
+
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.PopupPanel.PositionCallback;
+
+
+public class ShowHelpCommand extends KeyCommand {
+  public static final ShowHelpCommand INSTANCE = new ShowHelpCommand();
+  private static KeyHelpPopup current;
+
+  public ShowHelpCommand() {
+    super(0, '?', KeyConstants.I.showHelp());
+  }
+
+  @Override
+  public void onKeyPress(final KeyPressEvent event) {
+    if (current != null) {
+      // Already open? Close the dialog.
+      //
+      current.hide();
+      current = null;
+      return;
+    }
+
+    final KeyHelpPopup help = new KeyHelpPopup();
+    help.addCloseHandler(new CloseHandler<PopupPanel>() {
+      @Override
+      public void onClose(final CloseEvent<PopupPanel> event) {
+        current = null;
+      }
+    });
+    current = help;
+    help.setPopupPositionAndShow(new PositionCallback() {
+      @Override
+      public void setPosition(final int pWidth, final int pHeight) {
+        final int left = (Window.getClientWidth() - pWidth) >> 1;
+        final int wLeft = Window.getScrollLeft();
+        final int wTop = Window.getScrollTop();
+        help.setPopupPosition(wLeft + left, wTop + 50);
+      }
+    });
+  }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/key.css b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/key.css
new file mode 100644
index 0000000..9372e45
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/key.css
@@ -0,0 +1,99 @@
+/* Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@external .popupContent;
+
+.helpPopup {
+  background: #000000 none repeat scroll 0 50%;
+  color: #ffffff;
+  font-family: arial,sans-serif;
+  font-weight: bold;
+  overflow: hidden;
+  text-align: left;
+  text-shadow: 1px 1px 7px #000000;
+  width: 92%;
+  z-index: 1002;
+  opacity: 0.85;
+ }
+
+@if user.agent safari {
+  .helpPopup {
+    \-webkit-border-radius: 10px;
+  }
+}
+@if user.agent gecko1_8 {
+  .helpPopup {
+    \-moz-border-radius: 10px;
+  }
+}
+
+.helpPopup .popupContent {
+  margin: 10px;
+}
+
+.helpPopup hr {
+  width: 100%;
+}
+
+.helpHeader {
+  width: 100%;
+}
+
+.helpHeader td {
+  white-space: nowrap;
+  color: #ffffff;
+}
+
+.helpHeader a,
+.helpHeader a:visited,
+.helpHeader a:hover {
+  color: #dddd00;
+}
+
+.helpHeaderGlue {
+  width: 100%;
+}
+
+.helpTable {
+  width: 90%;
+}
+.helpTable td {
+  vertical-align: top;
+  white-space: nowrap;
+}
+
+.helpTableGlue {
+  width: 25px;
+}
+
+.helpGroup {
+  color: #dddd00;
+  padding-top: 0.8em;
+  text-align: left;
+}
+
+.helpKeyStroke {
+  text-align: right;
+}
+
+.helpSeparator {
+  width: 0.5em;
+  text-align: center;
+  font-weight: bold;
+}
+
+.helpKey {
+  color: #dddd00;
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/ServerPlannedIFrameLinker.gwt.xml
similarity index 63%
copy from gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml
copy to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/ServerPlannedIFrameLinker.gwt.xml
index d7e835f..a6978ab 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/ServerPlannedIFrameLinker.gwt.xml
@@ -1,11 +1,11 @@
 <!--
- Copyright (C) 2011 The Android Open Source Project
+ 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
+ http:..www.apache.org.licenses.LICENSE-2.0
 
  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,8 +13,7 @@
  See the License for the specific language governing permissions and
  limitations under the License.
 -->
-<module rename-to="gerrit_ui">
-  <inherits name='com.google.gerrit.GerritGwtUI'/>
-  <set-property name="user.agent" value="gecko1_8" />
-  <set-property name="locale" value="default" />
+<module>
+  <define-linker name='serverplanned' class='com.google.gwtexpui.linker.rebind.ServerPlannedIFrameLinker'/>
+  <add-linker name='serverplanned'/>
 </module>
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/rebind/ServerPlannedIFrameLinker.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/rebind/ServerPlannedIFrameLinker.java
new file mode 100644
index 0000000..3e2361c
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/rebind/ServerPlannedIFrameLinker.java
@@ -0,0 +1,67 @@
+// 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.gwtexpui.linker.rebind;
+
+import com.google.gwt.core.ext.LinkerContext;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.linker.AbstractLinker;
+import com.google.gwt.core.ext.linker.ArtifactSet;
+import com.google.gwt.core.ext.linker.CompilationResult;
+import com.google.gwt.core.ext.linker.LinkerOrder;
+import com.google.gwt.core.ext.linker.SelectionProperty;
+import com.google.gwt.core.ext.linker.StylesheetReference;
+
+import java.util.Map;
+import java.util.SortedMap;
+
+/** Saves data normally used by the {@code nocache.js} file. */
+@LinkerOrder(LinkerOrder.Order.POST)
+public class ServerPlannedIFrameLinker extends AbstractLinker {
+  @Override
+  public String getDescription() {
+    return "ServerPlannedIFrameLinker";
+  }
+
+  @Override
+  public ArtifactSet link(final TreeLogger logger, final LinkerContext context,
+      final ArtifactSet artifacts) throws UnableToCompleteException {
+    ArtifactSet toReturn = new ArtifactSet(artifacts);
+
+    StringBuilder table = new StringBuilder();
+    for (StylesheetReference r : artifacts.find(StylesheetReference.class)) {
+      table.append("css ");
+      table.append(r.getSrc());
+      table.append("\n");
+    }
+
+    for (CompilationResult r : artifacts.find(CompilationResult.class)) {
+      table.append(r.getStrongName() + "\n");
+      for (SortedMap<SelectionProperty, String> p : r.getPropertyMap()) {
+        for (Map.Entry<SelectionProperty, String> e : p.entrySet()) {
+          table.append("  ");
+          table.append(e.getKey().getName());
+          table.append("=");
+          table.append(e.getValue());
+          table.append('\n');
+        }
+      }
+      table.append("\n");
+    }
+
+    toReturn.add(emitString(logger, table.toString(), "permutations"));
+    return toReturn;
+  }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/ClientSideRule.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/ClientSideRule.java
new file mode 100644
index 0000000..89da529
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/ClientSideRule.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.linker.server;
+
+import javax.servlet.http.HttpServletRequest;
+
+/** A rule that must execute on the client, as we don't know how to compute it. */
+final class ClientSideRule implements Rule {
+  private final String name;
+
+  ClientSideRule(String name) {
+    this.name = name;
+  }
+
+  @Override
+  public String getName() {
+    return name;
+  }
+
+  @Override
+  public String select(HttpServletRequest req) {
+    return null;
+  }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/Permutation.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/Permutation.java
new file mode 100644
index 0000000..b319db1
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/Permutation.java
@@ -0,0 +1,160 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.linker.server;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Arrays;
+
+/** A single permutation of the compiled GWT application. */
+public class Permutation {
+  private final PermutationSelector selector;
+  private final String cacheHTML;
+  private final String[] values;
+
+  Permutation(PermutationSelector sel, String cacheHTML, String[] values) {
+    this.selector = sel;
+    this.cacheHTML = cacheHTML;
+    this.values = values;
+  }
+
+  boolean matches(String[] r) {
+    return Arrays.equals(values, r);
+  }
+
+  /**
+   * Append GWT bootstrap for this permutation onto the end of the body.
+   * <p>
+   * The GWT bootstrap for this particular permutation is appended onto the end
+   * of the {@code body} element of the passed host page.
+   * <p>
+   * To keep the bootstrap code small and simple, not all GWT features are
+   * actually supported. The {@code gwt:property}, {@code gwt:onPropertyErrorFn}
+   * and {@code gwt:onLoadErrorFn} meta tags are ignored and not handled.
+   * <p>
+   * Load order may differ from the standard GWT {@code nocache.js}. The browser
+   * is asked to load the iframe immediately, rather than after the body has
+   * finished loading.
+   *
+   * @param dom host page HTML document.
+   */
+  public void inject(Document dom) {
+    String moduleName = selector.getModuleName();
+    String moduleFunc = moduleName;
+
+    StringBuilder s = new StringBuilder();
+    s.append("\n");
+    s.append("function " + moduleFunc + "(){");
+    s.append("var s,l,t");
+    s.append(",w=window");
+    s.append(",d=document");
+    s.append(",n='" + moduleName + "'");
+    s.append(",f=d.createElement('iframe')");
+    s.append(";");
+
+    // Callback to execute the module once both s and l are true.
+    //
+    s.append("function m(){");
+    s.append("if(s&&l){");
+    // Base path needs to be absolute. There isn't an easy way to do this
+    // other than forcing an image to load and then pulling the URL back.
+    //
+    s.append("var b,i=d.createElement('img');");
+    s.append("i.src=n+'/clear.cache.gif';");
+    s.append("b=i.src;");
+    s.append("b=b.substring(0,b.lastIndexOf('/')+1);");
+    s.append(moduleFunc + "=null;"); // allow us to GC
+    s.append("f.contentWindow.gwtOnLoad(undefined,n,b);");
+    s.append("}");
+    s.append("}");
+
+    // Set s true when the module script has finished loading. The
+    // exact name here is known to the IFrameLinker and is called by
+    // the code in the iframe.
+    //
+    s.append(moduleFunc + ".onScriptLoad=function(){");
+    s.append("s=1;m();");
+    s.append("};");
+
+    // Set l true when the browser has finished processing the iframe
+    // tag, and everything else on the page.
+    //
+    s.append(moduleFunc + ".r=function(){");
+    s.append("l=1;m();");
+    s.append("};");
+
+    // Prevents mixed mode security in IE6/7.
+    s.append("f.src=\"javascript:''\";");
+    s.append("f.id=n;");
+    s.append("f.style.cssText"
+        + "='position:absolute;width:0;height:0;border:none';");
+    s.append("f.tabIndex=-1;");
+    s.append("d.body.appendChild(f);");
+
+    // The src has to be set after the iframe is attached to the DOM to avoid
+    // refresh quirks in Safari. We have to use the location.replace trick to
+    // avoid FF2 refresh quirks.
+    //
+    s.append("f.contentWindow.location.replace(n+'/" + cacheHTML + "');");
+
+    // defer attribute here is to workaround IE running immediately.
+    //
+    s.append("d.write('<script defer=\"defer\">" //
+        + moduleFunc + ".r()</'+'script>');");
+    s.append("}");
+    s.append(moduleFunc + "();");
+    s.append("\n//");
+
+    final Element html = dom.getDocumentElement();
+    final Element head = (Element) html.getElementsByTagName("head").item(0);
+    final Element body = (Element) html.getElementsByTagName("body").item(0);
+
+    for (String css : selector.getCSS()) {
+      if (isRelativeURL(css)) {
+        css = moduleName + '/' + css;
+      }
+
+      final Element link = dom.createElement("link");
+      link.setAttribute("rel", "stylesheet");
+      link.setAttribute("href", css);
+      head.appendChild(link);
+    }
+
+    final Element script = dom.createElement("script");
+    script.setAttribute("type", "text/javascript");
+    script.setAttribute("language", "javascript");
+    script.appendChild(dom.createComment(s.toString()));
+    body.appendChild(script);
+  }
+
+  private static boolean isRelativeURL(String src) {
+    if (src.startsWith("/")) {
+      return false;
+    }
+
+    try {
+      // If it parses as a URL, assume it is not relative.
+      //
+      new URL(src);
+      return false;
+    } catch (MalformedURLException e) {
+    }
+
+    return true;
+  }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/PermutationSelector.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/PermutationSelector.java
new file mode 100644
index 0000000..d3e5ae3
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/PermutationSelector.java
@@ -0,0 +1,205 @@
+// 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.gwtexpui.linker.server;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Selects a permutation based on the HTTP request.
+ * <p>
+ * To use this class the application's GWT module must include our linker by
+ * inheriting our module:
+ *
+ * <pre>
+ *   &lt;inherits name='com.google.gwtexpui.linker.ServerPlannedIFrameLinker'/&gt;
+ * </pre>
+ */
+public class PermutationSelector {
+  private final String moduleName;
+  private final Map<String, Rule> rulesByName;
+  private final List<Rule> ruleOrder;
+  private final List<Permutation> permutations;
+  private final List<String> css;
+
+  /**
+   * Create an empty selector for a module.
+   * <p>
+   * {@link UserAgentRule} rule is automatically registered. Additional custom
+   * selector rules may be registered before {@link #init(ServletContext)} is
+   * called to finish the selector setup.
+   *
+   * @param moduleName the name of the module within the context.
+   */
+  public PermutationSelector(final String moduleName) {
+    this.moduleName = moduleName;
+
+    this.rulesByName = new HashMap<String, Rule>();
+    this.ruleOrder = new ArrayList<Rule>();
+    this.permutations = new ArrayList<Permutation>();
+    this.css = new ArrayList<String>();
+
+    register(new UserAgentRule());
+  }
+
+  private void notInitialized() {
+    if (!ruleOrder.isEmpty()) {
+      throw new IllegalStateException("Already initialized");
+    }
+  }
+
+  /**
+   * Register a property selection rule.
+   *
+   * @param r the rule implementation.
+   */
+  public void register(Rule r) {
+    notInitialized();
+    rulesByName.put(r.getName(), r);
+  }
+
+  /**
+   * Initialize the selector by reading the module's {@code permutations} file.
+   *
+   * @param ctx context to load the module data from.
+   * @throws ServletException
+   * @throws IOException
+   */
+  public void init(ServletContext ctx) throws ServletException, IOException {
+    notInitialized();
+
+    final String tableName = "/" + moduleName + "/permutations";
+    final InputStream in = ctx.getResourceAsStream(tableName);
+    if (in == null) {
+      throw new ServletException("No " + tableName + " in context");
+    }
+    try {
+      BufferedReader r = new BufferedReader(new InputStreamReader(in, "UTF-8"));
+      for (;;) {
+        final String strongName = r.readLine();
+        if (strongName == null) {
+          break;
+        }
+
+        if (strongName.startsWith("css ")) {
+          css.add(strongName.substring("css ".length()));
+          continue;
+        }
+
+        Map<String, String> selections = new LinkedHashMap<String, String>();
+        for (;;) {
+          String permutation = r.readLine();
+          if (permutation == null || permutation.isEmpty()) {
+            break;
+          }
+
+          int eq = permutation.indexOf('=');
+          if (eq < 0) {
+            throw new ServletException(tableName + " has malformed content");
+          }
+
+          String k = permutation.substring(0, eq).trim();
+          String v = permutation.substring(eq + 1);
+
+          Rule rule = get(k);
+          if (!ruleOrder.contains(rule)) {
+            ruleOrder.add(rule);
+          }
+
+          if (selections.put(k, v) != null) {
+            throw new ServletException("Table " + tableName
+                + " has multiple values for " + k + " within permutation "
+                + strongName);
+          }
+        }
+
+        String cacheHtml = strongName + ".cache.html";
+        String[] values = new String[ruleOrder.size()];
+        for (int i = 0; i < values.length; i++) {
+          values[i] = selections.get(ruleOrder.get(i).getName());
+        }
+        permutations.add(new Permutation(this, cacheHtml, values));
+      }
+    } finally {
+      in.close();
+    }
+  }
+
+  private Rule get(final String name) {
+    Rule r = rulesByName.get(name);
+    if (r == null) {
+      r = new ClientSideRule(name);
+      register(r);
+    }
+    return r;
+  }
+
+  /** @return name of the module (within the application context). */
+  public String getModuleName() {
+    return moduleName;
+  }
+
+  /** @return all possible permutations */
+  public List<Permutation> getPermutations() {
+    return Collections.unmodifiableList(permutations);
+  }
+
+  /**
+   * Select the permutation that best matches the browser request.
+   *
+   * @param req current request.
+   * @return the selected permutation; null if no permutation can be fit to the
+   *         request and the standard {@code nocache.js} loader must be used.
+   */
+  public Permutation select(HttpServletRequest req) {
+    final String[] values = new String[ruleOrder.size()];
+    for (int i = 0; i < values.length; i++) {
+      final String value = ruleOrder.get(i).select(req);
+      if (value == null) {
+        // If the rule returned null it doesn't know how to compute
+        // the value for this HTTP request. Since we can't do that
+        // defer to JavaScript by not picking a permutation.
+        //
+        return null;
+      }
+      values[i] = value;
+    }
+
+    for (Permutation p : permutations) {
+      if (p.matches(values)) {
+        return p;
+      }
+    }
+
+    return null;
+  }
+
+  Collection<String> getCSS() {
+    return css;
+  }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/Rule.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/Rule.java
new file mode 100644
index 0000000..76b9b51
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/Rule.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.gwtexpui.linker.server;
+
+import javax.servlet.http.HttpServletRequest;
+
+/** A selection rule for a permutation property. */
+public interface Rule {
+  /** @return the property name, for example {@code "user.agent"}. */
+  public String getName();
+
+  /**
+   * Compute the value for this property, given the current request.
+   * <p>
+   * This rule method must compute the proper permutation value, matching what
+   * the GWT module XML files use for this property. The rule may use any state
+   * available in the current servlet request.
+   * <p>
+   * If this method returns {@code null} server side selection will be aborted
+   * and selection for all properties will be handled on the client side by the
+   * {@code nocache.js} file.
+   *
+   * @param req the request
+   * @return the value for the property; null if the value cannot be determined
+   *         on the server side.
+   */
+  public String select(HttpServletRequest req);
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/UserAgentRule.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/UserAgentRule.java
new file mode 100644
index 0000000..366b6c5
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/UserAgentRule.java
@@ -0,0 +1,93 @@
+// 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.gwtexpui.linker.server;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import static java.util.regex.Pattern.compile;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Selects the value for the {@code user.agent} property.
+ * <p>
+ * Examines the {@code User-Agent} HTTP request header, and tries to match it to
+ * known {@code user.agent} values.
+ * <p>
+ * Ported from JavaScript in {@code com.google.gwt.user.UserAgent.gwt.xml}.
+ */
+public class UserAgentRule implements Rule {
+  private static final Pattern msie = compile(".*msie ([0-9]+)\\.([0-9]+).*");
+  private static final Pattern gecko = compile(".*rv:([0-9]+)\\.([0-9]+).*");
+
+  public String getName() {
+    return "user.agent";
+  }
+
+  @Override
+  public String select(HttpServletRequest req) {
+    String ua = req.getHeader("User-Agent");
+    if (ua == null) {
+      return null;
+    }
+
+    ua = ua.toLowerCase();
+
+    if (ua.indexOf("opera") != -1) {
+      return "opera";
+
+    } else if (ua.indexOf("webkit") != -1) {
+      return "safari";
+
+    } else if (ua.indexOf("msie") != -1) {
+      // GWT 2.0 uses document.documentMode here, which we can't do
+      // on the server side.
+
+      Matcher m = msie.matcher(ua);
+      if (m.matches() && m.groupCount() == 2) {
+        int v = makeVersion(m);
+        if (v >= 10000) {
+          return "ie10";
+        }
+        if (v >= 9000) {
+          return "ie9";
+        }
+        if (v >= 8000) {
+          return "ie8";
+        }
+        if (v >= 6000) {
+          return "ie6";
+        }
+      }
+      return null;
+
+    } else if (ua.indexOf("gecko") != -1) {
+      Matcher m = gecko.matcher(ua);
+      if (m.matches() && m.groupCount() == 2) {
+        if (makeVersion(m) >= 1008) {
+          return "gecko1_8";
+        }
+      }
+      return "gecko";
+    }
+
+    return null;
+  }
+
+  private int makeVersion(Matcher result) {
+    return (Integer.parseInt(result.group(1)) * 1000)
+        + Integer.parseInt(result.group(2));
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/Progress.gwt.xml
similarity index 69%
copy from gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml
copy to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/Progress.gwt.xml
index d7e835f..0df8928 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/Progress.gwt.xml
@@ -1,5 +1,5 @@
 <!--
- Copyright (C) 2011 The Android Open Source Project
+ 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.
@@ -13,8 +13,7 @@
  See the License for the specific language governing permissions and
  limitations under the License.
 -->
-<module rename-to="gerrit_ui">
-  <inherits name='com.google.gerrit.GerritGwtUI'/>
-  <set-property name="user.agent" value="gecko1_8" />
-  <set-property name="locale" value="default" />
+<module>
+  <inherits name='com.google.gwt.resources.Resources'/>
+  <inherits name="com.google.gwt.user.User"/>
 </module>
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressBar.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressBar.java
new file mode 100644
index 0000000..5e13f55
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressBar.java
@@ -0,0 +1,77 @@
+// 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.gwtexpui.progress.client;
+
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.Label;
+
+/**
+ * A simple progress bar with a text label.
+ * <p>
+ * The bar is 200 pixels wide and 20 pixels high. To keep the implementation
+ * simple and lightweight this dimensions are fixed and shouldn't be modified by
+ * style overrides in client code or CSS.
+ */
+public class ProgressBar extends Composite {
+  static {
+    ProgressResources.I.css().ensureInjected();
+  }
+
+  private final String callerText;
+  private final Label bar;
+  private final Label msg;
+  private int value;
+
+  /** Create a bar with no message text. */
+  public ProgressBar() {
+    this("");
+  }
+
+  /** Create a bar displaying the specified message. */
+  public ProgressBar(final String text) {
+    if (text == null || text.length() == 0) {
+      callerText = "";
+    } else {
+      callerText = text + " ";
+    }
+
+    final FlowPanel body = new FlowPanel();
+    body.setStyleName(ProgressResources.I.css().container());
+
+    msg = new Label(callerText);
+    msg.setStyleName(ProgressResources.I.css().text());
+    body.add(msg);
+
+    bar = new Label("");
+    bar.setStyleName(ProgressResources.I.css().bar());
+    body.add(bar);
+
+    initWidget(body);
+  }
+
+  /** @return the current value of the progress meter. */
+  public int getValue() {
+    return value;
+  }
+
+  /** Update the bar's percent completion. */
+  public void setValue(final int pComplete) {
+    assert 0 <= pComplete && pComplete <= 100;
+    value = pComplete;
+    bar.setWidth("" + (2 * pComplete) + "px");
+    msg.setText(callerText + pComplete + "%");
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/QueryStats.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressCss.java
similarity index 67%
copy from gerrit-server/src/main/java/com/google/gerrit/server/events/QueryStats.java
copy to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressCss.java
index ecf2b9a..9de2748 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/QueryStats.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressCss.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 The Android Open Source Project
+// 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.
@@ -12,11 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.events;
+package com.google.gwtexpui.progress.client;
 
-public class QueryStats {
-  public final String type = "stats";
-  public int rowCount;
-  public long runTimeMilliseconds;
-  public String resumeSortKey;
+import com.google.gwt.resources.client.CssResource;
+
+public interface ProgressCss extends CssResource {
+  String container();
+  String text();
+  String bar();
 }
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressResources.java
similarity index 61%
copy from gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.java
copy to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressResources.java
index df60305..0276e9a 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressResources.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 The Android Open Source Project
+// 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.
@@ -12,16 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.prettify.common;
+package com.google.gwtexpui.progress.client;
 
 import com.google.gwt.core.client.GWT;
-import com.google.gwt.i18n.client.Constants;
+import com.google.gwt.resources.client.ClientBundle;
 
-public interface PrettifyConstants extends Constants {
-  static final PrettifyConstants C = GWT.create(PrettifyConstants.class);
+public interface ProgressResources extends ClientBundle {
+  public static final ProgressResources I = GWT.create(ProgressResources.class);
 
-  String wseTabAfterSpace();
-  String wseTrailingSpace();
-  String wseBareCR();
-  String leCR();
+  @Source("progress.css")
+  ProgressCss css();
 }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/progress.css b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/progress.css
new file mode 100644
index 0000000..683396e
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/progress.css
@@ -0,0 +1,43 @@
+/* Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.container {
+  position: relative;
+  border: 1px solid #6B90DA;
+  height: 20px;
+  width: 200px;
+}
+
+.text {
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  z-index: 2;
+  width: 200px;
+  padding-bottom: 3px;
+  text-align: center;
+  font-weight: bold;
+  font-style: italic;
+  font-size: smaller;
+}
+
+.bar {
+  background: #F0F7F9;
+  border-right: 1px solid #D0D7D9;
+  position: absolute;
+  top: 0;
+  left: 0;
+  height: 20px;
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/SafeHtml.gwt.xml
similarity index 69%
copy from gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml
copy to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/SafeHtml.gwt.xml
index d7e835f..0df8928 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/SafeHtml.gwt.xml
@@ -1,5 +1,5 @@
 <!--
- Copyright (C) 2011 The Android Open Source Project
+ 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.
@@ -13,8 +13,7 @@
  See the License for the specific language governing permissions and
  limitations under the License.
 -->
-<module rename-to="gerrit_ui">
-  <inherits name='com.google.gerrit.GerritGwtUI'/>
-  <set-property name="user.agent" value="gecko1_8" />
-  <set-property name="locale" value="default" />
+<module>
+  <inherits name='com.google.gwt.resources.Resources'/>
+  <inherits name="com.google.gwt.user.User"/>
 </module>
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/AttMap.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/AttMap.java
new file mode 100644
index 0000000..46d7f51
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/AttMap.java
@@ -0,0 +1,137 @@
+// 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.gwtexpui.safehtml.client;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/** Lightweight map of names/values for element attribute construction. */
+class AttMap {
+  private static final Tag ANY = new AnyTag();
+  private static final HashMap<String, Tag> TAGS;
+  static {
+    final Tag src = new SrcTag();
+    TAGS = new HashMap<String, Tag>();
+    TAGS.put("a", new AnchorTag());
+    TAGS.put("form", new FormTag());
+    TAGS.put("img", src);
+    TAGS.put("script", src);
+    TAGS.put("frame", src);
+  }
+
+  private final ArrayList<String> names = new ArrayList<String>();
+  private final ArrayList<String> values = new ArrayList<String>();
+
+  private Tag tag = ANY;
+  private int live;
+
+  void reset(final String tagName) {
+    tag = TAGS.get(tagName.toLowerCase());
+    if (tag == null) {
+      tag = ANY;
+    }
+    live = 0;
+  }
+
+  void onto(final Buffer raw, final SafeHtmlBuilder esc) {
+    for (int i = 0; i < live; i++) {
+      final String v = values.get(i);
+      if (v.length() > 0) {
+        raw.append(" ");
+        raw.append(names.get(i));
+        raw.append("=\"");
+        esc.append(v);
+        raw.append("\"");
+      }
+    }
+  }
+
+  String get(String name) {
+    name = name.toLowerCase();
+
+    for (int i = 0; i < live; i++) {
+      if (name.equals(names.get(i))) {
+        return values.get(i);
+      }
+    }
+    return "";
+  }
+
+  void set(String name, final String value) {
+    name = name.toLowerCase();
+    tag.assertSafe(name, value);
+
+    for (int i = 0; i < live; i++) {
+      if (name.equals(names.get(i))) {
+        values.set(i, value);
+        return;
+      }
+    }
+
+    final int i = live++;
+    if (names.size() < live) {
+      names.add(name);
+      values.add(value);
+    } else {
+      names.set(i, name);
+      values.set(i, value);
+    }
+  }
+
+  private static void assertNotJavascriptUrl(final String value) {
+    if (value.startsWith("#")) {
+      // common in GWT, and safe, so bypass further checks
+
+    } else if (value.trim().toLowerCase().startsWith("javascript:")) {
+      // possibly unsafe, we could have random user code here
+      // we can't tell if its safe or not so we refuse to accept
+      //
+      throw new RuntimeException("javascript unsafe in href: " + value);
+    }
+  }
+
+  private static interface Tag {
+    void assertSafe(String name, String value);
+  }
+
+  private static class AnyTag implements Tag {
+    public void assertSafe(String name, String value) {
+    }
+  }
+
+  private static class AnchorTag implements Tag {
+    public void assertSafe(String name, String value) {
+      if ("href".equals(name)) {
+        assertNotJavascriptUrl(value);
+      }
+    }
+  }
+
+  private static class FormTag implements Tag {
+    public void assertSafe(String name, String value) {
+      if ("action".equals(name)) {
+        assertNotJavascriptUrl(value);
+      }
+    }
+  }
+
+  private static class SrcTag implements Tag {
+    public void assertSafe(String name, String value) {
+      if ("src".equals(name)) {
+        assertNotJavascriptUrl(value);
+      }
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/QueryStats.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/Buffer.java
similarity index 63%
copy from gerrit-server/src/main/java/com/google/gerrit/server/events/QueryStats.java
copy to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/Buffer.java
index ecf2b9a..d79c580 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/QueryStats.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/Buffer.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 The Android Open Source Project
+// 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.
@@ -12,11 +12,22 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.events;
+package com.google.gwtexpui.safehtml.client;
 
-public class QueryStats {
-  public final String type = "stats";
-  public int rowCount;
-  public long runTimeMilliseconds;
-  public String resumeSortKey;
+interface Buffer {
+  void append(boolean v);
+
+  void append(char v);
+
+  void append(int v);
+
+  void append(long v);
+
+  void append(float v);
+
+  void append(double v);
+
+  void append(String v);
+
+  String toString();
 }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/BufferDirect.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/BufferDirect.java
new file mode 100644
index 0000000..a1801ad
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/BufferDirect.java
@@ -0,0 +1,56 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.safehtml.client;
+
+final class BufferDirect implements Buffer {
+  private final StringBuilder strbuf = new StringBuilder();
+
+  boolean isEmpty() {
+    return strbuf.length() == 0;
+  }
+
+  public void append(final boolean v) {
+    strbuf.append(v);
+  }
+
+  public void append(final char v) {
+    strbuf.append(v);
+  }
+
+  public void append(final int v) {
+    strbuf.append(v);
+  }
+
+  public void append(final long v) {
+    strbuf.append(v);
+  }
+
+  public void append(final float v) {
+    strbuf.append(v);
+  }
+
+  public void append(final double v) {
+    strbuf.append(v);
+  }
+
+  public void append(final String v) {
+    strbuf.append(v);
+  }
+
+  @Override
+  public String toString() {
+    return strbuf.toString();
+  }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/BufferSealElement.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/BufferSealElement.java
new file mode 100644
index 0000000..6b5346d
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/BufferSealElement.java
@@ -0,0 +1,56 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.safehtml.client;
+
+final class BufferSealElement implements Buffer {
+  private final SafeHtmlBuilder shb;
+
+  BufferSealElement(final SafeHtmlBuilder safeHtmlBuilder) {
+    shb = safeHtmlBuilder;
+  }
+
+  public void append(final boolean v) {
+    shb.sealElement().append(v);
+  }
+
+  public void append(final char v) {
+    shb.sealElement().append(v);
+  }
+
+  public void append(final double v) {
+    shb.sealElement().append(v);
+  }
+
+  public void append(final float v) {
+    shb.sealElement().append(v);
+  }
+
+  public void append(final int v) {
+    shb.sealElement().append(v);
+  }
+
+  public void append(final long v) {
+    shb.sealElement().append(v);
+  }
+
+  public void append(final String v) {
+    shb.sealElement().append(v);
+  }
+
+  @Override
+  public String toString() {
+    return shb.sealElement().toString();
+  }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/FindReplace.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/FindReplace.java
new file mode 100644
index 0000000..f7bc907
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/FindReplace.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.safehtml.client;
+
+import com.google.gwt.regexp.shared.RegExp;
+
+/** A Find/Replace pair used against the {@link SafeHtml} block of text. */
+public interface FindReplace {
+  /**
+   * @return regular expression to match substrings with; should be treated as
+   *     immutable.
+   */
+  public RegExp pattern();
+
+  /**
+   * Find and replace a single instance of this pattern in an input.
+   * <p>
+   * <b>WARNING:</b> No XSS sanitization is done on the return value of this
+   * method, e.g. this value may be passed directly to
+   * {@link SafeHtml#replaceAll(String, String)}. Implementations must sanitize output
+   * appropriately.
+   *
+   * @param input input string.
+   * @return result of regular expression replacement.
+   * @throws IllegalArgumentException if the input could not be safely sanitized.
+   */
+  public String replace(String input);
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java
new file mode 100644
index 0000000..e2c576b
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.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.gwtexpui.safehtml.client;
+
+import com.google.gwt.user.client.ui.SuggestOracle;
+
+import java.util.ArrayList;
+
+/**
+ * A suggestion oracle that tries to highlight the matched text.
+ * <p>
+ * Suggestions supplied by the implementation of
+ * {@link #onRequestSuggestions(Request, Callback)} are modified to wrap all
+ * occurrences of the {@link SuggestOracle.Request#getQuery()} substring in HTML
+ * <code>&lt;strong&gt;</code> tags, so they can be emphasized to the user.
+ */
+public abstract class HighlightSuggestOracle extends SuggestOracle {
+  private static String escape(String ds) {
+    return new SafeHtmlBuilder().append(ds).asString();
+  }
+
+  @Override
+  public final boolean isDisplayStringHTML() {
+    return true;
+  }
+
+  @Override
+  public final void requestSuggestions(final Request request, final Callback cb) {
+    onRequestSuggestions(request, new Callback() {
+      public void onSuggestionsReady(final Request request,
+          final Response response) {
+        final String qpat = getQueryPattern(request.getQuery());
+        final boolean html = isHTML();
+        final ArrayList<Suggestion> r = new ArrayList<Suggestion>();
+        for (final Suggestion s : response.getSuggestions()) {
+          r.add(new BoldSuggestion(qpat, s, html));
+        }
+        cb.onSuggestionsReady(request, new Response(r));
+      }
+    });
+  }
+
+  protected String getQueryPattern(final String query) {
+    return "(" + escape(query) + ")";
+  }
+
+  /**
+   * @return true if {@link SuggestOracle.Suggestion#getDisplayString()} returns
+   *         HTML; false if the text must be escaped before evaluating in an
+   *         HTML like context.
+   */
+  protected boolean isHTML() {
+    return false;
+  }
+
+  /** Compute the suggestions and return them for display. */
+  protected abstract void onRequestSuggestions(Request request, Callback done);
+
+  private static class BoldSuggestion implements Suggestion {
+    private final Suggestion suggestion;
+    private final String displayString;
+
+    BoldSuggestion(final String qstr, final Suggestion s, final boolean html) {
+      suggestion = s;
+
+      String ds = s.getDisplayString();
+      if (!html) {
+        ds = escape(ds);
+      }
+      displayString = sgi(ds, qstr, "<strong>$1</strong>");
+    }
+
+    private static native String sgi(String inString, String pat, String newHtml)
+    /*-{ return inString.replace(RegExp(pat, 'gi'), newHtml); }-*/;
+
+    public String getDisplayString() {
+      return displayString;
+    }
+
+    public String getReplacementString() {
+      return suggestion.getReplacementString();
+    }
+  }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/LinkFindReplace.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/LinkFindReplace.java
new file mode 100644
index 0000000..eaa4f23
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/LinkFindReplace.java
@@ -0,0 +1,84 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.safehtml.client;
+
+import com.google.gwt.regexp.shared.RegExp;
+
+/**
+ * A Find/Replace pair whose replacement string is a link.
+ * <p>
+ * It is safe to pass arbitrary user-provided links to this class. Links are
+ * sanitized as follows:
+ * <ul>
+ * <li>Only http(s) and mailto links are supported; any other scheme results in
+ * an {@link IllegalArgumentException} from {@link #replace(String)}.
+ * <li>Special characters in the link after regex replacement are escaped with
+ * {@link SafeHtmlBuilder}.</li>
+ * </ul>
+ */
+public class LinkFindReplace implements FindReplace {
+  public static boolean hasValidScheme(String link) {
+    int colon = link.indexOf(':');
+    if (colon < 0) {
+      return true;
+    }
+    String scheme = link.substring(0, colon);
+    return "http".equalsIgnoreCase(scheme)
+        || "https".equalsIgnoreCase(scheme)
+        || "mailto".equalsIgnoreCase(scheme);
+  }
+
+  private RegExp pat;
+  private String link;
+
+  protected LinkFindReplace() {
+  }
+
+  /**
+   * @param regex regular expression pattern to match substrings with.
+   * @param repl replacement link href. Capture groups within
+   *        <code>regex</code> can be referenced with <code>$<i>n</i></code>.
+   */
+  public LinkFindReplace(String find, String link) {
+    this.pat = RegExp.compile(find);
+    this.link = link;
+  }
+
+  @Override
+  public RegExp pattern() {
+    return pat;
+  }
+
+  @Override
+  public String replace(String input) {
+    String href = pat.replace(input, link);
+    if (!hasValidScheme(href)) {
+      throw new IllegalArgumentException(
+          "Invalid scheme (" + toString() + "): " + href);
+    }
+    String result = new SafeHtmlBuilder()
+        .openAnchor()
+        .setAttribute("href", href)
+        .append(SafeHtml.asis(input))
+        .closeAnchor()
+        .asString();
+    return result;
+  }
+
+  @Override
+  public String toString() {
+    return "find = " + pat.getSource() + ", link = " + link;
+  }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/RawFindReplace.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/RawFindReplace.java
new file mode 100644
index 0000000..d22fef6
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/RawFindReplace.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.gwtexpui.safehtml.client;
+
+import com.google.gwt.regexp.shared.RegExp;
+
+/**
+ * A Find/Replace pair whose replacement string is arbitrary HTML.
+ * <p>
+ * <b>WARNING:</b> This class is not safe used with user-provided patterns.
+ */
+public class RawFindReplace implements FindReplace {
+  private RegExp pat;
+  private String replace;
+
+  protected RawFindReplace() {
+  }
+
+  /**
+   * @param regex regular expression pattern to match substrings with.
+   * @param repl replacement expression. Capture groups within
+   *        <code>regex</code> can be referenced with <code>$<i>n</i></code>.
+   */
+  public RawFindReplace(String find, String replace) {
+    this.pat = RegExp.compile(find);
+    this.replace = replace;
+  }
+
+  @Override
+  public RegExp pattern() {
+    return pat;
+  }
+
+  @Override
+  public String replace(String input) {
+    return pat.replace(input, replace);
+  }
+
+  @Override
+  public String toString() {
+    return "find = " + pat.getSource() + ", replace = " + replace;
+  }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtml.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtml.java
new file mode 100644
index 0000000..0a9f7a2
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtml.java
@@ -0,0 +1,302 @@
+// 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.gwtexpui.safehtml.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.regexp.shared.MatchResult;
+import com.google.gwt.regexp.shared.RegExp;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.HTMLTable;
+import com.google.gwt.user.client.ui.HasHTML;
+import com.google.gwt.user.client.ui.InlineHTML;
+import com.google.gwt.user.client.ui.Widget;
+
+import java.util.Iterator;
+import java.util.List;
+
+/** Immutable string safely placed as HTML without further escaping. */
+public abstract class SafeHtml {
+  public static final SafeHtmlResources RESOURCES;
+
+  static {
+    if (GWT.isClient()) {
+      RESOURCES = GWT.create(SafeHtmlResources.class);
+      RESOURCES.css().ensureInjected();
+
+    } else {
+      RESOURCES = new SafeHtmlResources() {
+        @Override
+        public SafeHtmlCss css() {
+          return new SafeHtmlCss() {
+            public String wikiList() {
+              return "wikiList";
+            }
+
+            public String wikiPreFormat() {
+              return "wikiPreFormat";
+            }
+
+            public boolean ensureInjected() {
+              return false;
+            }
+
+            public String getName() {
+              return null;
+            }
+
+            public String getText() {
+              return null;
+            }
+          };
+        }
+      };
+    }
+  }
+
+  /** @return the existing HTML property of a widget. */
+  public static SafeHtml get(final HasHTML t) {
+    return new SafeHtmlString(t.getHTML());
+  }
+
+  /** @return the existing HTML text, wrapped in a safe buffer. */
+  public static SafeHtml asis(final String htmlText) {
+    return new SafeHtmlString(htmlText);
+  }
+
+  /** Set the HTML property of a widget. */
+  public static <T extends HasHTML> T set(final T e, final SafeHtml str) {
+    e.setHTML(str.asString());
+    return e;
+  }
+
+  /** @return the existing inner HTML of any element. */
+  public static SafeHtml get(final Element e) {
+    return new SafeHtmlString(DOM.getInnerHTML(e));
+  }
+
+  /** Set the inner HTML of any element. */
+  public static Element set(final Element e, final SafeHtml str) {
+    DOM.setInnerHTML(e, str.asString());
+    return e;
+  }
+
+  /** @return the existing inner HTML of a table cell. */
+  public static SafeHtml get(final HTMLTable t, final int row, final int col) {
+    return new SafeHtmlString(t.getHTML(row, col));
+  }
+
+  /** Set the inner HTML of a table cell. */
+  public static <T extends HTMLTable> T set(final T t, final int row,
+      final int col, final SafeHtml str) {
+    t.setHTML(row, col, str.asString());
+    return t;
+  }
+
+  /** Parse an HTML block and return the first (typically root) element. */
+  public static Element parse(final SafeHtml str) {
+    return DOM.getFirstChild(set(DOM.createDiv(), str));
+  }
+
+  /** Convert bare http:// and https:// URLs into &lt;a href&gt; tags. */
+  public SafeHtml linkify() {
+    final String part = "(?:" +
+    "[a-zA-Z0-9$_.+!*',%;:@=?#/~-]" +
+    "|&(?!lt;|gt;)" +
+    ")";
+    return replaceAll(
+        "(https?://" +
+          part + "{2,}" +
+          "(?:[(]" + part + "*" + "[)])*" +
+          part + "*" +
+        ")",
+        "<a href=\"$1\" target=\"_blank\">$1</a>");
+  }
+
+  /**
+   * Apply {@link #linkify()}, and "\n\n" to &lt;p&gt;.
+   * <p>
+   * Lines that start with whitespace are assumed to be preformatted, and are
+   * formatted by the {@link SafeHtmlCss#wikiPreFormat()} CSS class.
+   */
+  public SafeHtml wikify() {
+    final SafeHtmlBuilder r = new SafeHtmlBuilder();
+    for (final String p : linkify().asString().split("\n\n")) {
+      if (isPreFormat(p)) {
+        r.openElement("p");
+        for (final String line : p.split("\n")) {
+          r.openSpan();
+          r.setStyleName(RESOURCES.css().wikiPreFormat());
+          r.append(asis(line));
+          r.closeSpan();
+          r.br();
+        }
+        r.closeElement("p");
+
+      } else if (isList(p)) {
+        wikifyList(r, p);
+
+      } else {
+        r.openElement("p");
+        r.append(asis(p));
+        r.closeElement("p");
+      }
+    }
+    return r.toSafeHtml();
+  }
+
+  private void wikifyList(final SafeHtmlBuilder r, final String p) {
+    boolean in_ul = false;
+    boolean in_p = false;
+    for (String line : p.split("\n")) {
+      if (line.startsWith("-") || line.startsWith("*")) {
+        if (!in_ul) {
+          if (in_p) {
+            in_p = false;
+            r.closeElement("p");
+          }
+
+          in_ul = true;
+          r.openElement("ul");
+          r.setStyleName(RESOURCES.css().wikiList());
+        }
+        line = line.substring(1).trim();
+
+      } else if (!in_ul) {
+        if (!in_p) {
+          in_p = true;
+          r.openElement("p");
+        } else {
+          r.append(' ');
+        }
+        r.append(asis(line));
+        continue;
+      }
+
+      r.openElement("li");
+      r.append(asis(line));
+      r.closeElement("li");
+    }
+
+    if (in_ul) {
+      r.closeElement("ul");
+    } else if (in_p) {
+      r.closeElement("p");
+    }
+  }
+
+  private static boolean isPreFormat(final String p) {
+    return p.contains("\n ") || p.contains("\n\t") || p.startsWith(" ")
+        || p.startsWith("\t");
+  }
+
+  private static boolean isList(final String p) {
+    return p.contains("\n- ") || p.contains("\n* ") || p.startsWith("- ")
+        || p.startsWith("* ");
+  }
+
+  /**
+   * Replace first occurrence of <code>regex</code> with <code>repl</code> .
+   * <p>
+   * <b>WARNING:</b> This replacement is being performed against an otherwise
+   * safe HTML string. The caller must ensure that the replacement does not
+   * introduce cross-site scripting attack entry points.
+   *
+   * @param regex regular expression pattern to match the substring with.
+   * @param repl replacement expression. Capture groups within
+   *        <code>regex</code> can be referenced with <code>$<i>n</i></code>.
+   * @return a new string, after the replacement has been made.
+   */
+  public SafeHtml replaceFirst(final String regex, final String repl) {
+    return new SafeHtmlString(asString().replaceFirst(regex, repl));
+  }
+
+  /**
+   * Replace each occurrence of <code>regex</code> with <code>repl</code> .
+   * <p>
+   * <b>WARNING:</b> This replacement is being performed against an otherwise
+   * safe HTML string. The caller must ensure that the replacement does not
+   * introduce cross-site scripting attack entry points.
+   *
+   * @param regex regular expression pattern to match substrings with.
+   * @param repl replacement expression. Capture groups within
+   *        <code>regex</code> can be referenced with <code>$<i>n</i></code>.
+   * @return a new string, after the replacements have been made.
+   */
+  public SafeHtml replaceAll(final String regex, final String repl) {
+    return new SafeHtmlString(asString().replaceAll(regex, repl));
+  }
+
+  /**
+   * Replace all find/replace pairs in the list in a single pass.
+   *
+   * @param findReplaceList find/replace pairs to use.
+   * @return a new string, after the replacements have been made.
+   */
+  public <T> SafeHtml replaceAll(List<? extends FindReplace> findReplaceList) {
+    if (findReplaceList == null || findReplaceList.isEmpty()) {
+      return this;
+    }
+
+    StringBuilder pat = new StringBuilder();
+    Iterator<? extends FindReplace> it = findReplaceList.iterator();
+    while (it.hasNext()) {
+      FindReplace fr = it.next();
+      pat.append(fr.pattern().getSource());
+      if (it.hasNext()) {
+        pat.append('|');
+      }
+    }
+
+    StringBuilder result = new StringBuilder();
+    RegExp re = RegExp.compile(pat.toString(), "g");
+    String orig = asString();
+    int index = 0;
+    MatchResult mat;
+    while ((mat = re.exec(orig)) != null) {
+      String g = mat.getGroup(0);
+      // Re-run each candidate to find which one matched.
+      for (FindReplace fr : findReplaceList) {
+        if (fr.pattern().test(g)) {
+          try {
+            String repl = fr.replace(g);
+            result.append(orig.substring(index, mat.getIndex()));
+            result.append(repl);
+          } catch (IllegalArgumentException e) {
+            continue;
+          }
+          index = mat.getIndex() + g.length();
+          break;
+        }
+      }
+    }
+    result.append(orig.substring(index, orig.length()));
+    return asis(result.toString());
+  }
+
+  /** @return a GWT block display widget displaying this HTML. */
+  public Widget toBlockWidget() {
+    return new HTML(asString());
+  }
+
+  /** @return a GWT inline display widget displaying this HTML. */
+  public Widget toInlineWidget() {
+    return new InlineHTML(asString());
+  }
+
+  /** @return a clean HTML string safe for inclusion in any context. */
+  public abstract String asString();
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java
new file mode 100644
index 0000000..9fe3267
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java
@@ -0,0 +1,411 @@
+// 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.gwtexpui.safehtml.client;
+
+import com.google.gwt.core.client.GWT;
+
+/**
+ * Safely constructs a {@link SafeHtml}, escaping user provided content.
+ */
+public class SafeHtmlBuilder extends SafeHtml {
+  private static final Impl impl;
+
+  static {
+    if (GWT.isClient()) {
+      impl = new ClientImpl();
+    } else {
+      impl = new ServerImpl();
+    }
+  }
+
+  private final BufferDirect dBuf;
+  private Buffer cb;
+
+  private BufferSealElement sBuf;
+  private AttMap att;
+
+  public SafeHtmlBuilder() {
+    cb = dBuf = new BufferDirect();
+  }
+
+  /** @return true if this builder has not had an append occur yet. */
+  public boolean isEmpty() {
+    return dBuf.isEmpty();
+  }
+
+  /** @return true if this builder has content appended into it. */
+  public boolean hasContent() {
+    return !isEmpty();
+  }
+
+  public SafeHtmlBuilder append(final boolean in) {
+    cb.append(in);
+    return this;
+  }
+
+  public SafeHtmlBuilder append(final char in) {
+    switch (in) {
+      case '&':
+        cb.append("&amp;");
+        break;
+
+      case '>':
+        cb.append("&gt;");
+        break;
+
+      case '<':
+        cb.append("&lt;");
+        break;
+
+      case '"':
+        cb.append("&quot;");
+        break;
+
+      case '\'':
+        cb.append("&#39;");
+        break;
+
+      default:
+        cb.append(in);
+        break;
+    }
+    return this;
+  }
+
+  public SafeHtmlBuilder append(final int in) {
+    cb.append(in);
+    return this;
+  }
+
+  public SafeHtmlBuilder append(final long in) {
+    cb.append(in);
+    return this;
+  }
+
+  public SafeHtmlBuilder append(final float in) {
+    cb.append(in);
+    return this;
+  }
+
+  public SafeHtmlBuilder append(final double in) {
+    cb.append(in);
+    return this;
+  }
+
+  /** Append already safe HTML as-is, avoiding double escaping. */
+  public SafeHtmlBuilder append(final SafeHtml in) {
+    if (in != null) {
+      cb.append(in.asString());
+    }
+    return this;
+  }
+
+  /** Append the string, escaping unsafe characters. */
+  public SafeHtmlBuilder append(final String in) {
+    if (in != null) {
+      impl.escapeStr(this, in);
+    }
+    return this;
+  }
+
+  /** Append the string, escaping unsafe characters. */
+  public SafeHtmlBuilder append(final StringBuilder in) {
+    if (in != null) {
+      append(in.toString());
+    }
+    return this;
+  }
+
+  /** Append the string, escaping unsafe characters. */
+  public SafeHtmlBuilder append(final StringBuffer in) {
+    if (in != null) {
+      append(in.toString());
+    }
+    return this;
+  }
+
+  /** Append the result of toString(), escaping unsafe characters. */
+  public SafeHtmlBuilder append(final Object in) {
+    if (in != null) {
+      append(in.toString());
+    }
+    return this;
+  }
+
+  /** Append the string, escaping unsafe characters. */
+  public SafeHtmlBuilder append(final CharSequence in) {
+    if (in != null) {
+      escapeCS(this, in);
+    }
+    return this;
+  }
+
+  /**
+   * Open an element, appending "<tagName>" to the buffer.
+   * <p>
+   * After the element is open the attributes may be manipulated until the next
+   * <code>append</code>, <code>openElement</code>, <code>closeSelf</code> or
+   * <code>closeElement</code> call.
+   *
+   * @param tagName name of the HTML element to open.
+   */
+  public SafeHtmlBuilder openElement(final String tagName) {
+    assert isElementName(tagName);
+    cb.append("<");
+    cb.append(tagName);
+    if (sBuf == null) {
+      att = new AttMap();
+      sBuf = new BufferSealElement(this);
+    }
+    att.reset(tagName);
+    cb = sBuf;
+    return this;
+  }
+
+  /**
+   * Get an attribute of the last opened element.
+   *
+   * @param name name of the attribute to read.
+   * @return the attribute value, as a string. The empty string if the attribute
+   *         has not been assigned a value. The returned string is the raw
+   *         (unescaped) value.
+   */
+  public String getAttribute(final String name) {
+    assert isAttributeName(name);
+    assert cb == sBuf;
+    return att.get(name);
+  }
+
+  /**
+   * Set an attribute of the last opened element.
+   *
+   * @param name name of the attribute to set.
+   * @param value value to assign; any existing value is replaced. The value is
+   *        escaped (if necessary) during the assignment.
+   */
+  public SafeHtmlBuilder setAttribute(final String name, final String value) {
+    assert isAttributeName(name);
+    assert cb == sBuf;
+    att.set(name, value != null ? value : "");
+    return this;
+  }
+
+  /**
+   * Set an attribute of the last opened element.
+   *
+   * @param name name of the attribute to set.
+   * @param value value to assign, any existing value is replaced.
+   */
+  public SafeHtmlBuilder setAttribute(final String name, final int value) {
+    return setAttribute(name, String.valueOf(value));
+  }
+
+  /**
+   * Append a new value into a whitespace delimited attribute.
+   * <p>
+   * If the attribute is not yet assigned, this method sets the attribute. If
+   * the attribute is already assigned, the new value is appended onto the end,
+   * after appending a single space to delimit the values.
+   *
+   * @param name name of the attribute to append onto.
+   * @param value additional value to append.
+   */
+  public SafeHtmlBuilder appendAttribute(final String name, String value) {
+    if (value != null && value.length() > 0) {
+      final String e = getAttribute(name);
+      return setAttribute(name, e.length() > 0 ? e + " " + value : value);
+    }
+    return this;
+  }
+
+  /** Set the height attribute of the current element. */
+  public SafeHtmlBuilder setHeight(final int height) {
+    return setAttribute("height", height);
+  }
+
+  /** Set the width attribute of the current element. */
+  public SafeHtmlBuilder setWidth(final int width) {
+    return setAttribute("width", width);
+  }
+
+  /** Set the CSS class name for this element. */
+  public SafeHtmlBuilder setStyleName(final String style) {
+    assert isCssName(style);
+    return setAttribute("class", style);
+  }
+
+  /**
+   * Add an additional CSS class name to this element.
+   *<p>
+   * If no CSS class name has been specified yet, this method initializes it to
+   * the single name.
+   */
+  public SafeHtmlBuilder addStyleName(final String style) {
+    assert isCssName(style);
+    return appendAttribute("class", style);
+  }
+
+  private void sealElement0() {
+    assert cb == sBuf;
+    cb = dBuf;
+    att.onto(cb, this);
+  }
+
+  Buffer sealElement() {
+    sealElement0();
+    cb.append(">");
+    return cb;
+  }
+
+  /** Close the current element with a self closing suffix ("/ &gt;"). */
+  public SafeHtmlBuilder closeSelf() {
+    sealElement0();
+    cb.append(" />");
+    return this;
+  }
+
+  /** Append a closing tag for the named element. */
+  public SafeHtmlBuilder closeElement(final String name) {
+    assert isElementName(name);
+    cb.append("</");
+    cb.append(name);
+    cb.append(">");
+    return this;
+  }
+
+  /** Append "&amp;nbsp;" - a non-breaking space, useful in empty table cells. */
+  public SafeHtmlBuilder nbsp() {
+    cb.append("&nbsp;");
+    return this;
+  }
+
+  /** Append "&lt;br /&gt;" - a line break with no attributes */
+  public SafeHtmlBuilder br() {
+    cb.append("<br />");
+    return this;
+  }
+
+  /** Append "&lt;tr&gt;"; attributes may be set if needed */
+  public SafeHtmlBuilder openTr() {
+    return openElement("tr");
+  }
+
+  /** Append "&lt;/tr&gt;" */
+  public SafeHtmlBuilder closeTr() {
+    return closeElement("tr");
+  }
+
+  /** Append "&lt;td&gt;"; attributes may be set if needed */
+  public SafeHtmlBuilder openTd() {
+    return openElement("td");
+  }
+
+  /** Append "&lt;/td&gt;" */
+  public SafeHtmlBuilder closeTd() {
+    return closeElement("td");
+  }
+
+  /** Append "&lt;div&gt;"; attributes may be set if needed */
+  public SafeHtmlBuilder openDiv() {
+    return openElement("div");
+  }
+
+  /** Append "&lt;/div&gt;" */
+  public SafeHtmlBuilder closeDiv() {
+    return closeElement("div");
+  }
+
+  /** Append "&lt;span&gt;"; attributes may be set if needed */
+  public SafeHtmlBuilder openSpan() {
+    return openElement("span");
+  }
+
+  /** Append "&lt;/span&gt;" */
+  public SafeHtmlBuilder closeSpan() {
+    return closeElement("span");
+  }
+
+  /** Append "&lt;a&gt;"; attributes may be set if needed */
+  public SafeHtmlBuilder openAnchor() {
+    return openElement("a");
+  }
+
+  /** Append "&lt;/a&gt;" */
+  public SafeHtmlBuilder closeAnchor() {
+    return closeElement("a");
+  }
+
+  /** Append "&lt;param name=... value=... /&gt;". */
+  public SafeHtmlBuilder paramElement(final String name, final String value) {
+    openElement("param");
+    setAttribute("name", name);
+    setAttribute("value", value);
+    return closeSelf();
+  }
+
+  /** @return an immutable {@link SafeHtml} representation of the buffer. */
+  public SafeHtml toSafeHtml() {
+    return new SafeHtmlString(asString());
+  }
+
+  @Override
+  public String asString() {
+    return cb.toString();
+  }
+
+  private static void escapeCS(final SafeHtmlBuilder b, final CharSequence in) {
+    for (int i = 0; i < in.length(); i++) {
+      b.append(in.charAt(i));
+    }
+  }
+
+  private static boolean isElementName(final String name) {
+    return name.matches("^[a-zA-Z][a-zA-Z0-9_-]*$");
+  }
+
+  private static boolean isAttributeName(final String name) {
+    return isElementName(name);
+  }
+
+  private static boolean isCssName(final String name) {
+    return isElementName(name);
+  }
+
+  private static abstract class Impl {
+    abstract void escapeStr(SafeHtmlBuilder b, String in);
+  }
+
+  private static class ServerImpl extends Impl {
+    @Override
+    void escapeStr(final SafeHtmlBuilder b, final String in) {
+      SafeHtmlBuilder.escapeCS(b, in);
+    }
+  }
+
+  private static class ClientImpl extends Impl {
+    @Override
+    void escapeStr(final SafeHtmlBuilder b, final String in) {
+      b.cb.append(escape(in));
+    }
+
+    private static native String escape(String src)
+    /*-{ return src.replace(/&/g,'&amp;')
+                   .replace(/>/g,'&gt;')
+                   .replace(/</g,'&lt;')
+                   .replace(/"/g,'&quot;')
+                   .replace(/'/g,'&#39;');
+     }-*/;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/QueryStats.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlCss.java
similarity index 68%
copy from gerrit-server/src/main/java/com/google/gerrit/server/events/QueryStats.java
copy to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlCss.java
index ecf2b9a..f6836a0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/QueryStats.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlCss.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 The Android Open Source Project
+// 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.
@@ -12,11 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.events;
+package com.google.gwtexpui.safehtml.client;
 
-public class QueryStats {
-  public final String type = "stats";
-  public int rowCount;
-  public long runTimeMilliseconds;
-  public String resumeSortKey;
+import com.google.gwt.resources.client.CssResource;
+
+public interface SafeHtmlCss extends CssResource {
+  String wikiPreFormat();
+  String wikiList();
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/QueryStats.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlResources.java
similarity index 67%
copy from gerrit-server/src/main/java/com/google/gerrit/server/events/QueryStats.java
copy to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlResources.java
index ecf2b9a..e3f5724 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/QueryStats.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlResources.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 The Android Open Source Project
+// 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.
@@ -12,11 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.events;
+package com.google.gwtexpui.safehtml.client;
 
-public class QueryStats {
-  public final String type = "stats";
-  public int rowCount;
-  public long runTimeMilliseconds;
-  public String resumeSortKey;
+import com.google.gwt.resources.client.ClientBundle;
+
+public interface SafeHtmlResources extends ClientBundle {
+  @Source("safehtml.css")
+  SafeHtmlCss css();
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/QueryStats.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlString.java
similarity index 65%
copy from gerrit-server/src/main/java/com/google/gerrit/server/events/QueryStats.java
copy to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlString.java
index ecf2b9a..a229421 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/QueryStats.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlString.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 The Android Open Source Project
+// 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.
@@ -12,11 +12,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.events;
+package com.google.gwtexpui.safehtml.client;
 
-public class QueryStats {
-  public final String type = "stats";
-  public int rowCount;
-  public long runTimeMilliseconds;
-  public String resumeSortKey;
+class SafeHtmlString extends SafeHtml {
+  private final String html;
+
+  SafeHtmlString(final String h) {
+    html = h;
+  }
+
+  @Override
+  public String asString() {
+    return html;
+  }
 }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/safehtml.css b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/safehtml.css
new file mode 100644
index 0000000..fcad92c
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/safehtml.css
@@ -0,0 +1,23 @@
+/* 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.
+ */
+
+.wikiPreFormat {
+  white-space: pre;
+  font-family: 'Lucida Console', 'Lucida Sans Typewriter', Monaco, monospace;
+  font-size: small;
+}
+
+.wikiList {
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheControlFilter.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheControlFilter.java
new file mode 100644
index 0000000..c4d681f
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheControlFilter.java
@@ -0,0 +1,106 @@
+// 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.gwtexpui.server;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Forces GWT resources to cache for a very long time.
+ * <p>
+ * GWT compiled JavaScript and ImageBundles can be cached indefinitely by a
+ * browser and/or an edge proxy, as they never contain user-specific data and
+ * are named by a unique checksum. If their content is ever modified then the
+ * URL changes, so user agents would request a different resource. We force
+ * these resources to have very long expiration times.
+ * <p>
+ * To use, add the following block to your <code>web.xml</code>:
+ *
+ * <pre>
+ * &lt;filter&gt;
+ *     &lt;filter-name&gt;CacheControl&lt;/filter-name&gt;
+ *     &lt;filter-class&gt;com.google.gwtexpui.server.CacheControlFilter&lt;/filter-class&gt;
+ *   &lt;/filter&gt;
+ *   &lt;filter-mapping&gt;
+ *     &lt;filter-name&gt;CacheControl&lt;/filter-name&gt;
+ *     &lt;url-pattern&gt;/*&lt;/url-pattern&gt;
+ *   &lt;/filter-mapping&gt;
+ * </pre>
+ */
+public class CacheControlFilter implements Filter {
+  public void init(final FilterConfig config) {
+  }
+
+  public void destroy() {
+  }
+
+  public void doFilter(final ServletRequest sreq, final ServletResponse srsp,
+      final FilterChain chain) throws IOException, ServletException {
+    final HttpServletRequest req = (HttpServletRequest) sreq;
+    final HttpServletResponse rsp = (HttpServletResponse) srsp;
+    final String pathInfo = pathInfo(req);
+
+    if (cacheForever(pathInfo, req)) {
+      CacheHeaders.setCacheable(req, rsp, 365, TimeUnit.DAYS);
+    } else if (nocache(pathInfo)) {
+      CacheHeaders.setNotCacheable(rsp);
+    }
+
+    chain.doFilter(req, rsp);
+  }
+
+  private static boolean cacheForever(final String pathInfo,
+      final HttpServletRequest req) {
+    if (pathInfo.endsWith(".cache.html")) {
+      return true;
+    } else if (pathInfo.endsWith(".cache.gif")) {
+      return true;
+    } else if (pathInfo.endsWith(".cache.png")) {
+      return true;
+    } else if (pathInfo.endsWith(".cache.css")) {
+      return true;
+    } else if (pathInfo.endsWith(".cache.jar")) {
+      return true;
+    } else if (pathInfo.endsWith(".cache.swf")) {
+      return true;
+    } else if (pathInfo.endsWith(".nocache.js")) {
+      final String v = req.getParameter("content");
+      return v != null && v.length() > 20;
+    }
+    return false;
+  }
+
+  private static boolean nocache(final String pathInfo) {
+    if (pathInfo.endsWith(".nocache.js")) {
+      return true;
+    }
+    return false;
+  }
+
+  private static String pathInfo(final HttpServletRequest req) {
+    final String uri = req.getRequestURI();
+    final String ctx = req.getContextPath();
+    return uri.startsWith(ctx) ? uri.substring(ctx.length()) : uri;
+  }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheHeaders.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheHeaders.java
new file mode 100644
index 0000000..11409e8
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheHeaders.java
@@ -0,0 +1,118 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.server;
+
+import static java.util.concurrent.TimeUnit.DAYS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import java.util.concurrent.TimeUnit;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/** Utilities to manage HTTP caching directives in responses. */
+public class CacheHeaders {
+  private static final long MAX_CACHE_DURATION = DAYS.toSeconds(365);
+
+  /**
+   * Do not cache the response, anywhere.
+   *
+   * @param res response being returned.
+   */
+  public static void setNotCacheable(HttpServletResponse res) {
+    String cc = "no-cache, no-store, max-age=0, must-revalidate";
+    res.setHeader("Cache-Control", cc);
+    res.setHeader("Pragma", "no-cache");
+    res.setHeader("Expires", "Fri, 01 Jan 1990 00:00:00 GMT");
+    res.setDateHeader("Date", System.currentTimeMillis());
+  }
+
+  /**
+   * Permit caching the response for up to the age specified.
+   * <p>
+   * If the request is on a secure connection (e.g. SSL) private caching is
+   * used. This allows the user-agent to cache the response, but requests
+   * intermediate proxies to not cache. This may offer better protection for
+   * Set-Cookie headers.
+   * <p>
+   * If the request is on plaintext (insecure), public caching is used. This may
+   * allow an intermediate proxy to cache the response, including any Set-Cookie
+   * header that may have also been included.
+   *
+   * @param req current request.
+   * @param res response being returned.
+   * @param age how long the response can be cached.
+   * @param unit time unit for age, usually {@link TimeUnit#SECONDS}.
+   */
+  public static void setCacheable(
+      HttpServletRequest req, HttpServletResponse res,
+      long age, TimeUnit unit) {
+    if (req.isSecure()) {
+      setCacheablePrivate(res, age, unit);
+    } else {
+      setCacheablePublic(res, age, unit);
+    }
+  }
+
+  /**
+   * Allow the response to be cached by proxies and user-agents.
+   * <p>
+   * If the response includes a Set-Cookie header the cookie may be cached by a
+   * proxy and returned to multiple browsers behind the same proxy. This is
+   * insecure for authenticated connections.
+   *
+   * @param res response being returned.
+   * @param age how long the response can be cached.
+   * @param unit time unit for age, usually {@link TimeUnit#SECONDS}.
+   */
+  public static void setCacheablePublic(HttpServletResponse res,
+      long age, TimeUnit unit) {
+    long now = System.currentTimeMillis();
+    long sec = maxAgeSeconds(age, unit);
+
+    res.setDateHeader("Expires", now + SECONDS.toMillis(sec));
+    res.setDateHeader("Date", now);
+    cache(res, "public", age, unit);
+  }
+
+  /**
+   * Allow the response to be cached only by the user-agent.
+   *
+   * @param res response being returned.
+   * @param age how long the response can be cached.
+   * @param unit time unit for age, usually {@link TimeUnit#SECONDS}.
+   */
+  public static void setCacheablePrivate(HttpServletResponse res,
+      long age, TimeUnit unit) {
+    long now = System.currentTimeMillis();
+    res.setDateHeader("Expires", now);
+    res.setDateHeader("Date", now);
+    cache(res, "private", age, unit);
+  }
+
+  private static void cache(HttpServletResponse res,
+      String type, long age, TimeUnit unit) {
+    res.setHeader("Cache-Control", String.format(
+        "%s, max-age=%d",
+        type, maxAgeSeconds(age, unit)));
+  }
+
+  private static long maxAgeSeconds(long age, TimeUnit unit) {
+    return Math.min(unit.toSeconds(age), MAX_CACHE_DURATION);
+  }
+
+  private CacheHeaders() {
+  }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/User.gwt.xml b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/User.gwt.xml
new file mode 100644
index 0000000..c681d89
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/User.gwt.xml
@@ -0,0 +1,27 @@
+<!--
+ 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.
+-->
+<module>
+  <inherits name="com.google.gwt.user.User"/>
+
+  <replace-with class="com.google.gwtexpui.user.client.PluginSafeDialogBoxImplAutoHide">
+    <when-type-is class="com.google.gwtexpui.user.client.PluginSafeDialogBoxImpl" />
+    <any>
+      <when-property-is name="user.agent" value="safari"/>
+      <when-property-is name="user.agent" value="gecko"/>
+      <when-property-is name="user.agent" value="gecko1_8"/>
+    </any>
+  </replace-with>
+</module>
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/AutoCenterDialogBox.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/AutoCenterDialogBox.java
new file mode 100644
index 0000000..78ea8d6
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/AutoCenterDialogBox.java
@@ -0,0 +1,76 @@
+// 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.gwtexpui.user.client;
+
+import com.google.gwt.event.logical.shared.ResizeEvent;
+import com.google.gwt.event.logical.shared.ResizeHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.Window;
+
+/** A DialogBox that automatically re-centers itself if the window changes */
+public class AutoCenterDialogBox extends PluginSafeDialogBox {
+  private HandlerRegistration recenter;
+
+  public AutoCenterDialogBox() {
+    this(false);
+  }
+
+  public AutoCenterDialogBox(final boolean autoHide) {
+    this(autoHide, true);
+  }
+
+  public AutoCenterDialogBox(final boolean autoHide, final boolean modal) {
+    super(autoHide, modal);
+  }
+
+  @Override
+  public void show() {
+    if (recenter == null) {
+      recenter = Window.addResizeHandler(new ResizeHandler() {
+        @Override
+        public void onResize(final ResizeEvent event) {
+          final int w = event.getWidth();
+          final int h = event.getHeight();
+          AutoCenterDialogBox.this.onResize(w, h);
+        }
+      });
+    }
+    super.show();
+  }
+
+  @Override
+  protected void onUnload() {
+    if (recenter != null) {
+      recenter.removeHandler();
+      recenter = null;
+    }
+    super.onUnload();
+  }
+
+  /**
+   * Invoked when the outer browser window resizes.
+   * <p>
+   * Subclasses may override (but should ensure they still call super.onResize)
+   * to implement custom logic when a window resize occurs.
+   *
+   * @param width new browser window width
+   * @param height new browser window height
+   */
+  protected void onResize(final int width, final int height) {
+    if (isAttached()) {
+      center();
+    }
+  }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBox.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBox.java
new file mode 100644
index 0000000..c6ab09a
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBox.java
@@ -0,0 +1,65 @@
+// 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.gwtexpui.user.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.user.client.ui.DialogBox;
+
+/**
+ * A DialogBox that can appear over Flash movies and Java applets.
+ * <p>
+ * Some browsers have issues with placing a &lt;div&gt; (such as that used by
+ * the DialogBox implementation) over top of native UI such as that used by the
+ * Flash plugin. Often the native UI leaks over top of the &lt;div&gt;, which is
+ * not the desired behavior for a dialog box.
+ * <p>
+ * This implementation hides the native resources by setting their display
+ * property to 'none' when the dialog is shown, and restores them back to their
+ * prior setting when the dialog is hidden.
+ * */
+public class PluginSafeDialogBox extends DialogBox {
+  private final PluginSafeDialogBoxImpl impl =
+      GWT.create(PluginSafeDialogBoxImpl.class);
+
+  public PluginSafeDialogBox() {
+    this(false);
+  }
+
+  public PluginSafeDialogBox(final boolean autoHide) {
+    this(autoHide, true);
+  }
+
+  public PluginSafeDialogBox(final boolean autoHide, final boolean modal) {
+    super(autoHide, modal);
+  }
+
+  @Override
+  public void setVisible(final boolean show) {
+    impl.visible(show);
+    super.setVisible(show);
+  }
+
+  @Override
+  public void show() {
+    impl.visible(true);
+    super.show();
+  }
+
+  @Override
+  public void hide(final boolean autoClosed) {
+    impl.visible(false);
+    super.hide(autoClosed);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/TrackingIdAttribute.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBoxImpl.java
similarity index 75%
copy from gerrit-server/src/main/java/com/google/gerrit/server/events/TrackingIdAttribute.java
copy to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBoxImpl.java
index 7d55dd2..a32fc99 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/TrackingIdAttribute.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBoxImpl.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 The Android Open Source Project
+// 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.
@@ -12,9 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.events;
+package com.google.gwtexpui.user.client;
 
-public class TrackingIdAttribute {
-  public String system;
-  public String id;
+class PluginSafeDialogBoxImpl {
+  void visible(final boolean dialogVisible) {
+  }
 }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBoxImplAutoHide.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBoxImplAutoHide.java
new file mode 100644
index 0000000..e32fe78
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBoxImplAutoHide.java
@@ -0,0 +1,86 @@
+// 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.gwtexpui.user.client;
+
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.NodeList;
+import com.google.gwt.user.client.ui.UIObject;
+
+import java.util.ArrayList;
+
+class PluginSafeDialogBoxImplAutoHide extends PluginSafeDialogBoxImpl {
+  private boolean hidden;
+  private ArrayList<HiddenElement> hiddenElements =
+      new ArrayList<HiddenElement>();
+
+  @Override
+  void visible(final boolean dialogVisible) {
+    if (dialogVisible) {
+      hideAll();
+    } else {
+      showAll();
+    }
+  }
+
+  private void hideAll() {
+    if (!hidden) {
+      hideSet(Document.get().getElementsByTagName("object"));
+      hideSet(Document.get().getElementsByTagName("embed"));
+      hideSet(Document.get().getElementsByTagName("applet"));
+      hidden = true;
+    }
+  }
+
+  private void hideSet(final NodeList<Element> all) {
+    for (int i = 0; i < all.getLength(); i++) {
+      final Element e = all.getItem(i);
+      if (UIObject.isVisible(e)) {
+        hiddenElements.add(new HiddenElement(e));
+      }
+    }
+  }
+
+  private void showAll() {
+    if (hidden) {
+      for (final HiddenElement e : hiddenElements) {
+        e.restore();
+      }
+      hiddenElements.clear();
+      hidden = false;
+    }
+  }
+
+  private static class HiddenElement {
+    private final Element element;
+    private final String visibility;
+
+    HiddenElement(final Element element) {
+      this.element = element;
+      this.visibility = getVisibility(element);
+      setVisibility(element, "hidden");
+    }
+
+    void restore() {
+      setVisibility(element, visibility);
+    }
+
+    private static native String getVisibility(Element elem)
+    /*-{ return elem.style.visibility; }-*/;
+
+    private static native void setVisibility(Element elem, String disp)
+    /*-{ elem.style.visibility = disp; }-*/;
+  }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafePopupPanel.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafePopupPanel.java
new file mode 100644
index 0000000..7d9c9fc
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafePopupPanel.java
@@ -0,0 +1,65 @@
+// 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.gwtexpui.user.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.user.client.ui.PopupPanel;
+
+/**
+ * A PopupPanel that can appear over Flash movies and Java applets.
+ * <p>
+ * Some browsers have issues with placing a &lt;div&gt; (such as that used by
+ * the PopupPanel implementation) over top of native UI such as that used by the
+ * Flash plugin. Often the native UI leaks over top of the &lt;div&gt;, which is
+ * not the desired behavior for a dialog box.
+ * <p>
+ * This implementation hides the native resources by setting their display
+ * property to 'none' when the dialog is shown, and restores them back to their
+ * prior setting when the dialog is hidden.
+ * */
+public class PluginSafePopupPanel extends PopupPanel {
+  private final PluginSafeDialogBoxImpl impl =
+      GWT.create(PluginSafeDialogBoxImpl.class);
+
+  public PluginSafePopupPanel() {
+    this(false);
+  }
+
+  public PluginSafePopupPanel(final boolean autoHide) {
+    this(autoHide, true);
+  }
+
+  public PluginSafePopupPanel(final boolean autoHide, final boolean modal) {
+    super(autoHide, modal);
+  }
+
+  @Override
+  public void setVisible(final boolean show) {
+    impl.visible(show);
+    super.setVisible(show);
+  }
+
+  @Override
+  public void show() {
+    impl.visible(true);
+    super.show();
+  }
+
+  @Override
+  public void hide(final boolean autoClosed) {
+    impl.visible(false);
+    super.hide(autoClosed);
+  }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/UserAgent.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/UserAgent.java
new file mode 100644
index 0000000..02ba9ae
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/UserAgent.java
@@ -0,0 +1,81 @@
+// 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.gwtexpui.user.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.user.client.Window;
+
+/**
+ * User agent feature tests we don't create permutations for.
+ * <p>
+ * Some features aren't worth creating full permutations in GWT for, as each new
+ * boolean permutation (only two settings) doubles the compile time required. If
+ * the setting only affects a couple of lines of JavaScript code, the slightly
+ * larger cache files for user agents that lack the functionality requested is
+ * trivial compared to the time developers lose building their application.
+ */
+public class UserAgent {
+  /** Does the browser have ShockwaveFlash plugin enabled? */
+  public static final boolean hasFlash = hasFlash();
+
+  private static native boolean hasFlash()
+  /*-{
+    if (navigator.plugins && navigator.plugins.length) {
+      if (navigator.plugins['Shockwave Flash'])     return true;
+      if (navigator.plugins['Shockwave Flash 2.0']) return true;
+
+    } else if (navigator.mimeTypes && navigator.mimeTypes.length) {
+      var mimeType = navigator.mimeTypes['application/x-shockwave-flash'];
+      if (mimeType && mimeType.enabledPlugin) return true;
+
+    } else {
+      try { new ActiveXObject('ShockwaveFlash.ShockwaveFlash.7'); return true; } catch (e) {}
+      try { new ActiveXObject('ShockwaveFlash.ShockwaveFlash.6'); return true; } catch (e) {}
+      try { new ActiveXObject('ShockwaveFlash.ShockwaveFlash');   return true; } catch (e) {}
+    }
+    return false;
+  }-*/;
+
+  /**
+   * Test for and disallow running this application in an &lt;iframe&gt;.
+   * <p>
+   * If the application is running within an iframe this method requests a
+   * browser generated redirect to pop the application out of the iframe into
+   * the top level window, and then aborts execution by throwing an exception.
+   * This is call should be placed early within the module's onLoad() method,
+   * before any real UI can be initialized that an attacking site could try to
+   * snip out and present in a confusing context.
+   * <p>
+   * If the break out works, execution will restart automatically in a proper
+   * top level window, where the script has full control over the display. If
+   * the break out fails, execution will abort and stop immediately, preventing
+   * UI widgets from being created, leaving the user with an empty frame.
+   */
+  public static void assertNotInIFrame() {
+    if (GWT.isScript() && amInsideIFrame()) {
+      bustOutOfIFrame(Window.Location.getHref());
+      throw new RuntimeException();
+    }
+  }
+
+  private static native boolean amInsideIFrame()
+  /*-{ return top.location != $wnd.location; }-*/;
+
+  private static native void bustOutOfIFrame(String newloc)
+  /*-{ top.location.href = newloc }-*/;
+
+  private UserAgent() {
+  }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/View.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/View.java
new file mode 100644
index 0000000..35ecb12
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/View.java
@@ -0,0 +1,57 @@
+// 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.gwtexpui.user.client;
+
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.Widget;
+
+/**
+ * Widget to display within a {@link ViewSite}.
+ *<p>
+ * Implementations must override <code>protected void onLoad()</code> and
+ * arrange for {@link #display()} to be invoked once the DOM within the view is
+ * consistent for presentation to the user. Typically this means that the
+ * subclass can start RPCs within <code>onLoad()</code> and then invoke
+ * <code>display()</code> from within the AsyncCallback's
+ * <code>onSuccess(Object)</code> method.
+ */
+public abstract class View extends Composite {
+  ViewSite<? extends View> site;
+
+  @Override
+  protected void onUnload() {
+    site = null;
+    super.onUnload();
+  }
+
+  /** true if this is the current view of its parent view site */
+  public final boolean isCurrentView() {
+    Widget p = getParent();
+    while (p != null) {
+      if (p instanceof ViewSite<?>) {
+        return ((ViewSite<?>) p).getView() == this;
+      }
+      p = p.getParent();
+    }
+    return false;
+  }
+
+  /** Replace the current view in the parent ViewSite with this view. */
+  public final void display() {
+    if (site != null) {
+      site.swap(this);
+    }
+  }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/ViewSite.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/ViewSite.java
new file mode 100644
index 0000000..30b8408f
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/ViewSite.java
@@ -0,0 +1,87 @@
+// 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.gwtexpui.user.client;
+
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.SimplePanel;
+
+/**
+ * Hosts a single {@link View}.
+ * <p>
+ * View instances are attached inside of an invisible DOM node, permitting their
+ * <code>onLoad()</code> method to be invoked and to update the DOM prior to the
+ * elements being made visible in the UI.
+ * <p>
+ * Complaint View instances must invoke {@link View#display()} once the DOM is
+ * ready for presentation.
+ */
+public class ViewSite<V extends View> extends Composite {
+  private final FlowPanel main;
+  private SimplePanel current;
+  private SimplePanel next;
+
+  public ViewSite() {
+    main = new FlowPanel();
+    initWidget(main);
+  }
+
+  /** Get the current view; null if there is no view being displayed. */
+  @SuppressWarnings("unchecked")
+  public V getView() {
+    return current != null ? (V) current.getWidget() : null;
+  }
+
+  /**
+   * Set the next view to display.
+   * <p>
+   * The view will be attached to the DOM tree within a hidden container,
+   * permitting its <code>onLoad()</code> method to execute and update the DOM
+   * without the user seeing the result.
+   *
+   * @param view the next view to display.
+   */
+  public void setView(final V view) {
+    if (next != null) {
+      main.remove(next);
+    }
+    view.site = this;
+    next = new SimplePanel();
+    next.setVisible(false);
+    main.add(next);
+    next.add(view);
+  }
+
+  /**
+   * Invoked after the view becomes the current view and has been made visible.
+   *
+   * @param view the view being displayed.
+   */
+  protected void onShowView(final V view) {
+  }
+
+  @SuppressWarnings("unchecked")
+  final void swap(final View v) {
+    if (next != null && next.getWidget() == v) {
+      if (current != null) {
+        main.remove(current);
+      }
+      current = next;
+      next = null;
+      current.setVisible(true);
+      onShowView((V) v);
+    }
+  }
+}
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/LinkFindReplaceTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/LinkFindReplaceTest.java
new file mode 100644
index 0000000..97f816f
--- /dev/null
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/LinkFindReplaceTest.java
@@ -0,0 +1,77 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.safehtml.client;
+
+import static com.google.gwtexpui.safehtml.client.LinkFindReplace.hasValidScheme;
+
+import junit.framework.TestCase;
+
+public class LinkFindReplaceTest extends TestCase {
+  public void testNoEscaping() {
+    String find = "find";
+    String link = "link";
+    LinkFindReplace a = new LinkFindReplace(find, link);
+    assertEquals(find, a.pattern().getSource());
+    assertEquals("<a href=\"link\">find</a>", a.replace(find));
+    assertEquals("find = " + find + ", link = " + link, a.toString());
+  }
+
+  public void testBackreference() {
+    assertEquals("<a href=\"/bug?id=123\">issue 123</a>",
+        new LinkFindReplace("(bug|issue)\\s*([0-9]+)", "/bug?id=$2")
+            .replace("issue 123"));
+  }
+
+  public void testHasValidScheme() {
+    assertTrue(hasValidScheme("/absolute/path"));
+    assertTrue(hasValidScheme("relative/path"));
+    assertTrue(hasValidScheme("http://url/"));
+    assertTrue(hasValidScheme("HTTP://url/"));
+    assertTrue(hasValidScheme("https://url/"));
+    assertTrue(hasValidScheme("mailto://url/"));
+    assertFalse(hasValidScheme("ftp://url/"));
+    assertFalse(hasValidScheme("data:evil"));
+    assertFalse(hasValidScheme("javascript:alert(1)"));
+  }
+
+  public void testInvalidSchemeInReplace() {
+    try {
+      new LinkFindReplace("find", "javascript:alert(1)").replace("find");
+      fail("Expected IllegalStateException");
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  public void testInvalidSchemeWithBackreference() {
+    try {
+      new LinkFindReplace(".*(script:[^;]*)", "java$1")
+          .replace("Look at this script: alert(1);");
+      fail("Expected IllegalStateException");
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  public void testReplaceEscaping() {
+    assertEquals("<a href=\"a&quot;&amp;&#39;&lt;&gt;b\">find</a>",
+        new LinkFindReplace("find", "a\"&'<>b").replace("find"));
+  }
+
+  public void testHtmlInFind() {
+    String rawFind = "<b>&quot;bold&quot;</b>";
+    LinkFindReplace a = new LinkFindReplace(rawFind, "/bold");
+    assertEquals(rawFind, a.pattern().getSource());
+    assertEquals("<a href=\"/bold\">" + rawFind + "</a>", a.replace(rawFind));
+  }
+}
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/RawFindReplaceTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/RawFindReplaceTest.java
new file mode 100644
index 0000000..9c450bd
--- /dev/null
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/RawFindReplaceTest.java
@@ -0,0 +1,28 @@
+// 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.gwtexpui.safehtml.client;
+
+import junit.framework.TestCase;
+
+public class RawFindReplaceTest extends TestCase {
+  public void testFindReplace() {
+    final String find = "find";
+    final String replace = "replace";
+    final RawFindReplace a = new RawFindReplace(find, replace);
+    assertEquals(find, a.pattern().getSource());
+    assertEquals(replace, a.replace(find));
+    assertEquals("find = " + find + ", replace = " + replace, a.toString());
+  }
+}
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilderTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilderTest.java
new file mode 100644
index 0000000..a6b0012
--- /dev/null
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilderTest.java
@@ -0,0 +1,265 @@
+// 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.gwtexpui.safehtml.client;
+
+import junit.framework.TestCase;
+
+public class SafeHtmlBuilderTest extends TestCase {
+  public void testEmpty() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    assertTrue(b.isEmpty());
+    assertFalse(b.hasContent());
+    assertEquals("", b.asString());
+
+    b.append("a");
+    assertTrue(b.hasContent());
+    assertEquals("a", b.asString());
+  }
+
+  public void testToSafeHtml() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    b.append(1);
+
+    final SafeHtml h = b.toSafeHtml();
+    assertNotNull(h);
+    assertNotSame(h, b);
+    assertFalse(h instanceof SafeHtmlBuilder);
+    assertEquals("1", h.asString());
+  }
+
+  public void testAppend_boolean() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    assertSame(b, b.append(true));
+    assertSame(b, b.append(false));
+    assertEquals("truefalse", b.asString());
+  }
+
+  public void testAppend_char() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    assertSame(b, b.append('a'));
+    assertSame(b, b.append('b'));
+    assertEquals("ab", b.asString());
+  }
+
+  public void testAppend_int() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    assertSame(b, b.append(4));
+    assertSame(b, b.append(2));
+    assertSame(b, b.append(-100));
+    assertEquals("42-100", b.asString());
+  }
+
+  public void testAppend_long() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    assertSame(b, b.append(4L));
+    assertSame(b, b.append(2L));
+    assertEquals("42", b.asString());
+  }
+
+  public void testAppend_float() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    assertSame(b, b.append(0.0f));
+    assertEquals("0.0", b.asString());
+  }
+
+  public void testAppend_double() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    assertSame(b, b.append(0.0));
+    assertEquals("0.0", b.asString());
+  }
+
+  public void testAppend_String() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    assertSame(b, b.append((String) null));
+    assertEquals("", b.asString());
+    assertSame(b, b.append("foo"));
+    assertSame(b, b.append("bar"));
+    assertEquals("foobar", b.asString());
+  }
+
+  public void testAppend_StringBuilder() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    assertSame(b, b.append((StringBuilder) null));
+    assertEquals("", b.asString());
+    assertSame(b, b.append(new StringBuilder("foo")));
+    assertSame(b, b.append(new StringBuilder("bar")));
+    assertEquals("foobar", b.asString());
+  }
+
+  public void testAppend_StringBuffer() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    assertSame(b, b.append((StringBuffer) null));
+    assertEquals("", b.asString());
+    assertSame(b, b.append(new StringBuffer("foo")));
+    assertSame(b, b.append(new StringBuffer("bar")));
+    assertEquals("foobar", b.asString());
+  }
+
+  public void testAppend_Object() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    assertSame(b, b.append((Object) null));
+    assertEquals("", b.asString());
+    assertSame(b, b.append(new Object() {
+      @Override
+      public String toString() {
+        return "foobar";
+      }
+    }));
+    assertEquals("foobar", b.asString());
+  }
+
+  public void testAppend_CharSequence() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    assertSame(b, b.append((CharSequence) null));
+    assertEquals("", b.asString());
+    assertSame(b, b.append((CharSequence) "foo"));
+    assertSame(b, b.append((CharSequence) "bar"));
+    assertEquals("foobar", b.asString());
+  }
+
+  public void testAppend_SafeHtml() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    assertSame(b, b.append((SafeHtml) null));
+    assertEquals("", b.asString());
+    assertSame(b, b.append(new SafeHtmlString("foo")));
+    assertSame(b, b.append(new SafeHtmlBuilder().append("bar")));
+    assertEquals("foobar", b.asString());
+  }
+
+  public void testHtmlSpecialCharacters() {
+    assertEquals("&amp;", escape("&"));
+    assertEquals("&lt;", escape("<"));
+    assertEquals("&gt;", escape(">"));
+    assertEquals("&quot;", escape("\""));
+    assertEquals("&#39;", escape("'"));
+
+    assertEquals("&amp;", escape('&'));
+    assertEquals("&lt;", escape('<'));
+    assertEquals("&gt;", escape('>'));
+    assertEquals("&quot;", escape('"'));
+    assertEquals("&#39;", escape('\''));
+
+    assertEquals("&lt;b&gt;", escape("<b>"));
+    assertEquals("&amp;lt;b&amp;gt;", escape("&lt;b&gt;"));
+  }
+
+  public void testEntityNbsp() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    assertSame(b, b.nbsp());
+    assertEquals("&nbsp;", b.asString());
+  }
+
+  public void testTagBr() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    assertSame(b, b.br());
+    assertEquals("<br />", b.asString());
+  }
+
+  public void testTagTableTrTd() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    assertSame(b, b.openElement("table"));
+    assertSame(b, b.openTr());
+    assertSame(b, b.openTd());
+    assertSame(b, b.append("d<a>ta"));
+    assertSame(b, b.closeTd());
+    assertSame(b, b.closeTr());
+    assertSame(b, b.closeElement("table"));
+    assertEquals("<table><tr><td>d&lt;a&gt;ta</td></tr></table>", b.asString());
+  }
+
+  public void testTagDiv() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    assertSame(b, b.openDiv());
+    assertSame(b, b.append("d<a>ta"));
+    assertSame(b, b.closeDiv());
+    assertEquals("<div>d&lt;a&gt;ta</div>", b.asString());
+  }
+
+  public void testTagAnchor() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    assertSame(b, b.openAnchor());
+
+    assertEquals("", b.getAttribute("href"));
+    assertSame(b, b.setAttribute("href", "http://here"));
+    assertEquals("http://here", b.getAttribute("href"));
+    assertSame(b, b.setAttribute("href", "d<a>ta"));
+    assertEquals("d<a>ta", b.getAttribute("href"));
+
+    assertEquals("", b.getAttribute("target"));
+    assertSame(b, b.setAttribute("target", null));
+    assertEquals("", b.getAttribute("target"));
+
+    assertSame(b, b.append("go"));
+    assertSame(b, b.closeAnchor());
+    assertEquals("<a href=\"d&lt;a&gt;ta\">go</a>", b.asString());
+  }
+
+  public void testTagHeightWidth() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    assertSame(b, b.openElement("img"));
+    assertSame(b, b.setHeight(100));
+    assertSame(b, b.setWidth(42));
+    assertSame(b, b.closeSelf());
+    assertEquals("<img height=\"100\" width=\"42\" />", b.asString());
+  }
+
+  public void testStyleName() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    assertSame(b, b.openSpan());
+    assertSame(b, b.setStyleName("foo"));
+    assertSame(b, b.addStyleName("bar"));
+    assertSame(b, b.append("d<a>ta"));
+    assertSame(b, b.closeSpan());
+    assertEquals("<span class=\"foo bar\">d&lt;a&gt;ta</span>", b.asString());
+  }
+
+  public void testRejectJavaScript_AnchorHref() {
+    final String href = "javascript:window.close();";
+    try {
+      new SafeHtmlBuilder().openAnchor().setAttribute("href", href);
+      fail("accepted javascript in a href");
+    } catch (RuntimeException e) {
+      assertEquals("javascript unsafe in href: " + href, e.getMessage());
+    }
+  }
+
+  public void testRejectJavaScript_ImgSrc() {
+    final String href = "javascript:window.close();";
+    try {
+      new SafeHtmlBuilder().openElement("img").setAttribute("src", href);
+      fail("accepted javascript in img src");
+    } catch (RuntimeException e) {
+      assertEquals("javascript unsafe in href: " + href, e.getMessage());
+    }
+  }
+
+  public void testRejectJavaScript_FormAction() {
+    final String href = "javascript:window.close();";
+    try {
+      new SafeHtmlBuilder().openElement("form").setAttribute("action", href);
+      fail("accepted javascript in form action");
+    } catch (RuntimeException e) {
+      assertEquals("javascript unsafe in href: " + href, e.getMessage());
+    }
+  }
+
+  private static String escape(final char c) {
+    return new SafeHtmlBuilder().append(c).asString();
+  }
+
+  private static String escape(final String c) {
+    return new SafeHtmlBuilder().append(c).asString();
+  }
+}
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_LinkifyTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_LinkifyTest.java
new file mode 100644
index 0000000..a9d9450
--- /dev/null
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_LinkifyTest.java
@@ -0,0 +1,58 @@
+// 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.gwtexpui.safehtml.client;
+
+import junit.framework.TestCase;
+
+public class SafeHtml_LinkifyTest extends TestCase {
+  public void testLinkify_SimpleHttp1() {
+    final SafeHtml o = html("A http://go.here/ B");
+    final SafeHtml n = o.linkify();
+    assertNotSame(o, n);
+    assertEquals("A <a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a> B", n.asString());
+  }
+
+  public void testLinkify_SimpleHttps2() {
+    final SafeHtml o = html("A https://go.here/ B");
+    final SafeHtml n = o.linkify();
+    assertNotSame(o, n);
+    assertEquals("A <a href=\"https://go.here/\" target=\"_blank\">https://go.here/</a> B", n.asString());
+  }
+
+  public void testLinkify_Parens1() {
+    final SafeHtml o = html("A (http://go.here/) B");
+    final SafeHtml n = o.linkify();
+    assertNotSame(o, n);
+    assertEquals("A (<a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a>) B", n.asString());
+  }
+
+  public void testLinkify_Parens() {
+    final SafeHtml o = html("A http://go.here/#m() B");
+    final SafeHtml n = o.linkify();
+    assertNotSame(o, n);
+    assertEquals("A <a href=\"http://go.here/#m()\" target=\"_blank\">http://go.here/#m()</a> B", n.asString());
+  }
+
+  public void testLinkify_AngleBrackets1() {
+    final SafeHtml o = html("A <http://go.here/> B");
+    final SafeHtml n = o.linkify();
+    assertNotSame(o, n);
+    assertEquals("A &lt;<a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a>&gt; B", n.asString());
+  }
+
+  private static SafeHtml html(String text) {
+    return new SafeHtmlBuilder().append(text).toSafeHtml();
+  }
+}
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_ReplaceTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_ReplaceTest.java
new file mode 100644
index 0000000..d7a3aaf
--- /dev/null
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_ReplaceTest.java
@@ -0,0 +1,119 @@
+// 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.gwtexpui.safehtml.client;
+
+import junit.framework.TestCase;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+public class SafeHtml_ReplaceTest extends TestCase {
+  public void testReplaceEmpty() {
+    SafeHtml o = html("A\nissue42\nB");
+    assertSame(o, o.replaceAll(null));
+    assertSame(o, o.replaceAll(Collections.<FindReplace> emptyList()));
+  }
+
+  public void testReplaceOneLink() {
+    SafeHtml o = html("A\nissue 42\nB");
+    SafeHtml n = o.replaceAll(repls(
+        new RawFindReplace("(issue\\s(\\d+))", "<a href=\"?$2\">$1</a>")));
+    assertNotSame(o, n);
+    assertEquals("A\n<a href=\"?42\">issue 42</a>\nB", n.asString());
+  }
+
+  public void testReplaceNoLeadingOrTrailingText() {
+    SafeHtml o = html("issue 42");
+    SafeHtml n = o.replaceAll(repls(
+        new RawFindReplace("(issue\\s(\\d+))", "<a href=\"?$2\">$1</a>")));
+    assertNotSame(o, n);
+    assertEquals("<a href=\"?42\">issue 42</a>", n.asString());
+  }
+
+  public void testReplaceTwoLinks() {
+    SafeHtml o = html("A\nissue 42\nissue 9918\nB");
+    SafeHtml n = o.replaceAll(repls(
+        new RawFindReplace("(issue\\s(\\d+))", "<a href=\"?$2\">$1</a>")));
+    assertNotSame(o, n);
+    assertEquals("A\n"
+        + "<a href=\"?42\">issue 42</a>\n"
+        + "<a href=\"?9918\">issue 9918</a>\n"
+        + "B"
+    , n.asString());
+  }
+
+  public void testReplaceInOrder() {
+    SafeHtml o = html("A\nissue 42\nReally GWTEXPUI-9918 is better\nB");
+    SafeHtml n = o.replaceAll(repls(
+        new RawFindReplace("(GWTEXPUI-(\\d+))",
+            "<a href=\"gwtexpui-bug?$2\">$1</a>"),
+        new RawFindReplace("(issue\\s+(\\d+))",
+            "<a href=\"generic-bug?$2\">$1</a>")));
+    assertNotSame(o, n);
+    assertEquals("A\n"
+        + "<a href=\"generic-bug?42\">issue 42</a>\n"
+        + "Really <a href=\"gwtexpui-bug?9918\">GWTEXPUI-9918</a> is better\n"
+        + "B"
+    , n.asString());
+  }
+
+  public void testReplaceOverlappingAfterFirstChar() {
+    SafeHtml o = html("abcd");
+    RawFindReplace ab = new RawFindReplace("ab", "AB");
+    RawFindReplace bc = new RawFindReplace("bc", "23");
+    RawFindReplace cd = new RawFindReplace("cd", "YZ");
+
+    assertEquals("ABcd", o.replaceAll(repls(ab, bc)).asString());
+    assertEquals("ABcd", o.replaceAll(repls(bc, ab)).asString());
+    assertEquals("ABYZ", o.replaceAll(repls(ab, bc, cd)).asString());
+  }
+
+  public void testReplaceOverlappingAtFirstCharLongestMatch() {
+    SafeHtml o = html("abcd");
+    RawFindReplace ab = new RawFindReplace("ab", "AB");
+    RawFindReplace abc = new RawFindReplace("[^d][^d][^d]", "234");
+
+    assertEquals("ABcd", o.replaceAll(repls(ab, abc)).asString());
+    assertEquals("234d", o.replaceAll(repls(abc, ab)).asString());
+  }
+
+  public void testReplaceOverlappingAtFirstCharFirstMatch() {
+    SafeHtml o = html("abcd");
+    RawFindReplace ab1 = new RawFindReplace("ab", "AB");
+    RawFindReplace ab2 = new RawFindReplace("[^cd][^cd]", "12");
+
+    assertEquals("ABcd", o.replaceAll(repls(ab1, ab2)).asString());
+    assertEquals("12cd", o.replaceAll(repls(ab2, ab1)).asString());
+  }
+
+  public void testFailedSanitization() {
+    SafeHtml o = html("abcd");
+    LinkFindReplace evil = new LinkFindReplace("(b)", "javascript:alert('$1')");
+    LinkFindReplace ok = new LinkFindReplace("(b)", "/$1");
+    assertEquals("abcd", o.replaceAll(repls(evil)).asString());
+    String linked = "a<a href=\"/b\">b</a>cd";
+    assertEquals(linked, o.replaceAll(repls(ok)).asString());
+    assertEquals(linked, o.replaceAll(repls(evil, ok)).asString());
+  }
+
+  private static SafeHtml html(String text) {
+    return new SafeHtmlBuilder().append(text).toSafeHtml();
+  }
+
+  private static List<FindReplace> repls(FindReplace... repls) {
+    return Arrays.asList(repls);
+  }
+}
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyListTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyListTest.java
new file mode 100644
index 0000000..250a1b5
--- /dev/null
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyListTest.java
@@ -0,0 +1,133 @@
+// 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 "<p>AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.safehtml.client;
+
+import junit.framework.TestCase;
+
+public class SafeHtml_WikifyListTest extends TestCase {
+  private static final String BEGIN_LIST = "<ul class=\"wikiList\">";
+  private static final String END_LIST = "</ul>";
+
+  private static String item(String raw) {
+    return "<li>" + raw + "</li>";
+  }
+
+  public void testBulletList1() {
+    final SafeHtml o = html("A\n\n* line 1\n* 2nd line");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals("<p>A</p>"//
+        + BEGIN_LIST //
+        + item("line 1") //
+        + item("2nd line") //
+        + END_LIST //
+    , n.asString());
+  }
+
+  public void testBulletList2() {
+    final SafeHtml o = html("A\n\n* line 1\n* 2nd line\n\nB");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals("<p>A</p>"//
+        + BEGIN_LIST //
+        + item("line 1") //
+        + item("2nd line") //
+        + END_LIST //
+        + "<p>B</p>" //
+    , n.asString());
+  }
+
+  public void testBulletList3() {
+    final SafeHtml o = html("* line 1\n* 2nd line\n\nB");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals(BEGIN_LIST //
+        + item("line 1") //
+        + item("2nd line") //
+        + END_LIST //
+        + "<p>B</p>" //
+    , n.asString());
+  }
+
+  public void testBulletList4() {
+    final SafeHtml o = html("To see this bug, you have to:\n" //
+        + "* Be on IMAP or EAS (not on POP)\n"//
+        + "* Be very unlucky\n");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals("<p>To see this bug, you have to:</p>" //
+        + BEGIN_LIST //
+        + item("Be on IMAP or EAS (not on POP)") //
+        + item("Be very unlucky") //
+        + END_LIST //
+    , n.asString());
+  }
+
+  public void testBulletList5() {
+    final SafeHtml o = html("To see this bug,\n" //
+        + "you have to:\n" //
+        + "* Be on IMAP or EAS (not on POP)\n"//
+        + "* Be very unlucky\n");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals("<p>To see this bug, you have to:</p>" //
+        + BEGIN_LIST //
+        + item("Be on IMAP or EAS (not on POP)") //
+        + item("Be very unlucky") //
+        + END_LIST //
+    , n.asString());
+  }
+
+  public void testDashList1() {
+    final SafeHtml o = html("A\n\n- line 1\n- 2nd line");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals("<p>A</p>"//
+        + BEGIN_LIST //
+        + item("line 1") //
+        + item("2nd line") //
+        + END_LIST //
+    , n.asString());
+  }
+
+  public void testDashList2() {
+    final SafeHtml o = html("A\n\n- line 1\n- 2nd line\n\nB");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals("<p>A</p>"//
+        + BEGIN_LIST //
+        + item("line 1") //
+        + item("2nd line") //
+        + END_LIST //
+        + "<p>B</p>" //
+    , n.asString());
+  }
+
+  public void testDashList3() {
+    final SafeHtml o = html("- line 1\n- 2nd line\n\nB");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals(BEGIN_LIST //
+        + item("line 1") //
+        + item("2nd line") //
+        + END_LIST //
+        + "<p>B</p>" //
+    , n.asString());
+  }
+
+  private static SafeHtml html(String text) {
+    return new SafeHtmlBuilder().append(text).toSafeHtml();
+  }
+}
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyPreformatTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyPreformatTest.java
new file mode 100644
index 0000000..cbb315b
--- /dev/null
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyPreformatTest.java
@@ -0,0 +1,82 @@
+// 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 "<p>AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.safehtml.client;
+
+import junit.framework.TestCase;
+
+public class SafeHtml_WikifyPreformatTest extends TestCase {
+  private static final String B = "<span class=\"wikiPreFormat\">";
+  private static final String E = "</span><br />";
+
+  private static String pre(String raw) {
+    return B + raw + E;
+  }
+
+  public void testPreformat1() {
+    final SafeHtml o = html("A\n\n  This is pre\n  formatted");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals("<p>A</p>"//
+        + "<p>" //
+        + pre("  This is pre") //
+        + pre("  formatted") //
+        + "</p>" //
+    , n.asString());
+  }
+
+  public void testPreformat2() {
+    final SafeHtml o = html("A\n\n  This is pre\n  formatted\n\nbut this is not");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals("<p>A</p>" //
+        + "<p>" //
+        + pre("  This is pre") //
+        + pre("  formatted") //
+        + "</p>" //
+        + "<p>but this is not</p>" //
+    , n.asString());
+  }
+
+  public void testPreformat3() {
+    final SafeHtml o = html("A\n\n  Q\n    <R>\n  S\n\nB");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals("<p>A</p>" //
+        + "<p>" //
+        + pre("  Q") //
+        + pre("    &lt;R&gt;") //
+        + pre("  S") //
+        + "</p>" //
+        + "<p>B</p>" //
+    , n.asString());
+  }
+
+  public void testPreformat4() {
+    final SafeHtml o = html("  Q\n    <R>\n  S\n\nB");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals("<p>" //
+        + pre("  Q") //
+        + pre("    &lt;R&gt;") //
+        + pre("  S") //
+        + "</p>" //
+        + "<p>B</p>" //
+    , n.asString());
+  }
+
+  private static SafeHtml html(String text) {
+    return new SafeHtmlBuilder().append(text).toSafeHtml();
+  }
+}
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyTest.java
new file mode 100644
index 0000000..c9837037
--- /dev/null
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyTest.java
@@ -0,0 +1,93 @@
+// 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 "<p>AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.safehtml.client;
+
+import junit.framework.TestCase;
+
+public class SafeHtml_WikifyTest extends TestCase {
+  public void testWikify_OneLine1() {
+    final SafeHtml o = html("A  B");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals("<p>A  B</p>", n.asString());
+  }
+
+  public void testWikify_OneLine2() {
+    final SafeHtml o = html("A  B\n");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals("<p>A  B\n</p>", n.asString());
+  }
+
+  public void testWikify_OneParagraph1() {
+    final SafeHtml o = html("A\nB");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals("<p>A\nB</p>", n.asString());
+  }
+
+  public void testWikify_OneParagraph2() {
+    final SafeHtml o = html("A\nB\n");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals("<p>A\nB\n</p>", n.asString());
+  }
+
+  public void testWikify_TwoParagraphs() {
+    final SafeHtml o = html("A\nB\n\nC\nD");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals("<p>A\nB</p><p>C\nD</p>", n.asString());
+  }
+
+  public void testLinkify_SimpleHttp1() {
+    final SafeHtml o = html("A http://go.here/ B");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals("<p>A <a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a> B</p>", n.asString());
+  }
+
+  public void testLinkify_SimpleHttps2() {
+    final SafeHtml o = html("A https://go.here/ B");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals("<p>A <a href=\"https://go.here/\" target=\"_blank\">https://go.here/</a> B</p>", n.asString());
+  }
+
+  public void testLinkify_Parens1() {
+    final SafeHtml o = html("A (http://go.here/) B");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals("<p>A (<a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a>) B</p>", n.asString());
+  }
+
+  public void testLinkify_Parens() {
+    final SafeHtml o = html("A http://go.here/#m() B");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals("<p>A <a href=\"http://go.here/#m()\" target=\"_blank\">http://go.here/#m()</a> B</p>", n.asString());
+  }
+
+  public void testLinkify_AngleBrackets1() {
+    final SafeHtml o = html("A <http://go.here/> B");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals("<p>A &lt;<a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a>&gt; B</p>", n.asString());
+  }
+
+  private static SafeHtml html(String text) {
+    return new SafeHtmlBuilder().append(text).toSafeHtml();
+  }
+}
diff --git a/gerrit-gwtui/.settings/org.eclipse.core.resources.prefs b/gerrit-gwtui/.settings/org.eclipse.core.resources.prefs
index e9441bb..f9fe345 100644
--- a/gerrit-gwtui/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-gwtui/.settings/org.eclipse.core.resources.prefs
@@ -1,3 +1,4 @@
 eclipse.preferences.version=1
 encoding//src/main/java=UTF-8
+encoding//src/test/java=UTF-8
 encoding/<project>=UTF-8
diff --git a/gerrit-gwtui/pom.xml b/gerrit-gwtui/pom.xml
index 43b26c1..17065fe 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.6</version>
+    <version>2.7</version>
   </parent>
 
   <artifactId>gerrit-gwtui</artifactId>
@@ -40,12 +40,14 @@
     </dependency>
 
     <dependency>
-      <groupId>gwtexpui</groupId>
-      <artifactId>gwtexpui</artifactId>
+      <groupId>com.google.gerrit</groupId>
+      <artifactId>gerrit-gwtexpui</artifactId>
+      <version>${project.version}</version>
     </dependency>
     <dependency>
-      <groupId>gwtexpui</groupId>
-      <artifactId>gwtexpui</artifactId>
+      <groupId>com.google.gerrit</groupId>
+      <artifactId>gerrit-gwtexpui</artifactId>
+      <version>${project.version}</version>
       <classifier>sources</classifier>
       <type>jar</type>
     </dependency>
@@ -149,53 +151,6 @@
     </dependency>
   </dependencies>
 
-  <profiles>
-    <profile>
-      <id>all</id>
-      <activation>
-        <activeByDefault>true</activeByDefault>
-      </activation>
-      <properties>
-        <GerritGwtUI.browserType>com.google.gerrit.GerritGwtUI</GerritGwtUI.browserType>
-        <GerritGwtUI.draftCompile>false</GerritGwtUI.draftCompile>
-      </properties>
-    </profile>
-    <profile>
-      <id>safari</id>
-      <properties>
-        <GerritGwtUI.browserType>com.google.gerrit.GerritGwtUIsafari</GerritGwtUI.browserType>
-        <GerritGwtUI.draftCompile>true</GerritGwtUI.draftCompile>
-      </properties>
-    </profile>
-    <profile>
-      <id>chrome</id>
-      <properties>
-        <GerritGwtUI.browserType>com.google.gerrit.GerritGwtUIsafari</GerritGwtUI.browserType>
-        <GerritGwtUI.draftCompile>true</GerritGwtUI.draftCompile>
-      </properties>
-    </profile>
-    <profile>
-      <id>webkit</id>
-      <properties>
-        <GerritGwtUI.browserType>com.google.gerrit.GerritGwtUIsafari</GerritGwtUI.browserType>
-        <GerritGwtUI.draftCompile>true</GerritGwtUI.draftCompile>
-      </properties>
-    </profile>
-    <profile>
-      <id>gecko1_8</id>
-      <properties>
-        <GerritGwtUI.browserType>com.google.gerrit.GerritGwtUIgecko1_8</GerritGwtUI.browserType>
-        <GerritGwtUI.draftCompile>true</GerritGwtUI.draftCompile>
-      </properties>
-    </profile>
-    <profile>
-      <id>firefox</id>
-      <properties>
-        <GerritGwtUI.browserType>com.google.gerrit.GerritGwtUIgecko1_8</GerritGwtUI.browserType>
-        <GerritGwtUI.draftCompile>true</GerritGwtUI.draftCompile>
-      </properties>
-    </profile>
-  </profiles>
 
   <build>
     <plugins>
@@ -206,12 +161,11 @@
           <execution>
             <id>optimized</id>
             <configuration>
-              <module>${GerritGwtUI.browserType}</module>
+              <module>com.google.gerrit.GerritGwtUI</module>
               <extraJvmArgs>-Xmx512m</extraJvmArgs>
               <compileReport>${gwt.compileReport}</compileReport>
               <disableClassMetadata>true</disableClassMetadata>
               <disableCastChecking>true</disableCastChecking>
-              <draftCompile>${GerritGwtUI.draftCompile}</draftCompile>
             </configuration>
             <goals>
               <goal>compile</goal>
@@ -221,11 +175,10 @@
             <id>debug</id>
             <configuration>
               <style>PRETTY</style>
-              <module>${GerritGwtUI.browserType}</module>
+              <module>com.google.gerrit.GerritGwtUI</module>
               <extraJvmArgs>-Xmx512m</extraJvmArgs>
               <disableClassMetadata>true</disableClassMetadata>
               <disableRunAsync>true</disableRunAsync>
-              <draftCompile>true</draftCompile>
               <webappDirectory>${project.build.directory}/${project.build.finalName}_dbg</webappDirectory>
             </configuration>
             <goals>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIsafari.gwt.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIsafari.gwt.xml
deleted file mode 100644
index 88bea84..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIsafari.gwt.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<!--
- Copyright (C) 2011 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<module rename-to="gerrit_ui">
-  <inherits name='com.google.gerrit.GerritGwtUI'/>
-  <set-property name="user.agent" value="safari" />
-  <set-property name="locale" value="default" />
-</module>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/UserAgent.gwt.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/UserAgent.gwt.xml
index f2774c9..1dce6df 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/UserAgent.gwt.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/UserAgent.gwt.xml
@@ -21,4 +21,13 @@
       <when-property-is name="user.agent" value="ie8"/>
     </any>
   </replace-with>
+
+  <replace-with class="com.google.gerrit.client.Themer.ThemerIE">
+    <when-type-is class="com.google.gerrit.client.Themer" />
+    <any>
+      <when-property-is name="user.agent" value="ie6"/>
+      <when-property-is name="user.agent" value="ie8"/>
+      <when-property-is name="user.agent" value="ie9"/>
+    </any>
+  </replace-with>
 </module>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java
index 0059723..5dcccb0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java
@@ -14,16 +14,26 @@
 
 package com.google.gerrit.client;
 
+import com.google.gerrit.client.account.AccountInfo;
+import com.google.gerrit.client.changes.Util;
 import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gwt.event.dom.client.ErrorEvent;
-import com.google.gwt.event.dom.client.ErrorHandler;
+import com.google.gwt.event.dom.client.LoadEvent;
+import com.google.gwt.event.dom.client.LoadHandler;
+import com.google.gwt.event.dom.client.MouseOutEvent;
+import com.google.gwt.event.dom.client.MouseOutHandler;
+import com.google.gwt.event.dom.client.MouseOverEvent;
+import com.google.gwt.event.dom.client.MouseOverHandler;
+import com.google.gwt.user.client.Timer;
 import com.google.gwt.user.client.ui.Image;
+import com.google.gwt.user.client.ui.UIObject;
 
 public class AvatarImage extends Image {
 
+  public AvatarImage() {
+  }
+
   /** A default sized avatar image. */
-  public AvatarImage(Account.Id account) {
+  public AvatarImage(AccountInfo account) {
     this(account, 0);
   }
 
@@ -35,30 +45,66 @@
    *        on the avatar provider. A size <= 0 indicates to let the provider
    *        decide a default size.
    */
-  public AvatarImage(Account.Id account, int size) {
-    super(url(account, size));
+  public AvatarImage(AccountInfo account, int size) {
+    this(account, size, true);
+  }
+
+  /**
+   * An avatar image for the given account using the requested size.
+   *
+   * @param account The account in which we are interested
+   * @param size A requested size. Note that the size can be ignored depending
+   *        on the avatar provider. A size <= 0 indicates to let the provider
+   *        decide a default size.
+   * @param addPopup show avatar popup with user info on hovering over the
+   *        avatar image
+   */
+  public AvatarImage(AccountInfo account, int size, boolean addPopup) {
+    setAccount(account, size, addPopup);
+  }
+
+  public void setAccount(AccountInfo account, int size, boolean addPopup) {
+    setUrl(isGerritServer(account) ? getGerritServerAvatarUrl() :
+      url(account.email(), size));
+    setVisible(false);
 
     if (size > 0) {
       // If the provider does not resize the image, force it in the browser.
       setSize(size + "px", size + "px");
     }
 
-    addErrorHandler(new ErrorHandler() {
+    addLoadHandler(new LoadHandler() {
       @Override
-      public void onError(ErrorEvent event) {
-        // We got a 404, don't bother showing the image. Either the user doesn't
-        // have an avatar or there is no avatar provider plugin installed.
-        setVisible(false);
+      public void onLoad(LoadEvent event) {
+        setVisible(true);
       }
     });
+
+    if (addPopup) {
+      PopupHandler popupHandler = new PopupHandler(account, this);
+      addMouseOverHandler(popupHandler);
+      addMouseOutHandler(popupHandler);
+    }
   }
 
-  private static String url(Account.Id id, int size) {
+  private static boolean isGerritServer(AccountInfo account) {
+    return account._account_id() == 0
+        && Util.C.messageNoAuthor().equals(account.name());
+  }
+
+  private static String getGerritServerAvatarUrl() {
+    return Gerrit.RESOURCES.gerritAvatar().getSafeUri().asString();
+  }
+
+  private static String url(String email, int size) {
+    if (email == null) {
+      return "";
+    }
     String u;
-    if (Gerrit.isSignedIn() && id.equals(Gerrit.getUserAccount().getId())) {
+    if (Gerrit.isSignedIn() && email.equals(Gerrit.getUserAccount().getPreferredEmail())) {
       u = "self";
     } else {
-      u = id.toString();
+      u = email;
     }
     RestApi api = new RestApi("/accounts/").id(u).view("avatar");
     if (size > 0) {
@@ -66,4 +112,87 @@
     }
     return api.url();
   }
+
+  private class PopupHandler implements MouseOverHandler, MouseOutHandler {
+    private final AccountInfo account;
+    private final UIObject target;
+
+    private UserPopupPanel popup;
+    private Timer showTimer;
+    private Timer hideTimer;
+
+    public PopupHandler(AccountInfo account, UIObject target) {
+      this.account = account;
+      this.target = target;
+    }
+
+    private UserPopupPanel createPopupPanel(AccountInfo account) {
+      UserPopupPanel popup = new UserPopupPanel(account, false, false);
+      popup.addDomHandler(new MouseOverHandler() {
+        @Override
+        public void onMouseOver(MouseOverEvent event) {
+          scheduleShow();
+        }
+      }, MouseOverEvent.getType());
+      popup.addDomHandler(new MouseOutHandler() {
+        @Override
+        public void onMouseOut(MouseOutEvent event) {
+          scheduleHide();
+        }
+      }, MouseOutEvent.getType());
+      return popup;
+    }
+
+    @Override
+    public void onMouseOver(MouseOverEvent event) {
+      scheduleShow();
+    }
+
+    @Override
+    public void onMouseOut(MouseOutEvent event) {
+      scheduleHide();
+    }
+
+    private void scheduleShow() {
+      if (hideTimer != null) {
+        hideTimer.cancel();
+        hideTimer = null;
+      }
+      if ((popup != null && popup.isShowing() && popup.isVisible())
+          || showTimer != null) {
+        return;
+      }
+      showTimer = new Timer() {
+        @Override
+        public void run() {
+          if (popup == null) {
+            popup = createPopupPanel(account);
+          }
+          if (!popup.isShowing() || !popup.isVisible()) {
+            popup.showRelativeTo(target);
+          }
+
+        }
+      };
+      showTimer.schedule(600);
+    }
+
+    private void scheduleHide() {
+      if (showTimer != null) {
+        showTimer.cancel();
+        showTimer = null;
+      }
+      if (popup == null || !popup.isShowing() || !popup.isVisible()
+              || hideTimer != null) {
+        return;
+      }
+      hideTimer = new Timer() {
+        @Override
+        public void run() {
+          popup.hide();
+        }
+      };
+      hideTimer.schedule(50);
+    }
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
index 5448dc0..2e94db7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
@@ -119,6 +119,11 @@
     }
   }
 
+  /** Format a date using git log's relative date format. */
+  public static String relativeFormat(Date dt) {
+    return RelativeDateFormatter.format(dt);
+  }
+
   /**
    * Formats an account as a name and an email address.
    * <p>
@@ -173,7 +178,7 @@
     return nameEmail(ai);
   }
 
-  private static AccountInfo asInfo(Account acct) {
+  public static AccountInfo asInfo(Account acct) {
     if (acct == null) {
       return AccountInfo.create(0, null, null);
     }
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 d67a4ca..0b1af89 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
@@ -19,6 +19,7 @@
 import static com.google.gerrit.common.data.GlobalCapability.CREATE_PROJECT;
 
 import com.google.gerrit.client.account.AccountCapabilities;
+import com.google.gerrit.client.account.AccountInfo;
 import com.google.gerrit.client.admin.ProjectScreen;
 import com.google.gerrit.client.changes.ChangeConstants;
 import com.google.gerrit.client.changes.ChangeListScreen;
@@ -95,6 +96,7 @@
       GWT.create(GerritResources.class);
   public static final SystemInfoService SYSTEM_SVC;
   public static final EventBus EVENT_BUS = GWT.create(SimpleEventBus.class);
+  public static Themer THEMER = GWT.create(Themer.class);
 
   private static String myHost;
   private static GerritConfig myConfig;
@@ -248,6 +250,11 @@
     return myAccount;
   }
 
+  /** @return the currently signed in user's account data; empty account data if no account */
+  public static AccountInfo getUserAccountInfo() {
+    return FormatUtil.asInfo(myAccount);
+  }
+
   /** @return access token to prove user identity during REST API calls. */
   public static String getXGerritAuth() {
     return xGerritAuth;
@@ -552,9 +559,17 @@
     if (signInAnchor != null) {
       signInAnchor.setHref(loginRedirect(token));
     }
+
+    saveDefaultTheme();
     loadPlugins(hpd, token);
   }
 
+  private void saveDefaultTheme() {
+    THEMER.init(Document.get().getElementById("gerrit_sitecss"),
+        Document.get().getElementById("gerrit_header"),
+        Document.get().getElementById("gerrit_footer"));
+  }
+
   private void loadPlugins(HostPageData hpd, final String token) {
     if (hpd.plugins != null) {
       for (final String url : hpd.plugins) {
@@ -754,9 +769,9 @@
   }
 
   private static void whoAmI(boolean canLogOut) {
-    Account account = getUserAccount();
-    final CurrentUserPopupPanel userPopup =
-        new CurrentUserPopupPanel(account, canLogOut);
+    AccountInfo account = getUserAccountInfo();
+    final UserPopupPanel userPopup =
+        new UserPopupPanel(account, canLogOut, true);
     final FlowPanel userSummaryPanel = new FlowPanel();
     class PopupHandler implements KeyDownHandler, ClickHandler {
       private void showHidePopup() {
@@ -783,7 +798,7 @@
     final PopupHandler popupHandler = new PopupHandler();
     final InlineLabel l = new InlineLabel(FormatUtil.name(account));
     l.setStyleName(RESOURCES.css().menuBarUserName());
-    final AvatarImage avatar = new AvatarImage(account.getId(), 26);
+    final AvatarImage avatar = new AvatarImage(account, 26, false);
     avatar.setStyleName(RESOURCES.css().menuBarUserNameAvatar());
     userSummaryPanel.setStyleName(RESOURCES.css().menuBarUserNamePanel());
     userSummaryPanel.add(l);
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 489ff00..a33556e 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
@@ -21,6 +21,7 @@
   String accountContactPrivacyDetails();
   String accountDashboard();
   String accountInfoBlock();
+  String accountLinkPanel();
   String accountName();
   String accountPassword();
   String accountUsername();
@@ -34,6 +35,7 @@
   String approvalhint();
   String approvalrole();
   String approvalscore();
+  String avatarInfoPanel();
   String blockHeader();
   String bottomheader();
   String cAPPROVAL();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java
index fc7ea53..098cc77 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java
@@ -51,4 +51,10 @@
 
   @Source("addFileComment.png")
   public ImageResource addFileComment();
+
+  @Source("diffy.png")
+  public ImageResource gerritAvatar();
+
+  @Source("draftComments.png")
+  public ImageResource draftComments();
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GitwebLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GitwebLink.java
index 5f62a52..ec97e58 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GitwebLink.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GitwebLink.java
@@ -35,6 +35,16 @@
     type = link.type;
   }
 
+  /**
+   * Can we link to a patch set if it's a draft
+   *
+   * @param ps Patch set to check draft status
+   * @return true if it's not a draft, or we can link to drafts
+   */
+  public boolean canLink(final PatchSet ps) {
+    return !ps.isDraft() || type.getLinkDrafts();
+  }
+
   public String getLinkName() {
     return "(" + type.getLinkName() + ")";
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/RelativeDateFormatter.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/RelativeDateFormatter.java
new file mode 100644
index 0000000..3298a06
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/RelativeDateFormatter.java
@@ -0,0 +1,109 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client;
+
+import com.google.gerrit.client.changes.Util;
+
+import java.util.Date;
+
+/**
+ * Formatter to format timestamps relative to the current time using time units
+ * in the format defined by {@code git log --relative-date}.
+ */
+public class RelativeDateFormatter {
+  final static long SECOND_IN_MILLIS = 1000;
+
+  final static long MINUTE_IN_MILLIS = 60 * SECOND_IN_MILLIS;
+
+  final static long HOUR_IN_MILLIS = 60 * MINUTE_IN_MILLIS;
+
+  final static long DAY_IN_MILLIS = 24 * HOUR_IN_MILLIS;
+
+  final static long WEEK_IN_MILLIS = 7 * DAY_IN_MILLIS;
+
+  final static long MONTH_IN_MILLIS = 30 * DAY_IN_MILLIS;
+
+  final static long YEAR_IN_MILLIS = 365 * DAY_IN_MILLIS;
+
+  /**
+   * @param when {@link Date} to format
+   * @return age of given {@link Date} compared to now formatted in the same
+   *         relative format as returned by {@code git log --relative-date}
+   */
+  @SuppressWarnings("boxing")
+  public static String format(Date when) {
+    long ageMillis = (new Date()).getTime() - when.getTime();
+
+    // shouldn't happen in a perfect world
+    if (ageMillis < 0) return Util.C.inTheFuture();
+
+    // seconds
+    if (ageMillis < upperLimit(MINUTE_IN_MILLIS)) {
+      return Util.M.secondsAgo(round(ageMillis, SECOND_IN_MILLIS));
+    }
+
+    // minutes
+    if (ageMillis < upperLimit(HOUR_IN_MILLIS)) {
+      return Util.M.minutesAgo(round(ageMillis, MINUTE_IN_MILLIS));
+    }
+
+    // hours
+    if (ageMillis < upperLimit(DAY_IN_MILLIS)) {
+      return Util.M.hoursAgo(round(ageMillis, HOUR_IN_MILLIS));
+    }
+
+    // up to 14 days use days
+    if (ageMillis < 14 * DAY_IN_MILLIS) {
+      return Util.M.daysAgo(round(ageMillis, DAY_IN_MILLIS));
+    }
+
+    // up to 10 weeks use weeks
+    if (ageMillis < 10 * WEEK_IN_MILLIS) {
+      return Util.M.weeksAgo(round(ageMillis, WEEK_IN_MILLIS));
+    }
+
+    // months
+    if (ageMillis < YEAR_IN_MILLIS) {
+      return Util.M.monthsAgo(round(ageMillis, MONTH_IN_MILLIS));
+    }
+
+    // up to 5 years use "year, months" rounded to months
+    if (ageMillis < 5 * YEAR_IN_MILLIS) {
+      long years = ageMillis / YEAR_IN_MILLIS;
+      String yearLabel = (years > 1) ? Util.C.years() : Util.C.year();
+      long months = round(ageMillis % YEAR_IN_MILLIS, MONTH_IN_MILLIS);
+      String monthLabel =
+          (months > 1) ? Util.C.months() : (months == 1 ? Util.C.month() : "");
+      if (months == 0) {
+        return Util.M.years0MonthsAgo(years, yearLabel);
+      } else {
+        return Util.M.yearsMonthsAgo(years, yearLabel, months, monthLabel);
+      }
+    }
+
+    // years
+    return Util.M.yearsAgo(round(ageMillis, YEAR_IN_MILLIS));
+  }
+
+  private static long upperLimit(long unit) {
+    long limit = unit + unit / 2;
+    return limit;
+  }
+
+  private static long round(long n, long unit) {
+    long rounded = (n + unit / 2) / unit;
+    return rounded;
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Themer.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Themer.java
new file mode 100644
index 0000000..a532209
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Themer.java
@@ -0,0 +1,84 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.package com.google.gerrit.server.git;
+
+package com.google.gerrit.client;
+
+import com.google.gerrit.client.projects.ThemeInfo;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.StyleElement;
+
+public class Themer {
+  public static class ThemerIE extends Themer {
+    protected ThemerIE() {
+    }
+
+    @Override
+    protected String getCssText(StyleElement el) {
+      return el.getCssText();
+    }
+
+    @Override
+    protected void setCssText(StyleElement el, String css) {
+      el.setCssText(css);
+    }
+  }
+
+  protected StyleElement cssElement;
+  protected Element headerElement;
+  protected Element footerElement;
+  protected String cssText;
+  protected String headerHtml;
+  protected String footerHtml;
+
+  protected Themer() {
+  }
+
+  public void set(ThemeInfo theme) {
+    if (theme != null) {
+      set(theme.css() != null ? theme.css() : cssText,
+          theme.header() != null ? theme.header() : headerHtml,
+          theme.footer() != null ? theme.footer() : footerHtml);
+    } else {
+      set(cssText, headerHtml, footerHtml);
+    }
+  }
+
+  public void clear() {
+    set(null);
+  }
+
+  void init(Element css, Element header, Element footer) {
+    cssElement = StyleElement.as(css);
+    headerElement = header;
+    footerElement = footer;
+
+    cssText = getCssText(this.cssElement);
+    headerHtml = header.getInnerHTML();
+    footerHtml = footer.getInnerHTML();
+  }
+
+  protected String getCssText(StyleElement el) {
+    return el.getInnerHTML();
+  }
+
+  protected void setCssText(StyleElement el, String css) {
+    el.setInnerHTML(css);
+  }
+
+  private void set(String css, String header, String footer) {
+    setCssText(cssElement, css);
+    headerElement.setInnerHTML(header);
+    footerElement.setInnerHTML(footer);
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/CurrentUserPopupPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java
similarity index 74%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/CurrentUserPopupPanel.java
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java
index 7983f9c..01811a6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/CurrentUserPopupPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.client;
 
+import com.google.gerrit.client.account.AccountInfo;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.uibinder.client.UiBinder;
 import com.google.gwt.uibinder.client.UiField;
@@ -24,8 +24,8 @@
 import com.google.gwt.user.client.ui.Widget;
 import com.google.gwtexpui.user.client.PluginSafePopupPanel;
 
-public class CurrentUserPopupPanel extends PluginSafePopupPanel {
-  interface Binder extends UiBinder<Widget, CurrentUserPopupPanel> {
+public class UserPopupPanel extends PluginSafePopupPanel {
+  interface Binder extends UiBinder<Widget, UserPopupPanel> {
   }
 
   private static final Binder binder = GWT.create(Binder.class);
@@ -41,9 +41,10 @@
   @UiField
   Anchor settings;
 
-  public CurrentUserPopupPanel(Account account, boolean canLogOut) {
+  public UserPopupPanel(AccountInfo account, boolean canLogOut,
+      boolean showSettingsLink) {
     super(/* auto hide */true, /* modal */false);
-    avatar = new AvatarImage(account.getId(), 100);
+    avatar = new AvatarImage(account, 100, false);
     setWidget(binder.createAndBindUi(this));
     // We must show and then hide this popup so that it is part of the DOM.
     // Otherwise the image does not get any events.  Calling hide() would
@@ -51,17 +52,21 @@
     show();
     setVisible(false);
     setStyleName(Gerrit.RESOURCES.css().userInfoPopup());
-    if (account.getFullName() != null) {
-      userName.setText(account.getFullName());
+    if (account.name() != null) {
+      userName.setText(account.name());
     }
-    if (account.getPreferredEmail() != null) {
-      userEmail.setText(account.getPreferredEmail());
+    if (account.email() != null) {
+      userEmail.setText(account.email());
     }
     if (canLogOut) {
       logout.setHref(Gerrit.selfRedirect("/logout"));
     } else {
       logout.setVisible(false);
     }
-    settings.setHref(Gerrit.selfRedirect(PageLinks.SETTINGS));
+    if (showSettingsLink) {
+      settings.setHref(Gerrit.selfRedirect(PageLinks.SETTINGS));
+    } else {
+      settings.setVisible(false);
+    }
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/CurrentUserPopupPanel.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.ui.xml
similarity index 100%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/CurrentUserPopupPanel.ui.xml
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.ui.xml
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 fa2c5fd..917c078 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
@@ -19,11 +19,13 @@
 public interface AccountConstants extends Constants {
   String settingsHeading();
 
+  String changeAvatar();
   String fullName();
   String preferredEmail();
   String registeredOn();
   String accountId();
 
+  String commentVisibilityLabel();
   String maximumPageSizeFieldLabel();
   String dateFormatLabel();
   String contextWholeFile();
@@ -33,6 +35,7 @@
   String reversePatchSetOrder();
   String showUsernameInReviewCategory();
   String buttonSaveChanges();
+  String showRelativeDateInChangeTable();
 
   String tabAccountSummary();
   String tabPreferences();
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 fd54363..7175b6a 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,5 +1,6 @@
 settingsHeading = Settings
 
+changeAvatar = Change Avatar
 fullName = Full Name
 preferredEmail = Email Address
 registeredOn = Registered
@@ -9,11 +10,12 @@
 copySelfOnEmails = CC Me On Comments I Write
 reversePatchSetOrder = Display Patch Sets In Reverse Order
 showUsernameInReviewCategory = Display Person Name In Review Category
-defaultContextFieldLabel = Default Context:
 maximumPageSizeFieldLabel = Maximum Page Size:
+commentVisibilityLabel = Comment Visibility:
 dateFormatLabel = Date/Time Format:
 contextWholeFile = Whole File
 buttonSaveChanges = Save Changes
+showRelativeDateInChangeTable = Show Relative Dates in Changes Table
 
 tabAccountSummary = Profile
 tabPreferences = Preferences
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
index c17a0aa..639a1cf 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.client.ui.OnEditEnabler;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.CommentVisibilityStrategy;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.i18n.client.DateTimeFormat;
@@ -42,9 +43,11 @@
   private CheckBox copySelfOnEmails;
   private CheckBox reversePatchSetOrder;
   private CheckBox showUsernameInReviewCategory;
+  private CheckBox relativeDateInChangeTable;
   private ListBox maximumPageSize;
   private ListBox dateFormat;
   private ListBox timeFormat;
+  private ListBox commentVisibilityStrategy;
   private Button save;
 
   @Override
@@ -61,6 +64,24 @@
       maximumPageSize.addItem(Util.M.rowsPerPage(v), String.valueOf(v));
     }
 
+    commentVisibilityStrategy = new ListBox();
+    commentVisibilityStrategy.addItem(
+        com.google.gerrit.client.changes.Util.C.messageCollapseAll(),
+        AccountGeneralPreferences.CommentVisibilityStrategy.COLLAPSE_ALL.name()
+    );
+    commentVisibilityStrategy.addItem(
+        com.google.gerrit.client.changes.Util.C.messageExpandMostRecent(),
+        AccountGeneralPreferences.CommentVisibilityStrategy.EXPAND_MOST_RECENT.name()
+    );
+    commentVisibilityStrategy.addItem(
+        com.google.gerrit.client.changes.Util.C.messageExpandRecent(),
+        AccountGeneralPreferences.CommentVisibilityStrategy.EXPAND_RECENT.name()
+    );
+    commentVisibilityStrategy.addItem(
+        com.google.gerrit.client.changes.Util.C.messageExpandAll(),
+        AccountGeneralPreferences.CommentVisibilityStrategy.EXPAND_ALL.name()
+    );
+
     Date now = new Date();
     dateFormat = new ListBox();
     for (AccountGeneralPreferences.DateFormat fmt : AccountGeneralPreferences.DateFormat
@@ -94,7 +115,10 @@
       dateTimePanel.add(dateFormat);
       dateTimePanel.add(timeFormat);
     }
-    final Grid formGrid = new Grid(7, 2);
+
+    relativeDateInChangeTable = new CheckBox(Util.C.showRelativeDateInChangeTable());
+
+    final Grid formGrid = new Grid(9, 2);
 
     int row = 0;
     formGrid.setText(row, labelIdx, "");
@@ -125,6 +149,14 @@
     formGrid.setWidget(row, fieldIdx, dateTimePanel);
     row++;
 
+    formGrid.setText(row, labelIdx, "");
+    formGrid.setWidget(row, fieldIdx, relativeDateInChangeTable);
+    row++;
+
+    formGrid.setText(row, labelIdx, Util.C.commentVisibilityLabel());
+    formGrid.setWidget(row, fieldIdx, commentVisibilityStrategy);
+    row++;
+
     add(formGrid);
 
     save = new Button(Util.C.buttonSaveChanges());
@@ -146,6 +178,8 @@
     e.listenTo(maximumPageSize);
     e.listenTo(dateFormat);
     e.listenTo(timeFormat);
+    e.listenTo(relativeDateInChangeTable);
+    e.listenTo(commentVisibilityStrategy);
   }
 
   @Override
@@ -167,6 +201,8 @@
     maximumPageSize.setEnabled(on);
     dateFormat.setEnabled(on);
     timeFormat.setEnabled(on);
+    relativeDateInChangeTable.setEnabled(on);
+    commentVisibilityStrategy.setEnabled(on);
   }
 
   private void display(final AccountGeneralPreferences p) {
@@ -180,6 +216,10 @@
         p.getDateFormat());
     setListBox(timeFormat, AccountGeneralPreferences.TimeFormat.HHMM_12, //
         p.getTimeFormat());
+    relativeDateInChangeTable.setValue(p.isRelativeDateInChangeTable());
+    setListBox(commentVisibilityStrategy,
+        AccountGeneralPreferences.CommentVisibilityStrategy.EXPAND_MOST_RECENT,
+        p.getCommentVisibilityStrategy());
   }
 
   private void setListBox(final ListBox f, final short defaultValue,
@@ -243,6 +283,10 @@
     p.setTimeFormat(getListBox(timeFormat,
         AccountGeneralPreferences.TimeFormat.HHMM_12,
         AccountGeneralPreferences.TimeFormat.values()));
+    p.setRelativeDateInChangeTable(relativeDateInChangeTable.getValue());
+    p.setCommentVisibilityStrategy(getListBox(commentVisibilityStrategy,
+        CommentVisibilityStrategy.EXPAND_MOST_RECENT,
+        CommentVisibilityStrategy.values()));
 
     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
index ae04e0a..01d6e3c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyProfileScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyProfileScreen.java
@@ -16,13 +16,23 @@
 
 import static com.google.gerrit.client.FormatUtil.mediumFormat;
 
+import com.google.gerrit.client.AvatarImage;
+import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.rpc.NativeString;
+import com.google.gerrit.client.rpc.RestApi;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gwt.i18n.client.LocaleInfo;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.Grid;
 import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
+import com.google.gwt.user.client.ui.HorizontalPanel;
+import com.google.gwt.user.client.ui.VerticalPanel;
 
 public class MyProfileScreen extends SettingsScreen {
+  private AvatarImage avatar;
+  private Anchor changeAvatar;
   private int labelIdx, fieldIdx;
   private Grid info;
 
@@ -30,6 +40,18 @@
   protected void onInitUI() {
     super.onInitUI();
 
+    HorizontalPanel h = new HorizontalPanel();
+    add(h);
+
+    VerticalPanel v = new VerticalPanel();
+    v.addStyleName(Gerrit.RESOURCES.css().avatarInfoPanel());
+    h.add(v);
+    avatar = new AvatarImage();
+    v.add(avatar);
+    changeAvatar = new Anchor(Util.C.changeAvatar(), "", "_blank");
+    changeAvatar.setVisible(false);
+    v.add(changeAvatar);
+
     if (LocaleInfo.getCurrentLocale().isRTL()) {
       labelIdx = 1;
       fieldIdx = 0;
@@ -41,7 +63,7 @@
     info = new Grid((Gerrit.getConfig().siteHasUsernames() ? 1 : 0) + 4, 2);
     info.setStyleName(Gerrit.RESOURCES.css().infoBlock());
     info.addStyleName(Gerrit.RESOURCES.css().accountInfoBlock());
-    add(info);
+    h.add(info);
 
     int row = 0;
     if (Gerrit.getConfig().siteHasUsernames()) {
@@ -72,6 +94,20 @@
   }
 
   void display(final Account account) {
+    avatar.setAccount(FormatUtil.asInfo(account), 93, false);
+    new RestApi("/accounts/").id("self").view("avatar.change.url")
+        .get(new AsyncCallback<NativeString>() {
+          @Override
+          public void onSuccess(NativeString changeUrl) {
+            changeAvatar.setHref(changeUrl.asString());
+            changeAvatar.setVisible(true);
+          }
+
+          @Override
+          public void onFailure(Throwable caught) {
+          }
+        });
+
     int row = 0;
     if (Gerrit.getConfig().siteHasUsernames()) {
       info.setWidget(row++, fieldIdx, new UsernameField());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
index c276a89..51ff978 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
@@ -23,7 +23,7 @@
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.AccountGroupSuggestOracle;
-import com.google.gerrit.client.ui.AccountLink;
+import com.google.gerrit.client.ui.AccountLinkPanel;
 import com.google.gerrit.client.ui.AddMemberBox;
 import com.google.gerrit.client.ui.FancyFlexTable;
 import com.google.gerrit.client.ui.Hyperlink;
@@ -318,7 +318,7 @@
       CheckBox checkBox = new CheckBox();
       table.setWidget(row, 1, checkBox);
       checkBox.setEnabled(enabled);
-      table.setWidget(row, 2, new AccountLink(i));
+      table.setWidget(row, 2, new AccountLinkPanel(i));
       table.setText(row, 3, i.email());
 
       final FlexCellFormatter fmt = table.getFlexCellFormatter();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
index bf73847..178db6b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
@@ -158,6 +158,7 @@
   queryLimit, \
   runGC, \
   startReplication, \
+  streamEvents, \
   viewCaches, \
   viewConnections, \
   viewQueue
@@ -173,6 +174,7 @@
 queryLimit = Query Limit
 runGC = Run Garbage Collection
 startReplication = Start Replication
+streamEvents = Stream Events
 viewCaches = View Caches
 viewConnections = View Connections
 viewQueue = View Queue
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java
index deb867c..08877d9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java
@@ -27,7 +27,7 @@
 import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.client.rpc.NativeString;
 import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.ui.AccountLink;
+import com.google.gerrit.client.ui.AccountLinkPanel;
 import com.google.gerrit.client.ui.AddMemberBox;
 import com.google.gerrit.client.ui.ReviewerSuggestOracle;
 import com.google.gerrit.common.data.ApprovalDetail;
@@ -329,7 +329,7 @@
     final CellFormatter fmt = table.getCellFormatter();
     int col = 0;
 
-    table.setWidget(row, col++, new AccountLink(account));
+    table.setWidget(row, col++, new AccountLinkPanel(account));
     rows.put(account._account_id(), row);
 
     if (ad.canRemove()) {
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 500f06e..968c726 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
@@ -113,6 +113,7 @@
   String includedInTableTag();
 
   String messageNoAuthor();
+  String messageExpandMostRecent();
   String messageExpandRecent();
   String messageExpandAll();
   String messageCollapseAll();
@@ -122,6 +123,7 @@
   String patchSetInfoCommitter();
   String patchSetInfoDownload();
   String patchSetInfoParents();
+  String patchSetWithDraftCommentsToolTip();
   String initialCommit();
 
   String buttonRebaseChange();
@@ -171,4 +173,10 @@
 
   String diffAllSideBySide();
   String diffAllUnified();
+
+  String inTheFuture();
+  String month();
+  String months();
+  String year();
+  String years();
 }
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 06bd64d..4c12378 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
@@ -90,6 +90,7 @@
 includedInTableTag = Tag Name
 
 messageNoAuthor = Gerrit Code Review
+messageExpandMostRecent = Expand Most Recent
 messageExpandRecent = Expand Recent
 messageExpandAll = Expand All
 messageCollapseAll = Collapse All
@@ -99,6 +100,7 @@
 patchSetInfoCommitter = Committer
 patchSetInfoDownload = Download
 patchSetInfoParents = Parent(s)
+patchSetWithDraftCommentsToolTip = Draft comment(s) inside
 initialCommit = Initial Commit
 
 buttonAbandonChangeBegin = Abandon Change
@@ -152,3 +154,9 @@
 
 diffAllSideBySide = All Side-by-Side
 diffAllUnified = All Unified
+
+inTheFuture = in the future
+month = month
+months = months
+years = years
+year = year
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 8395907..2fcb7e8 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,6 +14,7 @@
 
 package com.google.gerrit.client.changes;
 
+import com.google.gerrit.client.ui.CommentLinkProcessor;
 import com.google.gerrit.common.data.AccountInfoCache;
 import com.google.gerrit.common.data.ChangeDetail;
 import com.google.gerrit.common.data.SubmitTypeRecord;
@@ -37,10 +38,11 @@
   }
 
   public void display(ChangeDetail chg, Boolean starred, Boolean canEditCommitMessage,
-      PatchSetInfo info,
-      final AccountInfoCache acc, SubmitTypeRecord submitTypeRecord) {
+      PatchSetInfo info, AccountInfoCache acc,
+      SubmitTypeRecord submitTypeRecord,
+      CommentLinkProcessor commentLinkProcessor) {
     infoBlock.display(chg, acc, submitTypeRecord);
     messageBlock.display(chg.getChange().currentPatchSetId(), starred,
-      canEditCommitMessage,  info.getMessage());
+        canEditCommitMessage, info.getMessage(), commentLinkProcessor);
   }
 }
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 afc17f7..b4ae2f3 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,7 +18,7 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.AccountLink;
+import com.google.gerrit.client.ui.AccountLinkPanel;
 import com.google.gerrit.client.ui.BranchLink;
 import com.google.gerrit.client.ui.CommentedActionDialog;
 import com.google.gerrit.client.ui.InlineHyperlink;
@@ -105,7 +105,7 @@
     changeIdLabel.setPreviewText(chg.getKey().get());
     table.setWidget(R_CHANGE_ID, 1, changeIdLabel);
 
-    table.setWidget(R_OWNER, 1, AccountLink.link(acc, chg.getOwner()));
+    table.setWidget(R_OWNER, 1, AccountLinkPanel.link(acc, chg.getOwner()));
 
     final FlowPanel p = new FlowPanel();
     p.add(new ProjectSearchLink(chg.getProject()));
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
index ba46702..098fe07 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
@@ -56,4 +56,15 @@
   String groupIsNotAllowed(String group);
   String groupHasTooManyMembers(String group);
   String groupManyMembersConfirmation(String group, int memberCount);
+
+  String secondsAgo(long seconds);
+  String minutesAgo(long minutes);
+  String hoursAgo(long hours);
+  String daysAgo(long days);
+  String weeksAgo(long weeks);
+  String monthsAgo(long months);
+  String yearsAgo(long years);
+  String years0MonthsAgo(long years, String yearLabel);
+  String yearsMonthsAgo(long years, String yearLabel, long months,
+      String monthLabel);
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
index caaf3bf..e02c27c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
@@ -39,3 +39,13 @@
 groupIsNotAllowed =  The group {0} cannot be added as reviewer.
 groupHasTooManyMembers = The group {0} has too many members to add them all as reviewers.
 groupManyMembersConfirmation = The group {0} has {1} members. Do you want to add them all as reviewers?
+
+secondsAgo = {0} seconds ago
+minutesAgo = {0} minutes ago
+hoursAgo = {0} hours ago
+daysAgo = {0} days ago
+weeksAgo = {0} weeks ago
+monthsAgo = {0} months ago
+years0MonthsAgo = {0} {1} ago
+yearsMonthsAgo = {0} {1}, {2} {3} ago
+yearsAgo = {0} years ago
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 91c8856..c69f915 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,10 @@
 import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.account.AccountInfo;
+import com.google.gerrit.client.projects.ConfigInfoCache;
+import com.google.gerrit.client.rpc.CallbackGroup;
 import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.CommentLinkProcessor;
 import com.google.gerrit.client.ui.CommentPanel;
 import com.google.gerrit.client.ui.ComplexDisclosurePanel;
 import com.google.gerrit.client.ui.ExpandAllCommand;
@@ -28,6 +31,7 @@
 import com.google.gerrit.common.data.AccountInfoCache;
 import com.google.gerrit.common.data.ChangeDetail;
 import com.google.gerrit.common.data.ChangeInfo;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.CommentVisibilityStrategy;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -78,6 +82,7 @@
   private PatchSetsBlock patchSetsBlock;
 
   private Panel comments;
+  private CommentLinkProcessor commentLinkProcessor;
 
   private KeyCommandSet keysNavigation;
   private KeyCommandSet keysAction;
@@ -264,10 +269,26 @@
   @Override
   public void onValueChange(final ValueChangeEvent<ChangeDetail> event) {
     if (isAttached() && isLastValueChangeHandler()) {
-      // Until this screen is fully migrated to the new API, this call must be
-      // sequential, because we can't start an async get at the source of every
-      // call that might trigger a value change.
-      ChangeApi.detail(event.getValue().getChange().getId().get(),
+      // Until this screen is fully migrated to the new API, these calls must
+      // happen sequentially after the ChangeDetail lookup, because we can't
+      // start an async get at the source of every call that might trigger a
+      // value change.
+      CallbackGroup cbs = new CallbackGroup();
+      ConfigInfoCache.get(
+          event.getValue().getChange().getProject(),
+          cbs.add(new GerritCallback<ConfigInfoCache.Entry>() {
+            @Override
+            public void onSuccess(ConfigInfoCache.Entry result) {
+              commentLinkProcessor = result.getCommentLinkProcessor();
+              setTheme(result.getTheme());
+            }
+
+            @Override
+            public void onFailure(Throwable caught) {
+              // Handled by last callback's onFailure.
+            }
+          }));
+      ChangeApi.detail(event.getValue().getChange().getId().get(), cbs.add(
           new GerritCallback<com.google.gerrit.client.changes.ChangeInfo>() {
             @Override
             public void onSuccess(
@@ -275,7 +296,7 @@
               changeInfo = result;
               display(event.getValue());
             }
-          });
+          }));
     }
   }
 
@@ -305,7 +326,8 @@
         detail.isStarred(),
         detail.canEditCommitMessage(),
         detail.getCurrentPatchSetDetail().getInfo(),
-        detail.getAccounts(), detail.getSubmitTypeRecord());
+        detail.getAccounts(), detail.getSubmitTypeRecord(),
+        commentLinkProcessor);
     dependsOn.display(detail.getDependsOn());
     neededBy.display(detail.getNeededBy());
     approvals.display(changeInfo);
@@ -399,6 +421,13 @@
     final long AGE = 7 * 24 * 60 * 60 * 1000L;
     final Timestamp aged = new Timestamp(System.currentTimeMillis() - AGE);
 
+    CommentVisibilityStrategy commentVisibilityStrategy =
+        CommentVisibilityStrategy.EXPAND_MOST_RECENT;
+    if (Gerrit.isSignedIn()) {
+      commentVisibilityStrategy = Gerrit.getUserAccount()
+          .getGeneralPreferences().getCommentVisibilityStrategy();
+    }
+
     for (int i = 0; i < msgList.size(); i++) {
       final ChangeMessage msg = msgList.get(i);
 
@@ -417,14 +446,29 @@
         isRecent = msg.getWrittenOn().after(aged);
       }
 
-      final CommentPanel cp =
-          new CommentPanel(author, msg.getWrittenOn(), msg.getMessage());
+      final CommentPanel cp = new CommentPanel(author, msg.getWrittenOn(),
+          msg.getMessage(), commentLinkProcessor);
       cp.setRecent(isRecent);
       cp.addStyleName(Gerrit.RESOURCES.css().commentPanelBorder());
       if (i == msgList.size() - 1) {
         cp.addStyleName(Gerrit.RESOURCES.css().commentPanelLast());
-        cp.setOpen(true);
       }
+      boolean isOpen = false;
+      switch (commentVisibilityStrategy) {
+        case COLLAPSE_ALL:
+          break;
+        case EXPAND_RECENT:
+          isOpen = isRecent;
+          break;
+        case EXPAND_ALL:
+          isOpen = true;
+          break;
+        case EXPAND_MOST_RECENT:
+        default:
+          isOpen = i == msgList.size() - 1;
+          break;
+      }
+      cp.setOpen(isOpen);
       comments.add(cp);
     }
 
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 97a5a09..3a65d20 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
@@ -17,7 +17,7 @@
 import static com.google.gerrit.client.FormatUtil.shortFormat;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.ui.AccountLink;
+import com.google.gerrit.client.ui.AccountLinkPanel;
 import com.google.gerrit.client.ui.BranchLink;
 import com.google.gerrit.client.ui.ChangeLink;
 import com.google.gerrit.client.ui.NavigationTable;
@@ -182,8 +182,8 @@
     }
   }
 
-  private AccountLink link(final Account.Id id) {
-    return AccountLink.link(accountCache, id);
+  private AccountLinkPanel link(final Account.Id id) {
+    return AccountLinkPanel.link(accountCache, id);
   }
 
   public void addSection(final Section s) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java
index 03cc11d..4694272 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java
@@ -14,11 +14,12 @@
 
 package com.google.gerrit.client.changes;
 
+import static com.google.gerrit.client.FormatUtil.relativeFormat;
 import static com.google.gerrit.client.FormatUtil.shortFormat;
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
-import com.google.gerrit.client.ui.AccountLink;
+import com.google.gerrit.client.ui.AccountLinkPanel;
 import com.google.gerrit.client.ui.BranchLink;
 import com.google.gerrit.client.ui.ChangeLink;
 import com.google.gerrit.client.ui.NavigationTable;
@@ -197,7 +198,7 @@
     table.setWidget(row, C_SUBJECT, new TableChangeLink(subject, c));
 
     if (c.owner() != null) {
-      table.setWidget(row, C_OWNER, new AccountLink(c.owner(), status));
+      table.setWidget(row, C_OWNER, new AccountLinkPanel(c.owner(), status));
     } else {
       table.setText(row, C_OWNER, "");
     }
@@ -206,7 +207,13 @@
         row, C_PROJECT, new ProjectLink(c.project_name_key(), c.status()));
     table.setWidget(row, C_BRANCH, new BranchLink(c.project_name_key(), c
         .status(), c.branch(), c.topic()));
-    table.setText(row, C_LAST_UPDATE, shortFormat(c.updated()));
+    if (Gerrit.isSignedIn()
+        && Gerrit.getUserAccount().getGeneralPreferences()
+            .isRelativeDateInChangeTable()) {
+      table.setText(row, C_LAST_UPDATE, relativeFormat(c.updated()));
+    } else {
+      table.setText(row, C_LAST_UPDATE, shortFormat(c.updated()));
+    }
 
     boolean displayName = Gerrit.isSignedIn() && Gerrit.getUserAccount()
         .getGeneralPreferences().isShowUsernameInReviewCategory();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.java
index ea184df..198480e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.java
@@ -68,8 +68,9 @@
     initWidget(uiBinder.createAndBindUi(this));
   }
 
-  public void display(final String commitMessage) {
-    display(null, null, false, commitMessage);
+  public void display(String commitMessage,
+      CommentLinkProcessor commentLinkProcessor) {
+    display(null, null, false, commitMessage, commentLinkProcessor);
   }
 
   private abstract class CommitMessageEditDialog extends CommentedActionDialog<ChangeDetail> {
@@ -103,7 +104,8 @@
   }
 
   public void display(final PatchSet.Id patchSetId,
-      Boolean starred, Boolean canEditCommitMessage, final String commitMessage) {
+      Boolean starred, Boolean canEditCommitMessage, final String commitMessage,
+      CommentLinkProcessor commentLinkProcessor) {
     starPanel.clear();
     if (patchSetId != null && starred != null && Gerrit.isSignedIn()) {
       Change.Id changeId = patchSetId.getParentKey();
@@ -170,7 +172,7 @@
     // Linkify commit summary
     SafeHtml commitSummaryLinkified = new SafeHtmlBuilder().append(commitSummary);
     commitSummaryLinkified = commitSummaryLinkified.linkify();
-    commitSummaryLinkified = CommentLinkProcessor.apply(commitSummaryLinkified);
+    commitSummaryLinkified = commentLinkProcessor.apply(commitSummaryLinkified);
     commitSummaryPre.setInnerHTML(commitSummaryLinkified.asString());
 
     // Hide commit body if there is no body
@@ -180,7 +182,7 @@
       // Linkify commit body
       SafeHtml commitBodyLinkified = new SafeHtmlBuilder().append(commitBody);
       commitBodyLinkified = commitBodyLinkified.linkify();
-      commitBodyLinkified = CommentLinkProcessor.apply(commitBodyLinkified);
+      commitBodyLinkified = commentLinkProcessor.apply(commitBodyLinkified);
       commitBodyLinkified = commitBodyLinkified.replaceAll("\n\n", "<p></p>");
       commitBodyLinkified = commitBodyLinkified.replaceAll("\n", "<br />");
       commitBodyPre.setInnerHTML(commitBodyLinkified.asString());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java
index 76f77a2..ca326cd 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java
@@ -21,7 +21,7 @@
 import com.google.gerrit.client.download.DownloadPanel;
 import com.google.gerrit.client.patches.PatchUtil;
 import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.AccountLink;
+import com.google.gerrit.client.ui.AccountLinkPanel;
 import com.google.gerrit.client.ui.CommentedActionDialog;
 import com.google.gerrit.client.ui.ComplexDisclosurePanel;
 import com.google.gerrit.client.ui.ListenableAccountDiffPreference;
@@ -45,6 +45,7 @@
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.FocusWidget;
 import com.google.gwt.user.client.ui.Grid;
+import com.google.gwt.user.client.ui.Image;
 import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
 import com.google.gwt.user.client.ui.InlineLabel;
 import com.google.gwt.user.client.ui.Panel;
@@ -78,7 +79,8 @@
    * Creates a closed complex disclosure panel for a patch set.
    * The patch set details are loaded when the complex disclosure panel is opened.
    */
-  public PatchSetComplexDisclosurePanel(final PatchSet ps, boolean isOpen) {
+  public PatchSetComplexDisclosurePanel(final PatchSet ps, boolean isOpen,
+      boolean hasDraftComments) {
     super(Util.M.patchSetHeader(ps.getPatchSetId()), isOpen);
     detailCache = ChangeCache.get(ps.getId().getParentKey()).getChangeDetailCache();
     changeDetail = detailCache.get();
@@ -87,11 +89,17 @@
     body = new FlowPanel();
     setContent(body);
 
+    if (hasDraftComments) {
+      final Image draftComments = new Image(Gerrit.RESOURCES.draftComments());
+      draftComments.setTitle(Util.C.patchSetWithDraftCommentsToolTip());
+      getHeader().add(draftComments);
+    }
+
     final GitwebLink gw = Gerrit.getGitwebLink();
     final InlineLabel revtxt = new InlineLabel(ps.getRevision().get() + " ");
     revtxt.addStyleName(Gerrit.RESOURCES.css().patchSetRevision());
     getHeader().add(revtxt);
-    if (gw != null) {
+    if (gw != null && gw.canLink(ps)) {
       final Anchor revlink =
           new Anchor(gw.getLinkName(), false, gw.toRevision(changeDetail.getChange()
               .getProject(), ps));
@@ -110,6 +118,7 @@
     } else {
       addOpenHandler(this);
     }
+
   }
 
   public void setDiffBaseId(PatchSet.Id diffBaseId) {
@@ -250,7 +259,7 @@
     fp.setStyleName(Gerrit.RESOURCES.css().patchSetUserIdentity());
     if (who.getName() != null) {
       if (who.getAccount() != null) {
-        fp.add(new AccountLink(who));
+        fp.add(new AccountLinkPanel(who));
       } else {
         final InlineLabel lbl = new InlineLabel(who.getName());
         lbl.setStyleName(Gerrit.RESOURCES.css().accountName());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetsBlock.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetsBlock.java
index 5a6e427..4805185 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetsBlock.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetsBlock.java
@@ -88,7 +88,8 @@
 
     for (final PatchSet ps : patchSets) {
       final PatchSetComplexDisclosurePanel p =
-          new PatchSetComplexDisclosurePanel(ps, ps == currps);
+          new PatchSetComplexDisclosurePanel(ps, ps == currps,
+              detail.hasDraftComments(ps.getId()));
       if (diffBaseId != null) {
         p.setDiffBaseId(diffBaseId);
         if (ps == currps) {
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 b1fb5fe..d6c1de1 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
@@ -48,7 +48,9 @@
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 public class PatchTable extends Composite {
   public interface PatchValidator {
@@ -80,6 +82,7 @@
   private String savePointerId;
   private PatchSet.Id base;
   private List<Patch> patchList;
+  private Map<Patch.Key, Integer> patchMap;
   private ListenableAccountDiffPreference listenablePrefs;
 
   private List<ClickHandler> clickHandlers;
@@ -97,18 +100,25 @@
   }
 
   public int indexOf(Patch.Key patch) {
-    for (int i = 0; i < patchList.size(); i++) {
-      if (patchList.get(i).getKey().equals(patch)) {
-        return i;
+    Integer i = patchMap().get(patch);
+    return i != null ? i : -1;
+  }
+
+  private Map<Key, Integer> patchMap() {
+    if (patchMap == null) {
+      patchMap = new HashMap<Patch.Key, Integer>();
+      for (int i = 0; i < patchList.size(); i++) {
+        patchMap.put(patchList.get(i).getKey(), i);
       }
     }
-    return -1;
+    return patchMap;
   }
 
   public void display(PatchSet.Id base, PatchSetDetail detail) {
     this.base = base;
     this.detail = detail;
     this.patchList = detail.getPatches();
+    this.patchMap = null;
     myTable = null;
 
     final DisplayCommand cmd = new DisplayCommand(patchList, base);
@@ -328,36 +338,33 @@
     }
 
     void updateReviewedStatus(final Patch.Key patchKey, boolean reviewed) {
-      final int row = findRow(patchKey);
-      if (0 <= row) {
-        final Patch patch = getRowItem(row);
-        if (patch != null) {
-          patch.setReviewedByCurrentUser(reviewed);
-
+      int idx = patchMap().get(patchKey);
+      if (0 <= idx) {
+        Patch patch = patchList.get(idx);
+        if (patch.isReviewedByCurrentUser() != reviewed) {
+          int row = idx + 1;
           int col = C_SIDEBYSIDE + 2;
           if (patch.getPatchType() == Patch.PatchType.BINARY) {
             col = C_SIDEBYSIDE + 3;
           }
-
           if (reviewed) {
             table.setWidget(row, col, new Image(Gerrit.RESOURCES.greenCheck()));
           } else {
             table.clearCell(row, col);
           }
+          patch.setReviewedByCurrentUser(reviewed);
         }
       }
     }
 
     void notifyDraftDelta(final Patch.Key key, final int delta) {
-      final int row = findRow(key);
-      if (0 <= row) {
-        final Patch p = getRowItem(row);
-        if (p != null) {
-          p.setDraftCount(p.getDraftCount() + delta);
-          final SafeHtmlBuilder m = new SafeHtmlBuilder();
-          appendCommentCount(m, p);
-          SafeHtml.set(table, row, C_DRAFT, m);
-        }
+      int idx = patchMap().get(key);
+      if (0 <= idx) {
+        Patch p = patchList.get(idx);
+        p.setDraftCount(p.getDraftCount() + delta);
+        SafeHtmlBuilder m = new SafeHtmlBuilder();
+        appendCommentCount(m, p);
+        SafeHtml.set(table, idx + 1, C_DRAFT, m);
       }
     }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java
index 7f61977..4e710db 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
@@ -20,6 +20,7 @@
 import com.google.gerrit.client.patches.AbstractPatchContentTable;
 import com.google.gerrit.client.patches.CommentEditorContainer;
 import com.google.gerrit.client.patches.CommentEditorPanel;
+import com.google.gerrit.client.projects.ConfigInfoCache;
 import com.google.gerrit.client.rpc.CallbackGroup;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.Natives;
@@ -79,6 +80,7 @@
   private boolean saveStateOnUnload = true;
   private List<CommentEditorPanel> commentEditors;
   private ChangeInfo change;
+  private CommentLinkProcessor commentLinkProcessor;
 
   public PublishCommentScreen(final PatchSet.Id psi) {
     patchSetId = psi;
@@ -149,8 +151,8 @@
     super.onLoad();
 
     CallbackGroup cbs = new CallbackGroup();
-    ChangeApi.revision(patchSetId).view("review").get(cbs.add(
-        new AsyncCallback<ChangeInfo>() {
+    ChangeApi.revision(patchSetId).view("review")
+        .get(cbs.add(new AsyncCallback<ChangeInfo>() {
           @Override
           public void onSuccess(ChangeInfo result) {
             result.init();
@@ -167,7 +169,7 @@
           @Override
           protected void preDisplay(final PatchSetPublishDetail result) {
             send.setEnabled(true);
-            display(result);
+            PublishCommentScreen.this.preDisplay(result, this);
           }
 
           @Override
@@ -177,6 +179,24 @@
         }));
   }
 
+  private void preDisplay(final PatchSetPublishDetail pubDetail,
+      final ScreenLoadCallback<PatchSetPublishDetail> origCb) {
+    ConfigInfoCache.get(pubDetail.getChange().getProject(),
+        new AsyncCallback<ConfigInfoCache.Entry>() {
+          @Override
+          public void onSuccess(ConfigInfoCache.Entry result) {
+            commentLinkProcessor = result.getCommentLinkProcessor();
+            setTheme(result.getTheme());
+            display(pubDetail);
+          }
+
+          @Override
+          public void onFailure(Throwable caught) {
+            origCb.onFailure(caught);
+          }
+        });
+  }
+
   @Override
   protected void onUnload() {
     super.onUnload();
@@ -282,7 +302,7 @@
     for (String value : values) {
       ValueRadioButton b = new ValueRadioButton(label, value);
       SafeHtml buf = new SafeHtmlBuilder().append(b.format());
-      buf = CommentLinkProcessor.apply(buf);
+      buf = commentLinkProcessor.apply(buf);
       SafeHtml.set(b, buf);
 
       if (lastState != null && patchSetId.equals(lastState.patchSetId)
@@ -305,7 +325,7 @@
     setPageTitle(Util.M.publishComments(r.getChange().getKey().abbreviate(),
         patchSetId.get()));
     descBlock.display(changeDetail, null, false, r.getPatchSetInfo(), r.getAccounts(),
-        r.getSubmitTypeRecord());
+       r.getSubmitTypeRecord(), commentLinkProcessor);
 
     if (r.getChange().getStatus().isOpen()) {
       initApprovals(approvalPanel);
@@ -341,11 +361,14 @@
           priorFile = fn;
         }
 
-        final CommentEditorPanel editor = new CommentEditorPanel(c);
+        final CommentEditorPanel editor =
+            new CommentEditorPanel(c, commentLinkProcessor);
         if (c.getLine() == AbstractPatchContentTable.R_HEAD) {
-          editor.setAuthorNameText(Util.C.fileCommentHeader());
+          editor.setAuthorNameText(Gerrit.getUserAccountInfo(),
+              Util.C.fileCommentHeader());
         } else {
-          editor.setAuthorNameText(Util.M.lineHeader(c.getLine()));
+          editor.setAuthorNameText(Gerrit.getUserAccountInfo(),
+              Util.M.lineHeader(c.getLine()));
         }
         editor.setOpen(true);
         commentEditors.add(editor);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diffy.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diffy.png
new file mode 100644
index 0000000..4be4541
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diffy.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/draftComments.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/draftComments.png
new file mode 100644
index 0000000..31c770f
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/draftComments.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
index 9425443..5f16af6 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
@@ -20,7 +20,7 @@
 
 @def black #000000;
 @def white #ffffff;
-@def norm-font  Arial Unicode MS, Arial, sans-serif;
+@def norm-font  sans-serif;
 @def mono-font  monospace;
 
 @eval backgroundColor com.google.gerrit.client.Gerrit.getTheme().backgroundColor;
@@ -86,6 +86,21 @@
   text-decoration: underline;
 }
 
+.accountLinkPanel {
+  display: inline;
+}
+
+.accountLinkPanel img {
+  margin-right: 0.2em;
+  position: relative;
+  top: 2px;
+}
+
+.accountLinkPanel a {
+  position: relative;
+  top: -1px;
+}
+
 .accountName {
   white-space: nowrap;
 }
@@ -922,7 +937,6 @@
 }
 
 .sideBySideTableBinaryHeader {
-  border-right: thin solid #b0bdcc;
   border-left:  thin solid #b0bdcc;
   width: 100%;
   color: grey;
@@ -967,6 +981,13 @@
   float: left;
 }
 
+.avatarInfoPanel {
+  margin-right: 10px;
+}
+.avatarInfoPanel td {
+  text-align: center;
+}
+
 .infoBlock {
   border-collapse: collapse;
   border-spacing: 0;
@@ -1160,7 +1181,7 @@
   margin-right: 5em;
   font-weight: bold;
   font-size: medium;
-  font-family: Arial Unicode;
+  font-family: norm-font;
 }
 
 /** Patch History Table **/
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java
index 3b4abe5..f529990 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java
@@ -26,7 +26,7 @@
   }
 
   public final AccountGroup.UUID getGroupUUID() {
-    return new AccountGroup.UUID(URL.decodePathSegment(id()));
+    return new AccountGroup.UUID(URL.decodeQueryString(id()));
   }
 
   public final native String id() /*-{ return this.id; }-*/;
@@ -46,13 +46,13 @@
   public final AccountGroup.UUID getOwnerUUID() {
     String owner = owner_id();
     if (owner != null) {
-        return new AccountGroup.UUID(URL.decodePathSegment(owner));
+        return new AccountGroup.UUID(URL.decodeQueryString(owner));
     }
     return null;
   }
 
   public final void setOwnerUUID(AccountGroup.UUID uuid) {
-    owner_id(URL.encodePathSegment(uuid.get()));
+    owner_id(URL.encodeQueryString(uuid.get()));
   }
 
   protected GroupInfo() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java
index 12f8e62..c07783d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.client.changes.PatchTable;
 import com.google.gerrit.client.changes.Util;
 import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.CommentLinkProcessor;
 import com.google.gerrit.client.ui.CommentPanel;
 import com.google.gerrit.client.ui.NavigationTable;
 import com.google.gerrit.client.ui.NeedsSignInKeyCommand;
@@ -29,13 +30,14 @@
 import com.google.gerrit.common.data.PatchScript;
 import com.google.gerrit.common.data.PatchSetDetail;
 import com.google.gerrit.prettify.client.ClientSideFormatter;
-import com.google.gerrit.prettify.common.PrettyFormatter;
+import com.google.gerrit.prettify.client.PrettyFormatter;
+import com.google.gerrit.prettify.client.SparseHtmlFile;
 import com.google.gerrit.prettify.common.SparseFileContent;
-import com.google.gerrit.prettify.common.SparseHtmlFile;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.GWT;
 import com.google.gwt.event.dom.client.BlurEvent;
 import com.google.gwt.event.dom.client.BlurHandler;
 import com.google.gwt.event.dom.client.ClickEvent;
@@ -60,6 +62,7 @@
 import com.google.gwtexpui.globalkey.client.KeyCommand;
 import com.google.gwtexpui.globalkey.client.KeyCommandSet;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+import com.google.gwtorm.client.KeyUtil;
 
 import org.eclipse.jgit.diff.Edit;
 
@@ -86,6 +89,7 @@
   private HandlerRegistration regComment;
   private final KeyCommandSet keysOpenByEnter;
   private HandlerRegistration regOpenByEnter;
+  private CommentLinkProcessor commentLinkProcessor;
   boolean isDisplayBinary;
 
   protected AbstractPatchContentTable() {
@@ -241,6 +245,10 @@
     render(s, d);
   }
 
+  void setCommentLinkProcessor(CommentLinkProcessor commentLinkProcessor) {
+    this.commentLinkProcessor = commentLinkProcessor;
+  }
+
   protected boolean hasDifferences(PatchScript script) {
     return hasEdits(script) || hasMeta(script) || hasComments(script);
   }
@@ -312,6 +320,23 @@
     return f;
   }
 
+  protected String getUrlA() {
+    final String rawBase = GWT.getHostPageBaseURL() + "cat/";
+    final String url;
+    if (idSideA == null) {
+      url = rawBase + KeyUtil.encode(patchKey.toString()) + "^1";
+    } else {
+      Patch.Key k = new Patch.Key(idSideA, patchKey.get());
+      url = rawBase + KeyUtil.encode(k.toString()) + "^0";
+    }
+    return url;
+  }
+
+  protected String getUrlB() {
+    final String rawBase = GWT.getHostPageBaseURL() + "cat/";
+    return rawBase + KeyUtil.encode(patchKey.toString()) + "^0";
+  }
+
   protected abstract void render(PatchScript script, final PatchSetDetail detail);
 
   protected abstract void onInsertComment(PatchLine pl);
@@ -559,7 +584,8 @@
       return null;
     }
 
-    final CommentEditorPanel ed = new CommentEditorPanel(newComment);
+    final CommentEditorPanel ed =
+        new CommentEditorPanel(newComment, commentLinkProcessor);
     ed.addFocusHandler(this);
     ed.addBlurHandler(this);
     boolean isCommentRow = false;
@@ -696,7 +722,8 @@
   protected void bindComment(final int row, final int col,
       final PatchLineComment line, final boolean isLast, boolean expandComment) {
     if (line.getStatus() == PatchLineComment.Status.DRAFT) {
-      final CommentEditorPanel plc = new CommentEditorPanel(line);
+      final CommentEditorPanel plc =
+          new CommentEditorPanel(line, commentLinkProcessor);
       plc.addFocusHandler(this);
       plc.addBlurHandler(this);
       table.setWidget(row, col, plc);
@@ -870,7 +897,7 @@
     final Button replyDone;
 
     PublishedCommentPanel(final AccountInfo author, final PatchLineComment c) {
-      super(author, c.getWrittenOn(), c.getMessage());
+      super(author, c.getWrittenOn(), c.getMessage(), commentLinkProcessor);
       this.comment = c;
 
       reply = new Button(PatchUtil.C.buttonReply());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java
index b609f15..9ed3f18 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java
@@ -16,20 +16,20 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.CommentLinkProcessor;
 import com.google.gerrit.client.ui.CommentPanel;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.dom.client.DoubleClickEvent;
 import com.google.gwt.event.dom.client.DoubleClickHandler;
-import com.google.gwt.event.dom.client.KeyCodes;
 import com.google.gwt.event.dom.client.KeyDownEvent;
 import com.google.gwt.event.dom.client.KeyDownHandler;
 import com.google.gwt.user.client.Timer;
-import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.Widget;
 import com.google.gwtexpui.globalkey.client.NpTextArea;
+import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtjsonrpc.common.VoidResult;
 
 import java.sql.Timestamp;
@@ -59,11 +59,13 @@
   private final Button discard;
   private final Timer expandTimer;
 
-  public CommentEditorPanel(final PatchLineComment plc) {
+  public CommentEditorPanel(final PatchLineComment plc,
+      final CommentLinkProcessor commentLinkProcessor) {
+    super(commentLinkProcessor);
     comment = plc;
 
     addStyleName(Gerrit.RESOURCES.css().commentEditorPanel());
-    setAuthorNameText(PatchUtil.C.draft());
+    setAuthorNameText(Gerrit.getUserAccountInfo(), PatchUtil.C.draft());
     setMessageText(plc.getMessage());
     addDoubleClickHandler(this);
 
@@ -81,18 +83,6 @@
     text.addKeyDownHandler(new KeyDownHandler() {
       @Override
       public void onKeyDown(final KeyDownEvent event) {
-        if (event.getNativeKeyCode() == KeyCodes.KEY_ESCAPE
-            && !event.isAnyModifierKeyDown()) {
-          event.preventDefault();
-
-          if (isNew()) {
-            onDiscard();
-          } else {
-            render();
-          }
-          return;
-        }
-
         if ((event.isControlKeyDown() || event.isMetaKeyDown())
             && !event.isAltKeyDown() && !event.isShiftKeyDown()) {
           switch (event.getNativeKeyCode()) {
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 d914283..200562a 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
@@ -21,14 +21,17 @@
 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.projects.ConfigInfoCache;
+import com.google.gerrit.client.rpc.CallbackGroup;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
+import com.google.gerrit.client.ui.CommentLinkProcessor;
 import com.google.gerrit.client.ui.ListenableAccountDiffPreference;
 import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.common.data.PatchScript;
 import com.google.gerrit.common.data.PatchSetDetail;
 import com.google.gerrit.prettify.client.ClientSideFormatter;
-import com.google.gerrit.prettify.common.PrettyFactory;
+import com.google.gerrit.prettify.client.PrettyFactory;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -38,6 +41,7 @@
 import com.google.gwt.event.logical.shared.ValueChangeEvent;
 import com.google.gwt.event.logical.shared.ValueChangeHandler;
 import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwtexpui.globalkey.client.GlobalKey;
 import com.google.gwtexpui.globalkey.client.KeyCommand;
@@ -97,6 +101,7 @@
   protected PatchSet.Id idSideB;
   protected PatchScriptSettingsPanel settingsPanel;
   protected TopView topView;
+  protected CommentLinkProcessor commentLinkProcessor;
 
   private ReviewedPanels reviewedPanels;
   private HistoryTable historyTable;
@@ -364,23 +369,40 @@
     if (isFirst && fileList != null && fileList.isLoaded()) {
       fileList.movePointerTo(patchKey);
     }
-    PatchUtil.DETAIL_SVC.patchScript(patchKey, idSideA, idSideB, //
-        settingsPanel.getValue(), new ScreenLoadCallback<PatchScript>(this) {
+
+    CallbackGroup cb = new CallbackGroup();
+    ConfigInfoCache.get(patchSetDetail.getProject(),
+        cb.add(new AsyncCallback<ConfigInfoCache.Entry>() {
           @Override
-          protected void preDisplay(final PatchScript result) {
-            if (rpcSequence == rpcseq) {
-              onResult(result, isFirst);
-            }
+          public void onSuccess(ConfigInfoCache.Entry result) {
+            commentLinkProcessor = result.getCommentLinkProcessor();
+            contentTable.setCommentLinkProcessor(commentLinkProcessor);
+            setTheme(result.getTheme());
           }
 
           @Override
-          public void onFailure(final Throwable caught) {
-            if (rpcSequence == rpcseq) {
-              settingsPanel.setEnabled(true);
-              super.onFailure(caught);
-            }
+          public void onFailure(Throwable caught) {
+            // Handled by ScreenLoadCallback.onFailure.
           }
-        });
+        }));
+    PatchUtil.DETAIL_SVC.patchScript(patchKey, idSideA, idSideB,
+        settingsPanel.getValue(), cb.addGwtjsonrpc(
+            new ScreenLoadCallback<PatchScript>(this) {
+              @Override
+              protected void preDisplay(final PatchScript result) {
+                if (rpcSequence == rpcseq) {
+                  onResult(result, isFirst);
+                }
+              }
+
+              @Override
+              public void onFailure(final Throwable caught) {
+                if (rpcSequence == rpcseq) {
+                  settingsPanel.setEnabled(true);
+                  super.onFailure(caught);
+                }
+              }
+        }));
   }
 
   private void onResult(final PatchScript script, final boolean isFirst) {
@@ -396,7 +418,8 @@
 
     if (idSideB.equals(patchSetDetail.getPatchSet().getId())) {
       commitMessageBlock.setVisible(true);
-      commitMessageBlock.display(patchSetDetail.getInfo().getMessage());
+      commitMessageBlock.display(patchSetDetail.getInfo().getMessage(),
+          commentLinkProcessor);
     } else {
       commitMessageBlock.setVisible(false);
       Util.DETAIL_SVC.patchSetDetail(idSideB,
@@ -404,7 +427,8 @@
             @Override
             public void onSuccess(PatchSetDetail result) {
               commitMessageBlock.setVisible(true);
-              commitMessageBlock.display(result.getInfo().getMessage());
+              commitMessageBlock.display(result.getInfo().getMessage(),
+                  commentLinkProcessor);
             }
           });
     }
@@ -431,6 +455,7 @@
       contentTable.removeFromParent();
       contentTable = new UnifiedDiffTable();
       contentTable.fileList = fileList;
+      contentTable.setCommentLinkProcessor(commentLinkProcessor);
       contentPanel.add(contentTable);
       setToken(Dispatcher.toPatchUnified(idSideA, patchKey));
     }
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 350c639..5ebeaf5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SideBySideTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SideBySideTable.java
@@ -22,10 +22,11 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.common.data.CommentDetail;
 import com.google.gerrit.common.data.PatchScript;
+import com.google.gerrit.common.data.PatchScript.DisplayMethod;
 import com.google.gerrit.common.data.PatchScript.FileMode;
 import com.google.gerrit.common.data.PatchSetDetail;
+import com.google.gerrit.prettify.client.SparseHtmlFile;
 import com.google.gerrit.prettify.common.EditList;
-import com.google.gerrit.prettify.common.SparseHtmlFile;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
@@ -186,11 +187,16 @@
           lines.add(new SkippedLine(lastA, lastB, b.size() - lastB));
         }
       }
-    }else{
+    } else {
       // Display the patch header for binary
       for (final String line : script.getPatchHeader()) {
         appendFileHeader(nc, line);
       }
+      // If there is a safe picture involved, we show it
+      if (script.getDisplayMethodA() == DisplayMethod.IMG
+          || script.getDisplayMethodB() == DisplayMethod.IMG) {
+        appendImageLine(script, nc);
+      }
     }
     if (!hasDifferences(script)) {
       appendNoDifferences(nc);
@@ -210,6 +216,42 @@
     }
   }
 
+  private SafeHtml createImage(String url) {
+    SafeHtmlBuilder m = new SafeHtmlBuilder();
+    m.openElement("img");
+    m.setAttribute("src", url);
+    m.closeElement("img");
+    return m.toSafeHtml();
+  }
+
+  private void appendImageLine(final PatchScript script,
+      final SafeHtmlBuilder m) {
+    m.openTr();
+    m.setAttribute("valign", "center");
+    m.setAttribute("align", "center");
+
+    m.openTd();
+    m.setStyleName(Gerrit.RESOURCES.css().iconCell());
+    m.closeTd();
+
+    appendLineNumber(m, false);
+    if (script.getDisplayMethodA() == DisplayMethod.IMG) {
+      final String url = getUrlA();
+      appendLineText(m, DELETE, createImage(url), false, true);
+    } else {
+      appendLineNone(m, DELETE);
+    }
+    if (script.getDisplayMethodB() == DisplayMethod.IMG) {
+      final String url = getUrlB();
+      appendLineText(m, INSERT, createImage(url), false, true);
+    } else {
+      appendLineNone(m, INSERT);
+    }
+
+    appendLineNumber(m, true);
+    m.closeTr();
+  }
+
   private void populateTableHeader(final PatchScript script,
       final PatchSetDetail detail) {
     initHeaders(script, detail);
@@ -400,10 +442,7 @@
     m.addStyleName(Gerrit.RESOURCES.css().iconCell());
     m.closeTd();
 
-    m.openTd();
-    m.setStyleName(Gerrit.RESOURCES.css().lineNumber());
-    m.nbsp();
-    m.closeTd();
+    appendLineNumber(m, false);
 
     m.openTd();
     m.setStyleName(Gerrit.RESOURCES.css().sideBySideTableBinaryHeader());
@@ -411,9 +450,7 @@
     m.append(line);
     m.closeTd();
 
-    m.openTd();
-    m.nbsp();
-    m.closeTd();
+    appendLineNumber(m, true);
 
     m.closeTr();
   }
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 82df54a..cacedfd 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedDiffTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedDiffTable.java
@@ -23,20 +23,17 @@
 import com.google.gerrit.common.data.PatchScript;
 import com.google.gerrit.common.data.PatchScript.DisplayMethod;
 import com.google.gerrit.common.data.PatchSetDetail;
+import com.google.gerrit.prettify.client.SparseHtmlFile;
 import com.google.gerrit.prettify.common.EditList;
 import com.google.gerrit.prettify.common.EditList.Hunk;
-import com.google.gerrit.prettify.common.SparseHtmlFile;
-import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gwt.core.client.GWT;
 import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.Element;
-import com.google.gwt.user.client.ui.UIObject;
 import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
+import com.google.gwt.user.client.ui.UIObject;
 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.Collections;
@@ -226,94 +223,18 @@
       appendFileHeader(nc, line);
     }
     final ArrayList<PatchLine> lines = new ArrayList<PatchLine>();
-    if (!isDisplayBinary) {
-      final SparseHtmlFile a = getSparseHtmlFileA(script);
-      final SparseHtmlFile b = getSparseHtmlFileB(script);
+
+    if (hasDifferences(script)) {
       if (script.getDisplayMethodA() == DisplayMethod.IMG
           || script.getDisplayMethodB() == DisplayMethod.IMG) {
-        final String rawBase = GWT.getHostPageBaseURL() + "cat/";
-
-        nc.openTr();
-        nc.setAttribute("valign", "center");
-        nc.setAttribute("align", "center");
-
-        nc.openTd();
-        nc.nbsp();
-        nc.closeTd();
-
-        nc.openTd();
-        nc.nbsp();
-        nc.closeTd();
-
-        nc.openTd();
-        nc.nbsp();
-        nc.closeTd();
-
-        nc.openTd();
-        if (script.getDisplayMethodA() == DisplayMethod.IMG) {
-          if (idSideA == null) {
-            appendImgTag(nc, rawBase + KeyUtil.encode(patchKey.toString()) + "^1");
-          } else {
-            Patch.Key k = new Patch.Key(idSideA, patchKey.get());
-            appendImgTag(nc, rawBase + KeyUtil.encode(k.toString()) + "^0");
-          }
-        }
-        if (script.getDisplayMethodB() == DisplayMethod.IMG) {
-          appendImgTag(nc, rawBase + KeyUtil.encode(patchKey.toString()) + "^0");
-        }
-        nc.closeTd();
-
-        nc.closeTr();
+        appendImageDifferences(script, nc);
+      } else if (!isDisplayBinary) {
+        appendTextDifferences(script, nc, lines);
       }
-
-      if (hasDifferences(script)) {
-        final boolean syntaxHighlighting =
-            script.getDiffPrefs().isSyntaxHighlighting();
-        for (final EditList.Hunk hunk : script.getHunks()) {
-          appendHunkHeader(nc, hunk);
-          while (hunk.next()) {
-            if (hunk.isContextLine()) {
-              openLine(nc);
-              appendLineNumberForSideA(nc, hunk.getCurA());
-              appendLineNumberForSideB(nc, hunk.getCurB());
-              appendLineText(nc, false, CONTEXT, a, hunk.getCurA());
-              closeLine(nc);
-              hunk.incBoth();
-              lines.add(new PatchLine(CONTEXT, hunk.getCurA(), hunk.getCurB()));
-
-            } else if (hunk.isDeletedA()) {
-              openLine(nc);
-              appendLineNumberForSideA(nc, hunk.getCurA());
-              padLineNumberForSideB(nc);
-              appendLineText(nc, syntaxHighlighting, DELETE, a, hunk.getCurA());
-              closeLine(nc);
-              hunk.incA();
-              lines.add(new PatchLine(DELETE, hunk.getCurA(), -1));
-              if (a.size() == hunk.getCurA()
-                  && script.getA().isMissingNewlineAtEnd()) {
-                appendNoLF(nc);
-              }
-
-            } else if (hunk.isInsertedB()) {
-              openLine(nc);
-              padLineNumberForSideA(nc);
-              appendLineNumberForSideB(nc, hunk.getCurB());
-              appendLineText(nc, syntaxHighlighting, INSERT, b, hunk.getCurB());
-              closeLine(nc);
-              hunk.incB();
-              lines.add(new PatchLine(INSERT, -1, hunk.getCurB()));
-              if (b.size() == hunk.getCurB()
-                  && script.getB().isMissingNewlineAtEnd()) {
-                appendNoLF(nc);
-              }
-            }
-          }
-        }
-      }
-    }
-    if (!hasDifferences(script)) {
+    } else {
       appendNoDifferences(nc);
     }
+
     resetHtml(nc);
     populateTableHeader(script, detail);
     if (hasDifferences(script)) {
@@ -347,6 +268,94 @@
     }
   }
 
+  private void appendImageLine(final SafeHtmlBuilder nc, final String url,
+      final boolean syntaxHighlighting, final boolean isInsert) {
+    nc.openTr();
+    nc.setAttribute("valign", "center");
+    nc.setAttribute("align", "center");
+
+    nc.openTd();
+    nc.setStyleName(Gerrit.RESOURCES.css().iconCell());
+    nc.closeTd();
+
+    padLineNumberForSideA(nc);
+    padLineNumberForSideB(nc);
+
+    nc.openTd();
+    nc.setStyleName(Gerrit.RESOURCES.css().fileLine());
+    if (isInsert) {
+      setStyleInsert(nc, syntaxHighlighting);
+    } else {
+      setStyleDelete(nc, syntaxHighlighting);
+    }
+    appendImgTag(nc, url);
+    nc.closeTd();
+
+    nc.closeTr();
+  }
+
+  private void appendImageDifferences(final PatchScript script,
+      final SafeHtmlBuilder nc) {
+    final boolean syntaxHighlighting =
+        script.getDiffPrefs().isSyntaxHighlighting();
+    if (script.getDisplayMethodA() == DisplayMethod.IMG) {
+      final String url = getUrlA();
+      appendImageLine(nc, url, syntaxHighlighting, false);
+    }
+    if (script.getDisplayMethodB() == DisplayMethod.IMG) {
+      final String url = getUrlB();
+      appendImageLine(nc, url, syntaxHighlighting, true);
+    }
+  }
+
+  private void appendTextDifferences(final PatchScript script,
+      final SafeHtmlBuilder nc, final ArrayList<PatchLine> lines) {
+    final SparseHtmlFile a = getSparseHtmlFileA(script);
+    final SparseHtmlFile b = getSparseHtmlFileB(script);
+    final boolean syntaxHighlighting =
+        script.getDiffPrefs().isSyntaxHighlighting();
+    for (final EditList.Hunk hunk : script.getHunks()) {
+      appendHunkHeader(nc, hunk);
+      while (hunk.next()) {
+        if (hunk.isContextLine()) {
+          openLine(nc);
+          appendLineNumberForSideA(nc, hunk.getCurA());
+          appendLineNumberForSideB(nc, hunk.getCurB());
+          appendLineText(nc, false, CONTEXT, a, hunk.getCurA());
+          closeLine(nc);
+          hunk.incBoth();
+          lines.add(new PatchLine(CONTEXT, hunk.getCurA(), hunk.getCurB()));
+
+        } else if (hunk.isDeletedA()) {
+          openLine(nc);
+          appendLineNumberForSideA(nc, hunk.getCurA());
+          padLineNumberForSideB(nc);
+          appendLineText(nc, syntaxHighlighting, DELETE, a, hunk.getCurA());
+          closeLine(nc);
+          hunk.incA();
+          lines.add(new PatchLine(DELETE, hunk.getCurA(), -1));
+          if (a.size() == hunk.getCurA()
+              && script.getA().isMissingNewlineAtEnd()) {
+            appendNoLF(nc);
+          }
+
+        } else if (hunk.isInsertedB()) {
+          openLine(nc);
+          padLineNumberForSideA(nc);
+          appendLineNumberForSideB(nc, hunk.getCurB());
+          appendLineText(nc, syntaxHighlighting, INSERT, b, hunk.getCurB());
+          closeLine(nc);
+          hunk.incB();
+          lines.add(new PatchLine(INSERT, -1, hunk.getCurB()));
+          if (b.size() == hunk.getCurB()
+              && script.getB().isMissingNewlineAtEnd()) {
+            appendNoLF(nc);
+          }
+        }
+      }
+    }
+  }
+
   @Override
   public void display(final CommentDetail cd, boolean expandComments) {
     if (cd.isEmpty()) {
@@ -519,6 +528,22 @@
     }
   }
 
+  private void setStyleDelete(final SafeHtmlBuilder m,
+      boolean syntaxHighlighting) {
+    m.addStyleName(Gerrit.RESOURCES.css().diffTextDELETE());
+    if (syntaxHighlighting) {
+      m.addStyleName(Gerrit.RESOURCES.css().fileLineDELETE());
+    }
+  }
+
+  private void setStyleInsert(final SafeHtmlBuilder m,
+      boolean syntaxHighlighting) {
+    m.addStyleName(Gerrit.RESOURCES.css().diffTextINSERT());
+    if (syntaxHighlighting) {
+      m.addStyleName(Gerrit.RESOURCES.css().fileLineINSERT());
+    }
+  }
+
   private void appendLineText(final SafeHtmlBuilder m,
       boolean syntaxHighlighting, final PatchLine.Type type,
       final SparseHtmlFile src, final int i) {
@@ -533,18 +558,12 @@
         m.append(text);
         break;
       case DELETE:
-        m.addStyleName(Gerrit.RESOURCES.css().diffTextDELETE());
-        if (syntaxHighlighting) {
-          m.addStyleName(Gerrit.RESOURCES.css().fileLineDELETE());
-        }
+        setStyleDelete(m, syntaxHighlighting);
         m.append("-");
         m.append(text);
         break;
       case INSERT:
-        m.addStyleName(Gerrit.RESOURCES.css().diffTextINSERT());
-        if (syntaxHighlighting) {
-          m.addStyleName(Gerrit.RESOURCES.css().fileLineINSERT());
-        }
+        setStyleInsert(m, syntaxHighlighting);
         m.append("+");
         m.append(text);
         break;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
new file mode 100644
index 0000000..522d348
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
@@ -0,0 +1,83 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.projects;
+
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwtexpui.safehtml.client.FindReplace;
+import com.google.gwtexpui.safehtml.client.LinkFindReplace;
+import com.google.gwtexpui.safehtml.client.RawFindReplace;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ConfigInfo extends JavaScriptObject {
+  public final native JavaScriptObject has_require_change_id()
+  /*-{ return this.hasOwnProperty('require_change_id'); }-*/;
+  public final native boolean require_change_id()
+  /*-{ return this.require_change_id; }-*/;
+
+  public final native JavaScriptObject has_use_content_merge()
+  /*-{ return this.hasOwnProperty('use_content_merge'); }-*/;
+  public final native boolean use_content_merge()
+  /*-{ return this.use_content_merge; }-*/;
+
+  public final native JavaScriptObject has_use_contributor_agreements()
+  /*-{ return this.hasOwnProperty('use_contributor_agreements'); }-*/;
+  public final native boolean use_contributor_agreements()
+  /*-{ return this.use_contributor_agreements; }-*/;
+
+  public final native JavaScriptObject has_use_signed_off_by()
+  /*-{ return this.hasOwnProperty('use_signed_off_by'); }-*/;
+  public final native boolean use_signed_off_by()
+  /*-{ return this.use_signed_off_by; }-*/;
+
+  private final native NativeMap<CommentLinkInfo> commentlinks0()
+  /*-{ return this.commentlinks; }-*/;
+  final List<FindReplace> commentlinks() {
+    JsArray<CommentLinkInfo> cls = commentlinks0().values();
+    List<FindReplace> commentLinks = new ArrayList<FindReplace>(cls.length());
+    for (int i = 0; i < cls.length(); i++) {
+      CommentLinkInfo cl = cls.get(i);
+      if (!cl.enabled()) {
+        continue;
+      }
+      if (cl.link() != null) {
+        commentLinks.add(new LinkFindReplace(cl.match(), cl.link()));
+      } else {
+        commentLinks.add(new RawFindReplace(cl.match(), cl.html()));
+      }
+    }
+    return commentLinks;
+  }
+
+  final native ThemeInfo theme() /*-{ return this.theme; }-*/;
+
+  protected ConfigInfo() {
+  }
+
+  static class CommentLinkInfo extends JavaScriptObject {
+    final native String match() /*-{ return this.match; }-*/;
+    final native String link() /*-{ return this.link; }-*/;
+    final native String html() /*-{ return this.html; }-*/;
+    final native boolean enabled() /*-{
+      return !this.hasOwnProperty('enabled') || this.enabled;
+    }-*/;
+
+    protected CommentLinkInfo() {
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfoCache.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfoCache.java
new file mode 100644
index 0000000..16406f4
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfoCache.java
@@ -0,0 +1,90 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.projects;
+
+import com.google.gerrit.client.ui.CommentLinkProcessor;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/** Cache of {@link ConfigInfo} objects by project name. */
+public class ConfigInfoCache {
+  private static final int LIMIT = 25;
+  private static final ConfigInfoCache instance =
+      GWT.create(ConfigInfoCache.class);
+
+  public static class Entry {
+    private final ConfigInfo info;
+    private CommentLinkProcessor commentLinkProcessor;
+
+    private Entry(ConfigInfo info) {
+      this.info = info;
+    }
+
+    public CommentLinkProcessor getCommentLinkProcessor() {
+      if (commentLinkProcessor == null) {
+        commentLinkProcessor = new CommentLinkProcessor(info.commentlinks());
+      }
+      return commentLinkProcessor;
+    }
+
+    public ThemeInfo getTheme() {
+      return info.theme();
+    }
+  }
+
+  public static void get(Project.NameKey name, AsyncCallback<Entry> cb) {
+    instance.getImpl(name, cb);
+  }
+
+  private final LinkedHashMap<String, Entry> cache;
+
+  protected ConfigInfoCache() {
+    cache = new LinkedHashMap<String, Entry>(LIMIT) {
+      private static final long serialVersionUID = 1L;
+
+      @Override
+      protected boolean removeEldestEntry(
+          Map.Entry<String, ConfigInfoCache.Entry> e) {
+        return size() > LIMIT;
+      }
+    };
+  }
+
+  private void getImpl(final Project.NameKey name,
+      final AsyncCallback<Entry> cb) {
+    Entry e = cache.get(name.get());
+    if (e != null) {
+      cb.onSuccess(e);
+      return;
+    }
+    ProjectApi.config(name).get(new AsyncCallback<ConfigInfo>() {
+      @Override
+      public void onSuccess(ConfigInfo result) {
+        Entry e = new Entry(result);
+        cache.put(name.get(), e);
+        cb.onSuccess(e);
+      }
+
+      @Override
+      public void onFailure(Throwable caught) {
+        cb.onFailure(caught);
+      }
+    });
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
index a6676dc..be133c5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
@@ -15,6 +15,7 @@
 
 import com.google.gerrit.client.VoidResult;
 import com.google.gerrit.client.rpc.RestApi;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 
@@ -32,6 +33,10 @@
         .put(input, asyncCallback);
   }
 
+  static RestApi config(Project.NameKey name) {
+    return new RestApi("/projects/").id(name.get()).view("config");
+  }
+
   private static class ProjectInput extends JavaScriptObject {
     static ProjectInput create() {
       return (ProjectInput) createObject();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ThemeInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ThemeInfo.java
new file mode 100644
index 0000000..67b6077
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ThemeInfo.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.package com.google.gerrit.server.git;
+
+package com.google.gerrit.client.projects;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class ThemeInfo extends JavaScriptObject {
+  public final native String css() /*-{ return this.css; }-*/;
+  public final native String header() /*-{ return this.header; }-*/;
+  public final native String footer() /*-{ return this.footer; }-*/;
+
+  protected ThemeInfo() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLinkPanel.java
similarity index 65%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLink.java
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLinkPanel.java
index fd52777..ef52176 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLink.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLinkPanel.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.client.ui;
 
+import com.google.gerrit.client.AvatarImage;
 import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.account.AccountInfo;
@@ -22,33 +23,46 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.UserIdentity;
+import com.google.gwt.user.client.ui.FlowPanel;
 
 /** Link to any user's account dashboard. */
-public class AccountLink extends InlineHyperlink {
+public class AccountLinkPanel extends FlowPanel {
   /** Create a link after locating account details from an active cache. */
-  public static AccountLink link(AccountInfoCache cache, Account.Id id) {
+  public static AccountLinkPanel link(AccountInfoCache cache, Account.Id id) {
     com.google.gerrit.common.data.AccountInfo ai = cache.get(id);
-    return ai != null ? new AccountLink(ai) : null;
+    return ai != null ? new AccountLinkPanel(ai) : null;
   }
 
-  public AccountLink(com.google.gerrit.common.data.AccountInfo ai) {
+  public AccountLinkPanel(com.google.gerrit.common.data.AccountInfo ai) {
     this(FormatUtil.asInfo(ai));
   }
 
-  public AccountLink(UserIdentity ident) {
+  public AccountLinkPanel(UserIdentity ident) {
     this(AccountInfo.create(
         ident.getAccount().get(),
         ident.getName(),
         ident.getEmail()));
   }
 
-  public AccountLink(AccountInfo info) {
+  public AccountLinkPanel(AccountInfo info) {
     this(info, Change.Status.NEW);
   }
 
-  public AccountLink(AccountInfo info, Change.Status status) {
-    super(FormatUtil.name(info), PageLinks.toAccountQuery(owner(info), status));
-    setTitle(FormatUtil.nameEmail(info));
+  public AccountLinkPanel(AccountInfo info, Change.Status status) {
+    addStyleName(Gerrit.RESOURCES.css().accountLinkPanel());
+
+    InlineHyperlink l =
+        new InlineHyperlink(FormatUtil.name(info), PageLinks.toAccountQuery(
+            owner(info), status)) {
+      @Override
+      public void go() {
+        Gerrit.display(getTargetHistoryToken());
+      }
+    };
+    l.setTitle(FormatUtil.nameEmail(info));
+
+    add(new AvatarImage(info, 16));
+    add(l);
   }
 
   private static String owner(AccountInfo ai) {
@@ -62,9 +76,4 @@
       return "";
     }
   }
-
-  @Override
-  public void go() {
-    Gerrit.display(getTargetHistoryToken());
-  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentLinkProcessor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentLinkProcessor.java
index a3c7a3c..10cd1f0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentLinkProcessor.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentLinkProcessor.java
@@ -15,9 +15,9 @@
 package com.google.gerrit.client.ui;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gwtjsonrpc.common.AsyncCallback;
-import com.google.gwtexpui.safehtml.client.RegexFindReplace;
+import com.google.gwtexpui.safehtml.client.FindReplace;
 import com.google.gwtexpui.safehtml.client.SafeHtml;
+import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtjsonrpc.common.VoidResult;
 
 import java.util.ArrayList;
@@ -25,17 +25,23 @@
 import java.util.List;
 
 public class CommentLinkProcessor {
-  public static SafeHtml apply(SafeHtml buf) {
-    try {
-      return buf.replaceAll(Gerrit.getConfig().getCommentLinks());
+  private List<FindReplace> commentLinks;
 
+  public CommentLinkProcessor(List<FindReplace> commentLinks) {
+    this.commentLinks = commentLinks;
+  }
+
+  public SafeHtml apply(SafeHtml buf) {
+    try {
+      return buf.replaceAll(commentLinks);
     } catch (RuntimeException err) {
       // One or more of the patterns isn't valid on this browser.
       // Try to filter the list down and remove the invalid ones.
 
-      List<RegexFindReplace> safe = new ArrayList<RegexFindReplace>();
+      List<FindReplace> safe = new ArrayList<FindReplace>(commentLinks.size());
+
       List<PatternError> bad = new ArrayList<PatternError>();
-      for (RegexFindReplace r : Gerrit.getConfig().getCommentLinks()) {
+      for (FindReplace r : commentLinks) {
         try {
           buf.replaceAll(Collections.singletonList(r));
           safe.add(r);
@@ -50,7 +56,7 @@
         for (PatternError e : bad) {
           msg.append("\n");
           msg.append("\"");
-          msg.append(e.pattern.find());
+          msg.append(e.pattern.pattern().getSource());
           msg.append("\": ");
           msg.append(e.errorMessage);
         }
@@ -67,28 +73,25 @@
       }
 
       try {
-        Gerrit.getConfig().setCommentLinks(safe);
+        commentLinks = safe;
         return buf.replaceAll(safe);
       } catch (RuntimeException err2) {
         // To heck with it. The patterns passed individually above but
-        // failed as a group? Just drop them all and render without.
+        // failed as a group? Just render without.
         //
-        Gerrit.getConfig().setCommentLinks(null);
+        commentLinks = null;
         return buf;
       }
     }
   }
 
   private static class PatternError {
-    RegexFindReplace pattern;
+    FindReplace pattern;
     String errorMessage;
 
-    PatternError(RegexFindReplace r, String w) {
+    PatternError(FindReplace r, String w) {
       pattern = r;
       errorMessage = w;
     }
   }
-
-  private CommentLinkProcessor() {
-  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java
index 8f8c7ea..05c6b5a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.client.ui;
 
+import com.google.gerrit.client.AvatarImage;
 import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.account.AccountInfo;
@@ -53,22 +54,25 @@
   private final InlineLabel messageSummary;
   private final FlowPanel content;
   private final DoubleClickHTML messageText;
+  private CommentLinkProcessor commentLinkProcessor;
   private FlowPanel buttons;
   private boolean recent;
 
-  public CommentPanel(final AccountInfo author, final Date when, String message) {
-    this();
+  public CommentPanel(final AccountInfo author, final Date when, String message,
+      CommentLinkProcessor commentLinkProcessor) {
+    this(commentLinkProcessor);
 
     setMessageText(message);
-    setAuthorNameText(FormatUtil.name(author));
+    setAuthorNameText(author, FormatUtil.name(author));
     setDateText(FormatUtil.shortFormatDayTime(when));
 
     final CellFormatter fmt = header.getCellFormatter();
-    fmt.getElement(0, 0).setTitle(FormatUtil.nameEmail(author));
-    fmt.getElement(0, 2).setTitle(FormatUtil.mediumFormat(when));
+    fmt.getElement(0, 1).setTitle(FormatUtil.nameEmail(author));
+    fmt.getElement(0, 3).setTitle(FormatUtil.mediumFormat(when));
   }
 
-  protected CommentPanel() {
+  protected CommentPanel(CommentLinkProcessor commentLinkProcessor) {
+    this.commentLinkProcessor = commentLinkProcessor;
     final FlowPanel body = new FlowPanel();
     initWidget(body);
     setStyleName(Gerrit.RESOURCES.css().commentPanel());
@@ -84,14 +88,14 @@
         setOpen(!isOpen());
       }
     });
-    header.setText(0, 0, "");
-    header.setWidget(0, 1, messageSummary);
-    header.setText(0, 2, "");
+    header.setText(0, 1, "");
+    header.setWidget(0, 2, messageSummary);
+    header.setText(0, 3, "");
     final CellFormatter fmt = header.getCellFormatter();
-    fmt.setStyleName(0, 0, Gerrit.RESOURCES.css().commentPanelAuthorCell());
-    fmt.setStyleName(0, 1, Gerrit.RESOURCES.css().commentPanelSummaryCell());
-    fmt.setStyleName(0, 2, Gerrit.RESOURCES.css().commentPanelDateCell());
-    fmt.setHorizontalAlignment(0, 2, HasHorizontalAlignment.ALIGN_RIGHT);
+    fmt.setStyleName(0, 1, Gerrit.RESOURCES.css().commentPanelAuthorCell());
+    fmt.setStyleName(0, 2, Gerrit.RESOURCES.css().commentPanelSummaryCell());
+    fmt.setStyleName(0, 3, Gerrit.RESOURCES.css().commentPanelDateCell());
+    fmt.setHorizontalAlignment(0, 3, HasHorizontalAlignment.ALIGN_RIGHT);
     body.add(header);
 
     content = new FlowPanel();
@@ -118,16 +122,17 @@
 
     messageSummary.setText(summarize(message));
     SafeHtml buf = new SafeHtmlBuilder().append(message).wikify();
-    buf = CommentLinkProcessor.apply(buf);
+    buf = commentLinkProcessor.apply(buf);
     SafeHtml.set(messageText, buf);
   }
 
-  public void setAuthorNameText(final String nameText) {
-    header.setText(0, 0, nameText);
+  public void setAuthorNameText(final AccountInfo author, final String nameText) {
+    header.setWidget(0, 0, new AvatarImage(author, 26));
+    header.setText(0, 1, nameText);
   }
 
   protected void setDateText(final String dateText) {
-    header.setText(0, 2, dateText);
+    header.setText(0, 3, dateText);
   }
 
   protected void setMessageTextVisible(final boolean show) {
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 e7c2d84..a26db05 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
@@ -15,6 +15,7 @@
 package com.google.gerrit.client.ui;
 
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.projects.ThemeInfo;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Grid;
 import com.google.gwt.user.client.ui.HasHorizontalAlignment;
@@ -41,6 +42,9 @@
   private String windowTitle;
   private Widget titleWidget;
 
+  private ThemeInfo theme;
+  private boolean setTheme;
+
   protected Screen() {
     initWidget(new FlowPanel());
     setStyleName(Gerrit.RESOURCES.css().screen());
@@ -54,6 +58,14 @@
     }
   }
 
+  @Override
+  protected void onUnload() {
+    super.onUnload();
+    if (setTheme) {
+      Gerrit.THEMER.clear();
+    }
+  }
+
   public void registerKeys() {
   }
 
@@ -124,6 +136,10 @@
     body.add(w);
   }
 
+  protected void setTheme(final ThemeInfo t) {
+    theme = t;
+  }
+
   /** Get the history token for this screen. */
   public String getToken() {
     return token;
@@ -167,5 +183,12 @@
     Gerrit.EVENT_BUS.fireEvent(new ScreenLoadEvent(this));
     Gerrit.setQueryString(null);
     registerKeys();
+
+    if (theme != null) {
+      Gerrit.THEMER.set(theme);
+      setTheme = true;
+    } else {
+      Gerrit.THEMER.clear();
+    }
   }
 }
diff --git a/gerrit-gwtui/src/test/java/com/google/gerrit/client/RelativeDateFormatterTest.java b/gerrit-gwtui/src/test/java/com/google/gerrit/client/RelativeDateFormatterTest.java
new file mode 100644
index 0000000..5be029c
--- /dev/null
+++ b/gerrit-gwtui/src/test/java/com/google/gerrit/client/RelativeDateFormatterTest.java
@@ -0,0 +1,98 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client;
+
+import static org.junit.Assert.assertEquals;
+import static com.google.gerrit.client.RelativeDateFormatter.YEAR_IN_MILLIS;
+import static com.google.gerrit.client.RelativeDateFormatter.SECOND_IN_MILLIS;
+import static com.google.gerrit.client.RelativeDateFormatter.MINUTE_IN_MILLIS;
+import static com.google.gerrit.client.RelativeDateFormatter.HOUR_IN_MILLIS;
+import static com.google.gerrit.client.RelativeDateFormatter.DAY_IN_MILLIS;
+
+import java.util.Date;
+
+import org.eclipse.jgit.util.RelativeDateFormatter;
+import org.junit.Test;
+
+public class RelativeDateFormatterTest {
+
+  private static void assertFormat(long ageFromNow, long timeUnit,
+      String expectedFormat) {
+    Date d = new Date(System.currentTimeMillis() - ageFromNow * timeUnit);
+    String s = RelativeDateFormatter.format(d);
+    assertEquals(expectedFormat, s);
+  }
+
+  @Test
+  public void testFuture() {
+    assertFormat(-100, YEAR_IN_MILLIS, "in the future");
+    assertFormat(-1, SECOND_IN_MILLIS, "in the future");
+  }
+
+  @Test
+  public void testFormatSeconds() {
+    assertFormat(1, SECOND_IN_MILLIS, "1 seconds ago");
+    assertFormat(89, SECOND_IN_MILLIS, "89 seconds ago");
+  }
+
+  @Test
+  public void testFormatMinutes() {
+    assertFormat(90, SECOND_IN_MILLIS, "2 minutes ago");
+    assertFormat(3, MINUTE_IN_MILLIS, "3 minutes ago");
+    assertFormat(60, MINUTE_IN_MILLIS, "60 minutes ago");
+    assertFormat(89, MINUTE_IN_MILLIS, "89 minutes ago");
+  }
+
+  @Test
+  public void testFormatHours() {
+    assertFormat(90, MINUTE_IN_MILLIS, "2 hours ago");
+    assertFormat(149, MINUTE_IN_MILLIS, "2 hours ago");
+    assertFormat(35, HOUR_IN_MILLIS, "35 hours ago");
+  }
+
+  @Test
+  public void testFormatDays() {
+    assertFormat(36, HOUR_IN_MILLIS, "2 days ago");
+    assertFormat(13, DAY_IN_MILLIS, "13 days ago");
+  }
+
+  @Test
+  public void testFormatWeeks() {
+    assertFormat(14, DAY_IN_MILLIS, "2 weeks ago");
+    assertFormat(69, DAY_IN_MILLIS, "10 weeks ago");
+  }
+
+  @Test
+  public void testFormatMonths() {
+    assertFormat(70, DAY_IN_MILLIS, "2 months ago");
+    assertFormat(75, DAY_IN_MILLIS, "3 months ago");
+    assertFormat(364, DAY_IN_MILLIS, "12 months ago");
+  }
+
+  @Test
+  public void testFormatYearsMonths() {
+    assertFormat(366, DAY_IN_MILLIS, "1 year ago");
+    assertFormat(380, DAY_IN_MILLIS, "1 year, 1 month ago");
+    assertFormat(410, DAY_IN_MILLIS, "1 year, 2 months ago");
+    assertFormat(2, YEAR_IN_MILLIS, "2 years ago");
+    assertFormat(1824, DAY_IN_MILLIS, "4 years, 12 months ago");
+  }
+
+  @Test
+  public void testFormatYears() {
+    assertFormat(5, YEAR_IN_MILLIS, "5 years ago");
+    assertFormat(60, YEAR_IN_MILLIS, "60 years ago");
+  }
+}
diff --git a/gerrit-httpd/pom.xml b/gerrit-httpd/pom.xml
index 6e26569..e498cac 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.6</version>
+    <version>2.7</version>
   </parent>
 
   <artifactId>gerrit-httpd</artifactId>
@@ -66,12 +66,6 @@
 
     <dependency>
       <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-launcher</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
       <artifactId>gerrit-server</artifactId>
       <version>${project.version}</version>
     </dependency>
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java
index aa56ae9..7241624 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java
@@ -26,7 +26,6 @@
 import com.google.gerrit.server.contact.ContactStore;
 import com.google.gerrit.server.mail.EmailSender;
 import com.google.gerrit.server.ssh.SshInfo;
-import com.google.gwtexpui.safehtml.client.RegexFindReplace;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
@@ -34,12 +33,8 @@
 import org.eclipse.jgit.lib.Config;
 
 import java.net.MalformedURLException;
-import java.util.ArrayList;
 import java.util.HashSet;
-import java.util.List;
 import java.util.Set;
-import java.util.regex.Pattern;
-import java.util.regex.PatternSyntaxException;
 
 import javax.servlet.ServletContext;
 
@@ -148,31 +143,6 @@
       config.setSshdAddress(sshInfo.getHostKeys().get(0).getHost());
     }
 
-    List<RegexFindReplace> links = new ArrayList<RegexFindReplace>();
-    for (String name : cfg.getSubsections("commentlink")) {
-      String match = cfg.getString("commentlink", name, "match");
-
-      // Unfortunately this validation isn't entirely complete. Clients
-      // can have exceptions trying to evaluate the pattern if they don't
-      // support a token used, even if the server does support the token.
-      //
-      // At the minimum, we can trap problems related to unmatched groups.
-      try {
-        Pattern.compile(match);
-      } catch (PatternSyntaxException e) {
-        throw new ProvisionException("Invalid pattern \"" + match
-            + "\" in commentlink." + name + ".match: " + e.getMessage());
-      }
-
-      String link = cfg.getString("commentlink", name, "link");
-      String html = cfg.getString("commentlink", name, "html");
-      if (html == null || html.isEmpty()) {
-        html = "<a href=\"" + link + "\">$&</a>";
-      }
-      links.add(new RegexFindReplace(match, html));
-    }
-    config.setCommentLinks(links);
-
     return config;
   }
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitWebConfig.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitWebConfig.java
index 7de4bc3..22d7568 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitWebConfig.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitWebConfig.java
@@ -55,6 +55,7 @@
     type.setProject(cfg.getString("gitweb", null, "project"));
     type.setRevision(cfg.getString("gitweb", null, "revision"));
     type.setFileHistory(cfg.getString("gitweb", null, "filehistory"));
+    type.setLinkDrafts(cfg.getBoolean("gitweb", null, "linkdrafts", true));
     String pathSeparator = cfg.getString("gitweb", null, "pathSeparator");
     if (pathSeparator != null) {
       if (pathSeparator.length() == 1) {
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 b93df43..bd25faf 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
@@ -81,7 +81,6 @@
     serve("/signout").with(HttpLogoutServlet.class);
     serve("/ssh_info").with(SshInfoServlet.class);
     serve("/static/*").with(StaticServlet.class);
-    serve("/tools/*").with(ToolServlet.class);
 
     serve("/Main.class").with(notFound());
     serve("/com/google/gerrit/launcher/*").with(notFound());
@@ -100,6 +99,7 @@
     serveRegex("^/r/(.+)/?$").with(DirectChangeByCommit.class);
 
     filter("/a/*").through(RequireIdentifiedUserFilter.class);
+    serveRegex("^/(?:a/)?tools/(.*)$").with(ToolServlet.class);
     serveRegex("^/(?:a/)?accounts/(.*)$").with(AccountsRestApiServlet.class);
     serveRegex("^/(?:a/)?changes/(.*)$").with(ChangesRestApiServlet.class);
     serveRegex("^/(?:a/)?groups/(.*)?$").with(GroupsRestApiServlet.class);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java
index ba7a5b8..3be0cd3 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java
@@ -32,7 +32,6 @@
 import com.google.gerrit.common.data.GerritConfig;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.httpd.GitWebConfig;
-import com.google.gerrit.launcher.GerritLauncher;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.IdentifiedUser;
@@ -140,7 +139,10 @@
 
   private void makeSiteConfig(final SitePaths site,
       final GerritConfig gerritConfig) throws IOException {
-    final File myconf = GerritLauncher.createTempFile("gitweb_config", ".perl");
+    if (!site.tmp_dir.exists()) {
+      site.tmp_dir.mkdirs();
+    }
+    File myconf = File.createTempFile("gitweb_config", ".perl", site.tmp_dir);
 
     // To make our configuration file only readable or writable by us;
     // this reduces the chances of someone tampering with the file.
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 6cdd9bd..ea2168a 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
@@ -352,11 +352,9 @@
 
       String css = HtmlDomUtil.readFile(src.getParentFile(), src.getName());
       if (css == null) {
-        banner.getParentNode().removeChild(banner);
         return info;
       }
 
-      banner.removeAttribute("id");
       banner.appendChild(hostDoc.createCDATASection("\n" + css + "\n"));
       return info;
     }
@@ -375,7 +373,6 @@
 
       Document html = HtmlDomUtil.parseFile(src);
       if (html == null) {
-        banner.getParentNode().removeChild(banner);
         return info;
       }
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index a2aa191..1040da3 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -68,6 +68,7 @@
 import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.OptionUtil;
 import com.google.gerrit.server.OutputFormat;
 import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gson.ExclusionStrategy;
@@ -587,9 +588,7 @@
       Multimap<String, String> config) {
     final Set<String> want = Sets.newHashSet();
     for (String p : config.get("fields")) {
-      Iterables.addAll(want, Splitter.on(',')
-          .omitEmptyStrings().trimResults()
-          .split(p));
+      Iterables.addAll(want, OptionUtil.splitOptionValue(p));
     }
     if (!want.isEmpty()) {
       gb.addSerializationExclusionStrategy(new ExclusionStrategy() {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java
index 559c270..22546a7 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java
@@ -201,8 +201,9 @@
 
   private List<GroupReference> suggestAccountGroup(
       @Nullable final ProjectControl projectControl, final String query, final int limit) {
-    final int n = limit <= 0 ? 10 : Math.min(limit, 10);
-    return Lists.newArrayList(Iterables.limit(groupBackend.suggest(query), n));
+    return Lists.newArrayList(Iterables.limit(
+        groupBackend.suggest(query, projectControl),
+        limit <= 0 ? 10 : Math.min(limit, 10)));
   }
 
   @Override
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java
index 120b9af..557e017 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java
@@ -177,13 +177,25 @@
   private void loadPatchSets() throws OrmException {
     ResultSet<PatchSet> source = db.patchSets().byChange(changeId);
     List<PatchSet> patches = new ArrayList<PatchSet>();
+    Set<PatchSet.Id> patchesWithDraftComments = new HashSet<PatchSet.Id>();
+    final CurrentUser user = control.getCurrentUser();
+    final Account.Id me =
+        user instanceof IdentifiedUser ? ((IdentifiedUser) user).getAccountId()
+            : null;
     for (PatchSet ps : source) {
+      final PatchSet.Id psId = ps.getId();
       if (control.isPatchVisible(ps, db)) {
         patches.add(ps);
+        if (me != null
+            && db.patchComments().draftByPatchSetAuthor(psId, me)
+                .iterator().hasNext()) {
+          patchesWithDraftComments.add(psId);
+        }
       }
-      patchsetsById.put(ps.getId(), ps);
+      patchsetsById.put(psId, ps);
     }
     detail.setPatchSets(patches);
+    detail.setPatchSetsWithDraftComments(patchesWithDraftComments);
   }
 
   private void loadMessages() throws OrmException {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/EditCommitMessageHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/EditCommitMessageHandler.java
index 5b064c8..ba50417 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/EditCommitMessageHandler.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/EditCommitMessageHandler.java
@@ -22,15 +22,14 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.TrackingFooters;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.mail.CommitMessageEditedSender;
 import com.google.gerrit.server.git.validators.CommitValidators;
+import com.google.gerrit.server.mail.CommitMessageEditedSender;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.ChangeControl;
@@ -76,7 +75,6 @@
   private final PatchSetInfoFactory patchSetInfoFactory;
 
   private final PersonIdent myIdent;
-  private final ApprovalsUtil approvalsUtil;
   private final TrackingFooters trackingFooters;
 
   @Inject
@@ -91,7 +89,7 @@
       final PatchSetInfoFactory patchSetInfoFactory,
       final GitReferenceUpdated gitRefUpdated,
       @GerritPersonIdent final PersonIdent myIdent,
-      final ApprovalsUtil approvalsUtil, TrackingFooters trackingFooters) {
+      TrackingFooters trackingFooters) {
     this.changeControlFactory = changeControlFactory;
     this.db = db;
     this.currentUser = currentUser;
@@ -107,7 +105,6 @@
     this.patchSetInfoFactory = patchSetInfoFactory;
     this.gitRefUpdated = gitRefUpdated;
     this.myIdent = myIdent;
-    this.approvalsUtil = approvalsUtil;
     this.trackingFooters = trackingFooters;
   }
 
@@ -136,7 +133,7 @@
 
       ChangeUtil.editCommitMessage(patchSetId, control.getRefControl(), commitValidators, currentUser, message, db,
           commitMessageEditedSenderFactory, hooks, git, patchSetInfoFactory, gitRefUpdated, myIdent,
-          approvalsUtil, trackingFooters);
+          trackingFooters);
 
       return changeDetailFactory.create(changeId).call();
     } finally {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java
index 95a8e26..8e81dd3 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java
@@ -106,7 +106,7 @@
         throw new NoSuchEntityException();
       }
     }
-
+    projectKey = control.getProject().getNameKey();
     final PatchList list;
 
     try {
@@ -114,8 +114,6 @@
         oldId = toObjectId(psIdBase);
         newId = toObjectId(psIdNew);
 
-        projectKey = control.getProject().getNameKey();
-
         list = listFor(keyFor(diffPrefs.getIgnoreWhitespace()));
       } else { // OK, means use base to compare
         list = patchListCache.get(control.getChange(), patchSet);
@@ -139,6 +137,7 @@
 
     detail = new PatchSetDetail();
     detail.setPatchSet(patchSet);
+    detail.setProject(projectKey);
 
     detail.setInfo(infoFactory.get(db, psIdNew));
     detail.setPatches(patches);
diff --git a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/HostPage.html b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/HostPage.html
index 907414f..ce100a5 100644
--- a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/HostPage.html
+++ b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/HostPage.html
@@ -31,7 +31,7 @@
       })();
     </script>
     <script id="gerrit_hostpagedata"></script>
-    <style  id="gerrit_sitecss" type="text/css"></style>
+    <style id="gerrit_sitecss" type="text/css"></style>
     <link rel="shortcut icon" type="image/x-icon" href="favicon.ico" />
   </head>
   <body>
diff --git a/gerrit-launcher/pom.xml b/gerrit-launcher/pom.xml
index 5b0c110..e04acca 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.6</version>
+    <version>2.7</version>
   </parent>
 
   <artifactId>gerrit-launcher</artifactId>
diff --git a/gerrit-main/pom.xml b/gerrit-main/pom.xml
index cf67261..46b0a9f 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.6</version>
+    <version>2.7</version>
   </parent>
 
   <artifactId>gerrit-main</artifactId>
diff --git a/gerrit-openid/pom.xml b/gerrit-openid/pom.xml
index 7bd8fbf..1e73867 100644
--- a/gerrit-openid/pom.xml
+++ b/gerrit-openid/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.6</version>
+    <version>2.7</version>
   </parent>
 
   <artifactId>gerrit-openid</artifactId>
diff --git a/gerrit-patch-commonsnet/pom.xml b/gerrit-patch-commonsnet/pom.xml
index b390af4..82c1d82 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.6</version>
+    <version>2.7</version>
   </parent>
 
   <artifactId>gerrit-patch-commonsnet</artifactId>
diff --git a/gerrit-patch-jgit/pom.xml b/gerrit-patch-jgit/pom.xml
index 2274493..a1b85b3 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.6</version>
+    <version>2.7</version>
   </parent>
 
   <artifactId>gerrit-patch-jgit</artifactId>
diff --git a/gerrit-pgm/pom.xml b/gerrit-pgm/pom.xml
index ca1cf10..9acb912 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.6</version>
+    <version>2.7</version>
   </parent>
 
   <artifactId>gerrit-pgm</artifactId>
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java
index 0336dda..aa413d6 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java
@@ -33,7 +33,6 @@
 import java.lang.annotation.Annotation;
 import java.util.List;
 import java.util.Set;
-import java.util.TreeSet;
 
 /** Initialize the {@code database} configuration section. */
 @Singleton
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/IoUtil.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/IoUtil.java
index f750748a..e28af7c 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/IoUtil.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/IoUtil.java
@@ -57,7 +57,6 @@
     if (!(cl instanceof URLClassLoader)) {
       throw noAddURL("Not loaded by URLClassLoader", null);
     }
-    @SuppressWarnings("resource")
     URLClassLoader urlClassLoader = (URLClassLoader) cl;
 
     Method addURL;
diff --git a/gerrit-plugin-api/pom.xml b/gerrit-plugin-api/pom.xml
index 6437674..0bfc55d 100644
--- a/gerrit-plugin-api/pom.xml
+++ b/gerrit-plugin-api/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.6</version>
+    <version>2.7</version>
   </parent>
 
   <artifactId>gerrit-plugin-api</artifactId>
@@ -72,8 +72,8 @@
           <createSourcesJar>true</createSourcesJar>
           <artifactSet>
             <excludes>
-              <exclude>gwtexpui:gwtexpui</exclude>
               <exclude>gwtjsonrpc:gwtjsonrpc</exclude>
+              <exclude>com.google.gerrit:gerrit-gwtexpui</exclude>
               <exclude>com.google.gerrit:gerrit-prettify</exclude>
               <exclude>com.google.gerrit:gerrit-patch-commonsnet</exclude>
               <exclude>com.google.gerrit:gerrit-patch-jgit</exclude>
diff --git a/gerrit-plugin-archetype/pom.xml b/gerrit-plugin-archetype/pom.xml
index 34ddd50..f721be4 100644
--- a/gerrit-plugin-archetype/pom.xml
+++ b/gerrit-plugin-archetype/pom.xml
@@ -21,7 +21,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.6</version>
+    <version>2.7</version>
   </parent>
 
   <artifactId>gerrit-plugin-archetype</artifactId>
diff --git a/gerrit-plugin-gwt-archetype/pom.xml b/gerrit-plugin-gwt-archetype/pom.xml
index d8e9129..9ea0cd0 100644
--- a/gerrit-plugin-gwt-archetype/pom.xml
+++ b/gerrit-plugin-gwt-archetype/pom.xml
@@ -21,7 +21,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.6</version>
+    <version>2.7</version>
   </parent>
 
   <artifactId>gerrit-plugin-gwt-archetype</artifactId>
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/public/hello.css b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/public/hello.css
index a88059d..73bf5c6 100644
--- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/public/hello.css
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/public/hello.css
@@ -6,7 +6,7 @@
  */
 
 body, table td, select {
-  font-family: Arial Unicode MS, Arial, sans-serif;
+  font-family: sans-serif;
   font-size: small;
 }
 pre {
diff --git a/gerrit-plugin-gwtui/pom.xml b/gerrit-plugin-gwtui/pom.xml
index 5b95831..3d1b923 100644
--- a/gerrit-plugin-gwtui/pom.xml
+++ b/gerrit-plugin-gwtui/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.6</version>
+    <version>2.7</version>
   </parent>
 
   <artifactId>gerrit-plugin-gwtui</artifactId>
diff --git a/gerrit-plugin-js-archetype/pom.xml b/gerrit-plugin-js-archetype/pom.xml
index a77626d..2972a8e 100644
--- a/gerrit-plugin-js-archetype/pom.xml
+++ b/gerrit-plugin-js-archetype/pom.xml
@@ -21,7 +21,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.6</version>
+    <version>2.7</version>
   </parent>
 
   <artifactId>gerrit-plugin-js-archetype</artifactId>
diff --git a/gerrit-prettify/pom.xml b/gerrit-prettify/pom.xml
index b150ac0..fafc399 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.6</version>
+    <version>2.7</version>
   </parent>
 
   <artifactId>gerrit-prettify</artifactId>
@@ -34,8 +34,9 @@
 
   <dependencies>
     <dependency>
-      <groupId>gwtexpui</groupId>
-      <artifactId>gwtexpui</artifactId>
+      <groupId>com.google.gerrit</groupId>
+      <artifactId>gerrit-gwtexpui</artifactId>
+      <version>${project.version}</version>
     </dependency>
 
     <dependency>
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 8d5ddb9..8e7c699 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
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.prettify.client;
 
-import com.google.gerrit.prettify.common.PrettyFactory;
-import com.google.gerrit.prettify.common.PrettyFormatter;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.user.client.ui.RootPanel;
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettifyConstants.java
similarity index 95%
rename from gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.java
rename to gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettifyConstants.java
index df60305..c191fa5 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettifyConstants.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.prettify.common;
+package com.google.gerrit.prettify.client;
 
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.i18n.client.Constants;
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.properties b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettifyConstants.properties
similarity index 100%
rename from gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.properties
rename to gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettifyConstants.properties
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettyFactory.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFactory.java
similarity index 94%
rename from gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettyFactory.java
rename to gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFactory.java
index 364789f..f68b629 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettyFactory.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFactory.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.prettify.common;
+package com.google.gerrit.prettify.client;
 
 /** Creates a new PrettyFormatter instance for one formatting run. */
 public interface PrettyFactory {
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/client/PrettyFormatter.java
similarity index 99%
rename from gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettyFormatter.java
rename to gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFormatter.java
index 5786e95..a84af5e 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettyFormatter.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFormatter.java
@@ -12,8 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.prettify.common;
+package com.google.gerrit.prettify.client;
 
+import com.google.gerrit.prettify.common.SparseFileContent;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 import com.google.gwtexpui.safehtml.client.SafeHtml;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseHtmlFile.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/SparseHtmlFile.java
similarity index 95%
rename from gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseHtmlFile.java
rename to gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/SparseHtmlFile.java
index ebe0855..0c2af36 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseHtmlFile.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/SparseHtmlFile.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.prettify.common;
+package com.google.gerrit.prettify.client;
 
 import com.google.gwtexpui.safehtml.client.SafeHtml;
 
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseFileContent.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseFileContent.java
index 609f091..aa08af0 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseFileContent.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseFileContent.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.prettify.common;
 
+
 import org.eclipse.jgit.diff.Edit;
 
 import java.util.ArrayList;
diff --git a/gerrit-reviewdb/pom.xml b/gerrit-reviewdb/pom.xml
index 0482a4a..5543c53 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.6</version>
+    <version>2.7</version>
   </parent>
 
   <artifactId>gerrit-reviewdb</artifactId>
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java
index 6f121ee..ad0f130 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java
@@ -65,6 +65,13 @@
     }
   }
 
+  public static enum CommentVisibilityStrategy {
+    COLLAPSE_ALL,
+    EXPAND_MOST_RECENT,
+    EXPAND_RECENT,
+    EXPAND_ALL;
+  }
+
   public static enum TimeFormat {
     /** 12-hour clock: 1:15 am, 2:13 pm */
     HHMM_12("h:mm a"),
@@ -123,6 +130,12 @@
   @Column(id = 11)
   protected boolean showUsernameInReviewCategory;
 
+  @Column(id = 12)
+  protected boolean relativeDateInChangeTable;
+
+  @Column(id = 13, length = 20, notNull = false)
+  protected String commentVisibilityStrategy;
+
   public AccountGeneralPreferences() {
   }
 
@@ -226,6 +239,26 @@
     timeFormat = fmt.name();
   }
 
+  public boolean isRelativeDateInChangeTable() {
+    return relativeDateInChangeTable;
+  }
+
+  public void setRelativeDateInChangeTable(final boolean relativeDateInChangeTable) {
+    this.relativeDateInChangeTable = relativeDateInChangeTable;
+  }
+
+  public CommentVisibilityStrategy getCommentVisibilityStrategy() {
+    if (commentVisibilityStrategy == null) {
+      return CommentVisibilityStrategy.EXPAND_MOST_RECENT;
+    }
+    return CommentVisibilityStrategy.valueOf(commentVisibilityStrategy);
+  }
+
+  public void setCommentVisibilityStrategy(
+      CommentVisibilityStrategy strategy) {
+    commentVisibilityStrategy = strategy.name();
+  }
+
   public void resetToDefaults() {
     maximumPageSize = DEFAULT_PAGESIZE;
     showSiteHeader = true;
@@ -237,5 +270,6 @@
     downloadCommand = null;
     dateFormat = null;
     timeFormat = null;
+    relativeDateInChangeTable = false;
   }
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java
index c070e3e..d243496 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java
@@ -113,6 +113,8 @@
 
   protected String localDefaultDashboardId;
 
+  protected String themeName;
+
   protected Project() {
   }
 
@@ -206,6 +208,14 @@
     this.localDefaultDashboardId = localDefaultDashboardId;
   }
 
+  public String getThemeName() {
+    return themeName;
+  }
+
+  public void setThemeName(final String themeName) {
+    this.themeName = themeName;
+  }
+
   public void copySettingsFrom(final Project update) {
     description = update.description;
     useContributorAgreements = update.useContributorAgreements;
diff --git a/gerrit-server/pom.xml b/gerrit-server/pom.xml
index a8930d5..e543634 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.6</version>
+    <version>2.7</version>
   </parent>
 
   <artifactId>gerrit-server</artifactId>
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 4116633..2d54601 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
@@ -32,7 +32,7 @@
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.events.ApprovalAttribute;
+import com.google.gerrit.server.data.ApprovalAttribute;
 import com.google.gerrit.server.events.ChangeAbandonedEvent;
 import com.google.gerrit.server.events.ChangeEvent;
 import com.google.gerrit.server.events.ChangeMergedEvent;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/CollectionsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/common/CollectionsUtil.java
deleted file mode 100644
index 6e635cb..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/common/CollectionsUtil.java
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License
-
-package com.google.gerrit.common;
-
-import java.util.Collection;
-
-/** Utilities for manipulating Collections . */
-public class CollectionsUtil {
-  /**
-   * Checks if any of the elements in the first collection can be found in the
-   * second collection.
-   *
-   * @param findAnyOfThese which elements to look for.
-   * @param inThisCollection where to look for them.
-   * @param <E> type of the elements in question.
-   * @return {@code true} if any of the elements in {@code findAnyOfThese} can
-   *         be found in {@code inThisCollection}, {@code false} otherwise.
-   */
-  public static <E> boolean isAnyIncludedIn(Collection<E> findAnyOfThese,
-      Collection<E> inThisCollection) {
-    for (E findThisItem : findAnyOfThese) {
-      if (inThisCollection.contains(findThisItem)) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  private CollectionsUtil() {
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
index 0ce6892..3fd24f1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Account.Id;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
@@ -30,7 +29,6 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
-import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 
@@ -66,39 +64,43 @@
   }
 
   /**
-   * Moves the PatchSetApprovals to the specified PatchSet on the change from
-   * the prior PatchSet, while keeping the vetos.
+   * Copy min/max scores from one patch set to another.
    *
-   * @param db database connection to use for updates.
-   * @param dest PatchSet to copy to
    * @throws OrmException
-   * @return List<PatchSetApproval> The previous approvals
    */
-  public List<PatchSetApproval> copyVetosToPatchSet(ReviewDb db,
-      LabelTypes labelTypes, PatchSet.Id dest) throws OrmException {
-    PatchSet.Id source;
-    if (dest.get() > 1) {
-      source = new PatchSet.Id(dest.getParentKey(), dest.get() - 1);
-    } else {
-      throw new OrmException("Previous patch set could not be found");
-    }
+  public static void copyLabels(ReviewDb db, LabelTypes labelTypes,
+      PatchSet.Id source, PatchSet.Id dest) throws OrmException {
+    Iterable<PatchSetApproval> sourceApprovals =
+        db.patchSetApprovals().byPatchSet(source);
+    copyLabels(db, labelTypes, sourceApprovals, source, dest);
+  }
 
-    List<PatchSetApproval> patchSetApprovals =
-        db.patchSetApprovals().byChange(dest.getParentKey()).toList();
-    for (PatchSetApproval a : patchSetApprovals) {
-      LabelType type = labelTypes.byLabel(a.getLabelId());
-      if (type != null && a.getPatchSetId().equals(source) &&
-          type.isCopyMinScore() &&
-          type.isMaxNegative(a)) {
-        db.patchSetApprovals().insert(
-            Collections.singleton(new PatchSetApproval(dest, a)));
+  /**
+   * Copy a set's min/max scores from one patch set to another.
+   *
+   * @throws OrmException
+   */
+  public static void copyLabels(ReviewDb db, LabelTypes labelTypes,
+      Iterable<PatchSetApproval> sourceApprovals, PatchSet.Id source,
+      PatchSet.Id dest) throws OrmException {
+    List<PatchSetApproval> copied = Lists.newArrayList();
+    for (PatchSetApproval a : sourceApprovals) {
+      if (source.equals(a.getPatchSetId())) {
+        LabelType type = labelTypes.byLabel(a.getLabelId());
+        if (type == null) {
+          continue;
+        } else if (type.isCopyMinScore() && type.isMaxNegative(a)) {
+          copied.add(new PatchSetApproval(dest, a));
+        } else if (type.isCopyMaxScore() && type.isMaxPositive(a)) {
+          copied.add(new PatchSetApproval(dest, a));
+        }
       }
     }
-    return patchSetApprovals;
+    db.patchSetApprovals().insert(copied);
   }
 
   public void addReviewers(ReviewDb db, LabelTypes labelTypes, Change change,
-      PatchSet ps, PatchSetInfo info, Set<Id> wantReviewers,
+      PatchSet ps, PatchSetInfo info, Set<Account.Id> wantReviewers,
       Set<Account.Id> existingReviewers) throws OrmException {
     List<LabelType> allTypes = labelTypes.getLabelTypes();
     if (allTypes.isEmpty()) {
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 9569a97..8f73014 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
@@ -15,7 +15,9 @@
 package com.google.gerrit.server;
 
 import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -24,6 +26,7 @@
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.client.TrackingId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.change.ChangeInserter;
 import com.google.gerrit.server.change.ChangeMessages;
 import com.google.gerrit.server.config.TrackingFooter;
 import com.google.gerrit.server.config.TrackingFooters;
@@ -193,9 +196,9 @@
       IdentifiedUser user, CommitValidators commitValidators, String message,
       ReviewDb db, RevertedSender.Factory revertedSenderFactory,
       ChangeHooks hooks, Repository git,
-      PatchSetInfoFactory patchSetInfoFactory,
-      GitReferenceUpdated gitRefUpdated, PersonIdent myIdent,
-      String canonicalWebUrl) throws NoSuchChangeException, EmailException,
+      PatchSetInfoFactory patchSetInfoFactory, PersonIdent myIdent,
+      ChangeInserter changeInserter)
+          throws NoSuchChangeException, EmailException,
       OrmException, MissingObjectException, IncorrectObjectTypeException,
       IOException, InvalidChangeOperationException {
     final Change.Id changeId = patchSetId.getParentKey();
@@ -272,9 +275,11 @@
         throw new InvalidChangeOperationException(e.getMessage());
       }
 
-      change.setCurrentPatchSet(patchSetInfoFactory.get(revertCommit, ps.getId()));
+      PatchSetInfo info = patchSetInfoFactory.get(revertCommit, ps.getId());
+      change.setCurrentPatchSet(info);
       ChangeUtil.updated(change);
 
+
       final RefUpdate ru = git.updateRef(ps.getRefName());
       ru.setExpectedOldObjectId(ObjectId.zeroId());
       ru.setNewObjectId(revertCommit);
@@ -284,17 +289,6 @@
             "Failed to create ref %s in %s: %s", ps.getRefName(),
             change.getDest().getParentKey().get(), ru.getResult()));
       }
-      gitRefUpdated.fire(change.getProject(), ru);
-
-      db.changes().beginTransaction(change.getId());
-      try {
-        insertAncestors(db, ps.getId(), revertCommit);
-        db.patchSets().insert(Collections.singleton(ps));
-        db.changes().insert(Collections.singleton(change));
-        db.commit();
-      } finally {
-        db.rollback();
-      }
 
       final ChangeMessage cmsg =
           new ChangeMessage(new ChangeMessage.Key(changeId,
@@ -303,17 +297,17 @@
           new StringBuilder("Patch Set " + patchSetId.get() + ": Reverted");
       msgBuf.append("\n\n");
       msgBuf.append("This patchset was reverted in change: " + change.getKey().get());
-
       cmsg.setMessage(msgBuf.toString());
-      db.changeMessages().insert(Collections.singleton(cmsg));
+
+      LabelTypes labelTypes = refControl.getProjectControl().getLabelTypes();
+      changeInserter.insertChange(db, change, cmsg, ps, revertCommit,
+          labelTypes, info, Collections.<Account.Id> emptySet());
 
       final RevertedSender cm = revertedSenderFactory.create(change);
       cm.setFrom(user.getAccountId());
       cm.setChangeMessage(cmsg);
       cm.send();
 
-      hooks.doPatchsetCreatedHook(change, ps, db);
-
       return change.getId();
     } finally {
       revWalk.release();
@@ -327,7 +321,7 @@
       final ChangeHooks hooks, Repository git,
       final PatchSetInfoFactory patchSetInfoFactory,
       final GitReferenceUpdated gitRefUpdated, PersonIdent myIdent,
-      final ApprovalsUtil approvalsUtil, final TrackingFooters trackingFooters)
+      final TrackingFooters trackingFooters)
       throws NoSuchChangeException, EmailException, OrmException,
       MissingObjectException, IncorrectObjectTypeException, IOException,
       InvalidChangeOperationException, PatchSetInfoNotAvailableException {
@@ -444,8 +438,9 @@
               "Change %s was modified", change.getId()));
         }
 
-        approvalsUtil.copyVetosToPatchSet(db,
+        ApprovalsUtil.copyLabels(db,
             refControl.getProjectControl().getLabelTypes(),
+            originalPS.getId(),
             change.currentPatchSetId());
 
         final List<FooterLine> footerLines = newCommit.getFooterLines();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/MimeUtilFileTypeRegistry.java b/gerrit-server/src/main/java/com/google/gerrit/server/MimeUtilFileTypeRegistry.java
index 792c1e7..b271d6a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/MimeUtilFileTypeRegistry.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/MimeUtilFileTypeRegistry.java
@@ -60,6 +60,38 @@
     mimeUtil.registerMimeDetector(name);
   }
 
+
+  /**
+   * Get specificity of mime types with generic types forced to low values
+   *
+   * "application/octet-stream" is forced to -1.
+   * "text/plain" is forced to 0.
+   * All other mime types return the specificity reported by mimeType itself.
+   *
+   * @param mimeType The mimeType to get the corrected specificity for.
+   * @return The corrected specificity.
+   */
+  private int getCorrectedMimeSpecificity(MimeType mimeType) {
+    // Although the documentation of MimeType's getSpecificity claims that for
+    // example "application/octet-stream" always has a specificity of 0, it
+    // effectively returns 1 for us. This causes problems when trying to get
+    // the correct mime type via sorting. For example in
+    // [application/octet-stream, image/x-icon] both mime types come with
+    // specificity 1 for us. Hence, getMimeType below may end up using
+    // application/octet-stream instead of the more specific image/x-icon.
+    // Therefore, we have to force the specificity of generic types below the
+    // default of 1.
+    //
+    final String mimeTypeStr = mimeType.toString();
+    if (mimeTypeStr.equals("application/octet-stream")) {
+      return -1;
+    }
+    if (mimeTypeStr.equals("text/plain")) {
+      return 0;
+    }
+    return mimeType.getSpecificity();
+  }
+
   @SuppressWarnings("unchecked")
   public MimeType getMimeType(final String path, final byte[] content) {
     Set<MimeType> mimeTypes = new HashSet<MimeType>();
@@ -84,7 +116,7 @@
     Collections.sort(types, new Comparator<MimeType>() {
       @Override
       public int compare(MimeType a, MimeType b) {
-        return b.getSpecificity() - a.getSpecificity();
+        return getCorrectedMimeSpecificity(b) - getCorrectedMimeSpecificity(a);
       }
     });
     return types.get(0);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/OptionUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/OptionUtil.java
new file mode 100644
index 0000000..24d10f7
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/OptionUtil.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Function;
+import com.google.common.base.Splitter;
+import com.google.common.collect.Iterables;
+
+/** Utilities for option parsing. */
+public class OptionUtil {
+  private static final Splitter COMMA_OR_SPACE =
+      Splitter.on(CharMatcher.anyOf(", ")).omitEmptyStrings().trimResults();
+
+  private static final Function<String, String> TO_LOWER_CASE =
+      new Function<String, String>() {
+        @Override
+        public String apply(String input) {
+          return input.toLowerCase();
+        }
+      };
+
+  public static Iterable<String> splitOptionValue(String value) {
+    return Iterables.transform(COMMA_OR_SPACE.split(value), TO_LOWER_CASE);
+  }
+
+  private OptionUtil() {
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
index e469c34..1827446 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
@@ -56,7 +56,7 @@
   private final IdentifiedUser.GenericFactory userFactory;
   private final ChangeUserName.Factory changeUserNameFactory;
   private final ProjectCache projectCache;
-  private final AtomicBoolean firstAccount;
+  private final AtomicBoolean awaitsFirstAccountCheck;
 
   @Inject
   AccountManager(final SchemaFactory<ReviewDb> schema,
@@ -73,14 +73,7 @@
     this.userFactory = userFactory;
     this.changeUserNameFactory = changeUserNameFactory;
     this.projectCache = projectCache;
-
-    firstAccount = new AtomicBoolean();
-    final ReviewDb db = schema.open();
-    try {
-      firstAccount.set(db.accounts().anyAccounts().toList().isEmpty());
-    } finally {
-      db.close();
-    }
+    this.awaitsFirstAccountCheck = new AtomicBoolean(true);
   }
 
   /**
@@ -274,10 +267,20 @@
     account.setFullName(who.getDisplayName());
     account.setPreferredEmail(extId.getEmailAddress());
 
-    db.accounts().insert(Collections.singleton(account));
-    db.accountExternalIds().insert(Collections.singleton(extId));
+    final boolean isFirstAccount = awaitsFirstAccountCheck.getAndSet(false)
+      && db.accounts().anyAccounts().toList().isEmpty();
 
-    if (firstAccount.get() && firstAccount.compareAndSet(true, false)) {
+    try {
+      db.accounts().insert(Collections.singleton(account));
+      db.accountExternalIds().insert(Collections.singleton(extId));
+    } finally {
+      // If adding the account failed, it may be that it actually was the
+      // first account. So we reset the 'check for first account'-guard, as
+      // otherwise the first account would not get administration permissions.
+      awaitsFirstAccountCheck.set(isFirstAccount);
+    }
+
+    if (isFirstAccount) {
       // This is the first user account on our site. Assume this user
       // is going to be the site's administrator and just make them that
       // to bootstrap the authentication database.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
index 942b0d7..d2014ec 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
@@ -142,6 +142,12 @@
         || canAdministrateServer();
   }
 
+  /** @return true if the user can stream Gerrit events. */
+  public boolean canStreamEvents() {
+    return canPerform(GlobalCapability.STREAM_EVENTS)
+        || canAdministrateServer();
+  }
+
   /** @return true if the user can run the Git garbage collection. */
   public boolean canRunGC() {
     return canPerform(GlobalCapability.RUN_GC)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAvatarChangeUrl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAvatarChangeUrl.java
new file mode 100644
index 0000000..ec538bc
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAvatarChangeUrl.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.avatar.AvatarProvider;
+import com.google.inject.Inject;
+
+public class GetAvatarChangeUrl implements RestReadView<AccountResource> {
+  private final DynamicItem<AvatarProvider> avatarProvider;
+
+  @Inject
+  GetAvatarChangeUrl(DynamicItem<AvatarProvider> avatarProvider) {
+    this.avatarProvider = avatarProvider;
+  }
+
+  @Override
+  public String apply(AccountResource rsrc)
+      throws ResourceNotFoundException {
+    AvatarProvider impl = avatarProvider.get();
+    if (impl == null) {
+      throw new ResourceNotFoundException();
+    }
+
+    String url = impl.getChangeAvatarUrl(rsrc.getUser());
+    if (Strings.isNullOrEmpty(url)) {
+      throw new ResourceNotFoundException();
+    } else {
+      return url;
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
index 81221aa..54f1980 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
@@ -24,12 +24,11 @@
 import static com.google.gerrit.common.data.GlobalCapability.PRIORITY;
 import static com.google.gerrit.common.data.GlobalCapability.RUN_GC;
 import static com.google.gerrit.common.data.GlobalCapability.START_REPLICATION;
+import static com.google.gerrit.common.data.GlobalCapability.STREAM_EVENTS;
 import static com.google.gerrit.common.data.GlobalCapability.VIEW_CACHES;
 import static com.google.gerrit.common.data.GlobalCapability.VIEW_CONNECTIONS;
 import static com.google.gerrit.common.data.GlobalCapability.VIEW_QUEUE;
 
-import com.google.common.base.Function;
-import com.google.common.base.Splitter;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
@@ -40,6 +39,7 @@
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.OptionUtil;
 import com.google.gerrit.server.OutputFormat;
 import com.google.gerrit.server.account.AccountResource.Capability;
 import com.google.gerrit.server.git.QueueProvider;
@@ -63,14 +63,7 @@
     if (query == null) {
       query = Sets.newHashSet();
     }
-    Iterables.addAll(query, Iterables.transform(
-        Splitter.onPattern("[, ]").omitEmptyStrings().trimResults().split(name),
-        new Function<String, String>() {
-          @Override
-          public String apply(String input) {
-            return input.toLowerCase();
-          }
-        }));
+    Iterables.addAll(query, OptionUtil.splitOptionValue(name));
   }
   private Set<String> query;
 
@@ -112,6 +105,7 @@
     have.put(VIEW_QUEUE, cc.canViewQueue());
     have.put(RUN_GC, cc.canRunGC());
     have.put(START_REPLICATION, cc.canStartReplication());
+    have.put(STREAM_EVENTS, cc.canStreamEvents());
     have.put(ACCESS_DATABASE, cc.canAccessDatabase());
 
     QueueProvider.QueueType queue = cc.getQueueType();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDiffPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDiffPreferences.java
new file mode 100644
index 0000000..8e65a23
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDiffPreferences.java
@@ -0,0 +1,78 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
+import com.google.gerrit.server.CurrentUser;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class GetDiffPreferences implements RestReadView<AccountResource> {
+
+  private final Provider<CurrentUser> self;
+
+  @Inject
+  GetDiffPreferences(Provider<CurrentUser> self) {
+    this.self = self;
+  }
+
+  @Override
+  public DiffPreferencesInfo apply(AccountResource rsrc) throws AuthException {
+    if (self.get() != rsrc.getUser()
+        && !self.get().getCapabilities().canAdministrateServer()) {
+      throw new AuthException("restricted to administrator");
+    }
+    return DiffPreferencesInfo.parse(rsrc.getUser().getAccountDiffPreference());
+  }
+
+  static class DiffPreferencesInfo {
+    static DiffPreferencesInfo parse(AccountDiffPreference p) {
+      DiffPreferencesInfo info = new DiffPreferencesInfo();
+      info.context = p.getContext();
+      info.expandAllComments = p.isExpandAllComments() ? true : null;
+      info.ignoreWhitespace = p.getIgnoreWhitespace();
+      info.intralineDifference = p.isIntralineDifference() ? true : null;
+      info.lineLength = p.getLineLength();
+      info.manualReview = p.isManualReview() ? true : null;
+      info.retainHeader = p.isRetainHeader() ? true : null;
+      info.showLineEndings = p.isShowLineEndings() ? true : null;
+      info.showTabs = p.isShowTabs() ? true : null;
+      info.showWhitespaceErrors = p.isShowWhitespaceErrors() ? true : null;
+      info.skipDeleted = p.isSkipDeleted() ? true : null;
+      info.skipUncommented = p.isSkipUncommented() ? true : null;
+      info.syntaxHighlighting = p.isSyntaxHighlighting() ? true : null;
+      info.tabSize = p.getTabSize();
+      return info;
+    }
+
+    short context;
+    Boolean expandAllComments;
+    Whitespace ignoreWhitespace;
+    Boolean intralineDifference;
+    int lineLength;
+    Boolean manualReview;
+    Boolean retainHeader;
+    Boolean showLineEndings;
+    Boolean showTabs;
+    Boolean showWhitespaceErrors;
+    Boolean skipDeleted;
+    Boolean skipUncommented;
+    Boolean syntaxHighlighting;
+    int tabSize;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackend.java
index b4e770f..34db967 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackend.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.project.ProjectControl;
 
 import java.util.Collection;
 
@@ -44,7 +45,9 @@
   GroupDescription.Basic get(AccountGroup.UUID uuid);
 
   /** @return suggestions for the group name sorted by name. */
-  Collection<GroupReference> suggest(String name);
+  Collection<GroupReference> suggest(
+      String name,
+      @Nullable ProjectControl project);
 
   /** @return the group membership checker for the backend. */
   GroupMembership membershipsOf(IdentifiedUser user);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackends.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackends.java
index cdbb0e4..f7e0634 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackends.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackends.java
@@ -16,6 +16,8 @@
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.project.ProjectControl;
 
 import java.util.Collection;
 import java.util.Comparator;
@@ -36,7 +38,7 @@
   };
 
   /**
-   * Runs {@link GroupBackend#suggest(String)} and filters the result to return
+   * Runs {@link GroupBackend#suggest(String, Project)} and filters the result to return
    * the best suggestion, or null if one does not exist.
    *
    * @param groupBackend the group backend
@@ -44,9 +46,23 @@
    * @return the best single GroupReference suggestion
    */
   @Nullable
-  public static GroupReference findBestSuggestion(
-      GroupBackend groupBackend, String name) {
-    Collection<GroupReference> refs = groupBackend.suggest(name);
+  public static GroupReference findBestSuggestion(GroupBackend groupBackend,
+      String name) {
+    return findBestSuggestion(groupBackend, name, null);
+  }
+  /**
+   * Runs {@link GroupBackend#suggest(String, Project)} and filters the result to return
+   * the best suggestion, or null if one does not exist.
+   *
+   * @param groupBackend the group backend
+   * @param name the name for which to suggest groups
+   * @param project the project for which to suggest groups
+   * @return the best single GroupReference suggestion
+   */
+  @Nullable
+  public static GroupReference findBestSuggestion(GroupBackend groupBackend,
+      String name, @Nullable ProjectControl project) {
+    Collection<GroupReference> refs = groupBackend.suggest(name, project);
     if (refs.size() == 1) {
       return Iterables.getOnlyElement(refs);
     }
@@ -60,7 +76,7 @@
   }
 
   /**
-   * Runs {@link GroupBackend#suggest(String)} and filters the result to return
+   * Runs {@link GroupBackend#suggest(String, Project)} and filters the result to return
    * the exact suggestion, or null if one does not exist.
    *
    * @param groupBackend the group backend
@@ -70,7 +86,22 @@
   @Nullable
   public static GroupReference findExactSuggestion(
       GroupBackend groupBackend, String name) {
-    Collection<GroupReference> refs = groupBackend.suggest(name);
+    return findExactSuggestion(groupBackend, name, null);
+  }
+
+  /**
+   * Runs {@link GroupBackend#suggest(String, Project)} and filters the result to return
+   * the exact suggestion, or null if one does not exist.
+   *
+   * @param groupBackend the group backend
+   * @param name the name for which to suggest groups
+   * @param project the project for which to suggest groups
+   * @return the exact single GroupReference suggestion
+   */
+  @Nullable
+  public static GroupReference findExactSuggestion(
+      GroupBackend groupBackend, String name, ProjectControl project) {
+    Collection<GroupReference> refs = groupBackend.suggest(name, project);
     for (GroupReference ref : refs) {
       if (isExactSuggestion(ref, name)) {
         return ref;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
index 2a8e7c9..2e01f26 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
@@ -127,10 +127,15 @@
   /** Can this user see this group exists? */
   public boolean isVisible() {
     AccountGroup accountGroup = GroupDescriptions.toAccountGroup(group);
+    /* Check for canAdministrateServer may seem redundant, but allows
+     * for visibility of all groups that are not an internal group to
+     * server administrators.
+     */
     return (accountGroup != null && accountGroup.isVisibleToAll())
       || user instanceof InternalUser
       || user.getEffectiveGroups().contains(group.getGroupUUID())
-      || isOwner();
+      || isOwner()
+      || user.getCapabilities().canAdministrateServer();
   }
 
   public boolean isOwner() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java
index d06db4d..a70f942 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.project.ProjectControl;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -71,7 +72,8 @@
   }
 
   @Override
-  public Collection<GroupReference> suggest(final String name) {
+  public Collection<GroupReference> suggest(final String name,
+      final ProjectControl project) {
     Iterable<AccountGroup> filtered = Iterables.filter(groupCache.all(),
         new Predicate<AccountGroup>() {
           @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java
index ac045f75e9..57a4a22 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java
@@ -31,8 +31,11 @@
 
     get(ACCOUNT_KIND).to(GetAccount.class);
     get(ACCOUNT_KIND, "avatar").to(GetAvatar.class);
+    get(ACCOUNT_KIND, "avatar.change.url").to(GetAvatarChangeUrl.class);
     child(ACCOUNT_KIND, "capabilities").to(Capabilities.class);
     get(ACCOUNT_KIND, "groups").to(GetGroups.class);
+    get(ACCOUNT_KIND, "preferences.diff").to(GetDiffPreferences.class);
+    put(ACCOUNT_KIND, "preferences.diff").to(SetDiffPreferences.class);
     get(CAPABILITY_KIND).to(GetCapabilities.CheckOne.class);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java
new file mode 100644
index 0000000..db9bc2d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java
@@ -0,0 +1,130 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.GetDiffPreferences.DiffPreferencesInfo;
+import com.google.gerrit.server.account.SetDiffPreferences.Input;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.Collections;
+
+public class SetDiffPreferences implements RestModifyView<AccountResource, Input> {
+  static class Input {
+    Short context;
+    Boolean expandAllComments;
+    Whitespace ignoreWhitespace;
+    Boolean intralineDifference;
+    Integer lineLength;
+    Boolean manualReview;
+    Boolean retainHeader;
+    Boolean showLineEndings;
+    Boolean showTabs;
+    Boolean showWhitespaceErrors;
+    Boolean skipDeleted;
+    Boolean skipUncommented;
+    Boolean syntaxHighlighting;
+    Integer tabSize;
+  }
+
+  private final Provider<CurrentUser> self;
+  private final ReviewDb db;
+
+  @Inject
+  SetDiffPreferences(Provider<CurrentUser> self, ReviewDb db) {
+    this.self = self;
+    this.db = db;
+  }
+
+  @Override
+  public DiffPreferencesInfo apply(AccountResource rsrc, Input input)
+      throws AuthException, OrmException {
+    if (self.get() != rsrc.getUser()
+        && !self.get().getCapabilities().canAdministrateServer()) {
+      throw new AuthException("restricted to administrator");
+    }
+    if (input == null) {
+      input = new Input();
+    }
+
+    Account.Id accountId = rsrc.getUser().getAccountId();
+    AccountDiffPreference p;
+
+    db.accounts().beginTransaction(accountId);
+    try {
+      p = db.accountDiffPreferences().get(accountId);
+      if (p == null) {
+        p = new AccountDiffPreference(accountId);
+      }
+
+      if (input.context != null) {
+        p.setContext(input.context);
+      }
+      if (input.ignoreWhitespace != null) {
+        p.setIgnoreWhitespace(input.ignoreWhitespace);
+      }
+      if (input.expandAllComments != null) {
+        p.setExpandAllComments(input.expandAllComments);
+      }
+      if (input.intralineDifference != null) {
+        p.setIntralineDifference(input.intralineDifference);
+      }
+      if (input.lineLength != null) {
+        p.setLineLength(input.lineLength);
+      }
+      if (input.manualReview != null) {
+        p.setManualReview(input.manualReview);
+      }
+      if (input.retainHeader != null) {
+        p.setRetainHeader(input.retainHeader);
+      }
+      if (input.showLineEndings != null) {
+        p.setShowLineEndings(input.showLineEndings);
+      }
+      if (input.showTabs != null) {
+        p.setShowTabs(input.showTabs);
+      }
+      if (input.showWhitespaceErrors != null) {
+        p.setShowWhitespaceErrors(input.showWhitespaceErrors);
+      }
+      if (input.skipDeleted != null) {
+        p.setSkipDeleted(input.skipDeleted);
+      }
+      if (input.skipUncommented != null) {
+        p.setSkipUncommented(input.skipUncommented);
+      }
+      if (input.syntaxHighlighting != null) {
+        p.setSyntaxHighlighting(input.syntaxHighlighting);
+      }
+      if (input.tabSize != null) {
+        p.setTabSize(input.tabSize);
+      }
+
+      db.accountDiffPreferences().upsert(Collections.singleton(p));
+      db.commit();
+    } finally {
+      db.rollback();
+    }
+    return DiffPreferencesInfo.parse(p);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java
index d9c9257..046dfa5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.project.ProjectControl;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -82,10 +83,10 @@
   }
 
   @Override
-  public Collection<GroupReference> suggest(String name) {
+  public Collection<GroupReference> suggest(String name, ProjectControl project) {
     Set<GroupReference> groups = Sets.newTreeSet(GROUP_REF_NAME_COMPARATOR);
     for (GroupBackend g : backends) {
-      groups.addAll(g.suggest(name));
+      groups.addAll(g.suggest(name, project));
     }
     return groups;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
index 2cf372b..97a0309 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
@@ -33,6 +33,7 @@
 import com.google.gerrit.server.account.ListGroupMembership;
 import com.google.gerrit.server.auth.ldap.Helper.LdapSchema;
 import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectControl;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.name.Named;
@@ -164,7 +165,7 @@
   }
 
   @Override
-  public Collection<GroupReference> suggest(String name) {
+  public Collection<GroupReference> suggest(String name, ProjectControl project) {
     AccountGroup.UUID uuid = new AccountGroup.UUID(name);
     if (isLdapUUID(uuid)) {
       GroupDescription.Basic g = get(uuid);
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 fc1102e..ac47cb5 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
@@ -239,7 +239,7 @@
         }
       }
     } catch (NamingException e) {
-      log.error("Cannot query LDAP to autenticate user", e);
+      log.error("Cannot query LDAP to authenticate user", e);
       throw new AuthenticationUnavailableException("Cannot query LDAP for account", e);
     } catch (LoginException e) {
       log.error("Cannot authenticate server via JAAS", e);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
index e0c78ea..5c965e2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetInfo;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -29,11 +30,9 @@
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.revwalk.FooterLine;
 import org.eclipse.jgit.revwalk.RevCommit;
 
 import java.util.Collections;
-import java.util.List;
 import java.util.Set;
 
 public class ChangeInserter {
@@ -53,17 +52,27 @@
   }
 
   public void insertChange(ReviewDb db, Change change, PatchSet ps,
-      RevCommit commit, LabelTypes labelTypes, List<FooterLine> footerLines,
-      PatchSetInfo info, Set<Account.Id> reviewers) throws OrmException {
+      RevCommit commit, LabelTypes labelTypes, PatchSetInfo info,
+      Set<Account.Id> reviewers) throws OrmException {
+    insertChange(db, change, null, ps, commit, labelTypes, info, reviewers);
+  }
+
+  public void insertChange(ReviewDb db, Change change,
+      ChangeMessage changeMessage, PatchSet ps, RevCommit commit,
+      LabelTypes labelTypes, PatchSetInfo info, Set<Account.Id> reviewers)
+      throws OrmException {
 
     db.changes().beginTransaction(change.getId());
     try {
       ChangeUtil.insertAncestors(db, ps.getId(), commit);
       db.patchSets().insert(Collections.singleton(ps));
       db.changes().insert(Collections.singleton(change));
-      ChangeUtil.updateTrackingIds(db, change, trackingFooters, footerLines);
+      ChangeUtil.updateTrackingIds(db, change, trackingFooters, commit.getFooterLines());
       approvalsUtil.addReviewers(db, labelTypes, change, ps, info, reviewers,
           Collections.<Account.Id> emptySet());
+      if (changeMessage != null) {
+        db.changeMessages().insert(Collections.singleton(changeMessage));
+      }
       db.commit();
     } finally {
       db.rollback();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
index b1d0603..99eb8e4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
@@ -23,6 +23,7 @@
 import static com.google.gerrit.common.changes.ListChangesOption.DETAILED_ACCOUNTS;
 import static com.google.gerrit.common.changes.ListChangesOption.DETAILED_LABELS;
 import static com.google.gerrit.common.changes.ListChangesOption.LABELS;
+import static com.google.gerrit.common.changes.ListChangesOption.MESSAGES;
 
 import com.google.common.base.Joiner;
 import com.google.common.base.Objects;
@@ -259,6 +260,9 @@
       }
       out.removable_reviewers = removableReviewers(cd, out.labels.values());
     }
+    if (options.contains(MESSAGES)) {
+      out.messages = messages(cd);
+    }
     out.finish();
 
     if (has(ALL_REVISIONS) || has(CURRENT_REVISION) || limited != null) {
@@ -609,6 +613,38 @@
     return permitted.asMap();
   }
 
+  private Collection<ChangeMessageInfo> messages(ChangeData cd)
+      throws OrmException {
+    List<ChangeMessage> messages =
+        db.get().changeMessages().byChange(cd.getId()).toList();
+    if (messages.isEmpty()) {
+      return Collections.emptyList();
+    }
+
+    // chronological order
+    Collections.sort(messages, new Comparator<ChangeMessage>() {
+      @Override
+      public int compare(ChangeMessage a, ChangeMessage b) {
+        return a.getWrittenOn().compareTo(b.getWrittenOn());
+      }
+    });
+
+    List<ChangeMessageInfo> result =
+        Lists.newArrayListWithCapacity(messages.size());
+    for (ChangeMessage message : messages) {
+      PatchSet.Id patchNum = message.getPatchSetId();
+
+      ChangeMessageInfo cmi = new ChangeMessageInfo();
+      cmi.id = message.getKey().get();
+      cmi.author = accountLoader.get(message.getAuthor());
+      cmi.date = message.getWrittenOn();
+      cmi.message = message.getMessage();
+      cmi._revisionNumber = patchNum != null ? patchNum.get() : null;
+      result.add(cmi);
+    }
+    return result;
+  }
+
   private Collection<AccountInfo> removableReviewers(ChangeData cd,
       Collection<LabelInfo> labels) throws OrmException {
     ChangeControl ctl = control(cd);
@@ -837,6 +873,7 @@
     Map<String, LabelInfo> labels;
     Map<String, Collection<String>> permitted_labels;
     Collection<AccountInfo> removable_reviewers;
+    Collection<ChangeMessageInfo> messages;
 
     String current_revision;
     Map<String, RevisionInfo> revisions;
@@ -922,4 +959,12 @@
       super(id);
     }
   }
+
+  static class ChangeMessageInfo {
+    String id;
+    AccountInfo author;
+    Timestamp date;
+    String message;
+    Integer _revisionNumber;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeTriplet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeTriplet.java
new file mode 100644
index 0000000..79c442c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeTriplet.java
@@ -0,0 +1,71 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+
+import org.eclipse.jgit.lib.Constants;
+
+public class ChangeTriplet {
+
+  private final Change.Key changeKey;
+  private final Project.NameKey projectNameKey;
+  private final Branch.NameKey branchNameKey;
+
+  public ChangeTriplet(final String triplet) throws ParseException {
+    int t2 = triplet.lastIndexOf('~');
+    int t1 = triplet.lastIndexOf('~', t2 - 1);
+    if (t1 < 0 || t2 < 0) {
+      throw new ParseException();
+    }
+
+    String project = Url.decode(triplet.substring(0, t1));
+    String branch = Url.decode(triplet.substring(t1 + 1, t2));
+    String changeId = Url.decode(triplet.substring(t2 + 1));
+
+    if (!branch.startsWith(Constants.R_REFS)) {
+      branch = Constants.R_HEADS + branch;
+    }
+
+    changeKey = new Change.Key(changeId);
+    projectNameKey = new Project.NameKey(project);
+    branchNameKey = new Branch.NameKey(projectNameKey, branch);
+  }
+
+  public Change.Key getChangeKey() {
+    return changeKey;
+  }
+
+  public Branch.NameKey getBranchNameKey() {
+    return branchNameKey;
+  }
+
+  public static String format(final Change change) {
+    return change.getProject().get() + "~"
+        + change.getDest().getShortName() + "~"
+        + change.getKey().get();
+  }
+
+  public static class ParseException extends Exception {
+    private static final long serialVersionUID = 1L;
+
+    ParseException() {
+      super();
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java
index 0100ebc..45328b1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java
@@ -21,10 +21,7 @@
 import com.google.gerrit.extensions.restapi.RestCollection;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
-import com.google.gerrit.extensions.restapi.Url;
-import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
@@ -33,8 +30,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-import org.eclipse.jgit.lib.Constants;
-
 import java.io.UnsupportedEncodingException;
 import java.util.Collections;
 import java.util.List;
@@ -72,8 +67,7 @@
   public ChangeResource parse(TopLevelResource root, IdString id)
       throws ResourceNotFoundException, OrmException,
       UnsupportedEncodingException {
-    ParsedId p = new ParsedId(id.encoded());
-    List<Change> changes = findChanges(p);
+    List<Change> changes = findChanges(id.encoded());
     if (changes.size() != 1) {
       throw new ResourceNotFoundException(id);
     }
@@ -87,59 +81,36 @@
     return new ChangeResource(control);
   }
 
-  private List<Change> findChanges(ParsedId k) throws OrmException {
-    if (k.legacyId != null) {
-      Change c = db.get().changes().get(k.legacyId);
+  private List<Change> findChanges(String id)
+      throws OrmException, ResourceNotFoundException {
+    // Try legacy id
+    if (id.matches("^[1-9][0-9]*$")) {
+      Change c = db.get().changes().get(Change.Id.parse(id));
       if (c != null) {
         return ImmutableList.of(c);
       }
       return Collections.emptyList();
-    } else if (k.project == null && k.branch == null && k.changeId != null) {
-      Change.Key id = new Change.Key(k.changeId);
-      if (id.get().length() == 41) {
-        return db.get().changes().byKey(id).toList();
+    }
+
+    // Try isolated changeId
+    if (!id.contains("~")) {
+      Change.Key key = new Change.Key(id);
+      if (key.get().length() == 41) {
+        return db.get().changes().byKey(key).toList();
       } else {
-        return db.get().changes().byKeyRange(id, id.max()).toList();
+        return db.get().changes().byKeyRange(key, key.max()).toList();
       }
     }
+
+    // Try change triplet
+    ChangeTriplet triplet;
+    try {
+        triplet = new ChangeTriplet(id);
+    } catch (ChangeTriplet.ParseException e) {
+        throw new ResourceNotFoundException(id);
+    }
     return db.get().changes().byBranchKey(
-        k.branch(),
-        new Change.Key(k.changeId)).toList();
-  }
-
-  private static class ParsedId {
-    Change.Id legacyId;
-    String project;
-    String branch;
-    String changeId;
-
-    ParsedId(String id) throws ResourceNotFoundException {
-      if (id.matches("^[1-9][0-9]*$")) {
-        legacyId = Change.Id.parse(id);
-        return;
-      }
-
-      int t2 = id.lastIndexOf('~');
-      int t1 = id.lastIndexOf('~', t2 - 1);
-      if (t1 < 0 || t2 < 0) {
-        if (!id.matches("^I[0-9a-z]{4,40}$")) {
-          throw new ResourceNotFoundException(id);
-        }
-        changeId = id;
-        return;
-      }
-
-      project = Url.decode(id.substring(0, t1));
-      branch = Url.decode(id.substring(t1 + 1, t2));
-      changeId = Url.decode(id.substring(t2 + 1));
-
-      if (!branch.startsWith(Constants.R_REFS)) {
-        branch = Constants.R_HEADS + branch;
-      }
-    }
-
-    Branch.NameKey branch() {
-      return new Branch.NameKey(new Project.NameKey(project), branch);
-    }
+        triplet.getBranchNameKey(),
+        triplet.getChangeKey()).toList();
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentInfo.java
new file mode 100644
index 0000000..798b9c6
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentInfo.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.package com.google.gerrit.server.change;
+
+package com.google.gerrit.server.change;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.server.account.AccountInfo;
+
+import java.sql.Timestamp;
+
+public class CommentInfo {
+  static enum Side {
+    PARENT, REVISION;
+  }
+
+  final String kind = "gerritcodereview#comment";
+  String id;
+  String path;
+  Side side;
+  Integer line;
+  String inReplyTo;
+  String message;
+  Timestamp updated;
+  AccountInfo author;
+
+  CommentInfo(PatchLineComment c, AccountInfo.Loader accountLoader) {
+    id = Url.encode(c.getKey().get());
+    path = c.getKey().getParentKey().getFileName();
+    if (c.getSide() == 0) {
+      side = Side.PARENT;
+    }
+    if (c.getLine() > 0) {
+      line = c.getLine();
+    }
+    inReplyTo = Url.encode(c.getParentUuid());
+    message = Strings.emptyToNull(c.getMessage());
+    updated = c.getWrittenOn();
+    if (accountLoader != null) {
+      author = accountLoader.get(c.getAuthor());
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentResource.java
new file mode 100644
index 0000000..ec47d01
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentResource.java
@@ -0,0 +1,51 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.inject.TypeLiteral;
+
+public class CommentResource implements RestResource {
+  public static final TypeLiteral<RestView<CommentResource>> COMMENT_KIND =
+      new TypeLiteral<RestView<CommentResource>>() {};
+
+  private final RevisionResource rev;
+  private final PatchLineComment comment;
+
+  CommentResource(RevisionResource rev, PatchLineComment c) {
+    this.rev = rev;
+    this.comment = c;
+  }
+
+  public PatchSet getPatchSet() {
+    return rev.getPatchSet();
+  }
+
+  PatchLineComment getComment() {
+    return comment;
+  }
+
+  String getId() {
+    return comment.getKey().get();
+  }
+
+  Account.Id getAuthorId() {
+    return comment.getAuthor();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Comments.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Comments.java
new file mode 100644
index 0000000..91cfbf8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Comments.java
@@ -0,0 +1,64 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+class Comments implements ChildCollection<RevisionResource, CommentResource> {
+  private final DynamicMap<RestView<CommentResource>> views;
+  private final Provider<ListComments> list;
+  private final Provider<ReviewDb> dbProvider;
+
+  @Inject
+  Comments(DynamicMap<RestView<CommentResource>> views,
+      Provider<ListComments> list,
+      Provider<ReviewDb> dbProvider) {
+    this.views = views;
+    this.list = list;
+    this.dbProvider = dbProvider;
+  }
+
+  @Override
+  public DynamicMap<RestView<CommentResource>> views() {
+    return views;
+  }
+
+  @Override
+  public RestView<RevisionResource> list() {
+    return list.get();
+  }
+
+  @Override
+  public CommentResource parse(RevisionResource rev, IdString id)
+      throws ResourceNotFoundException, OrmException {
+    String uuid = id.get();
+    for (PatchLineComment c : dbProvider.get().patchComments()
+        .publishedByPatchSet(rev.getPatchSet().getId())) {
+      if (uuid.equals(c.getKey().get())) {
+        return new CommentResource(rev, c);
+      }
+    }
+    throw new ResourceNotFoundException(id);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraft.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraft.java
index 348be97..5157af4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraft.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraft.java
@@ -42,7 +42,7 @@
   }
 
   @Override
-  public Response<GetDraft.Comment> apply(RevisionResource rsrc, Input in)
+  public Response<CommentInfo> apply(RevisionResource rsrc, Input in)
       throws AuthException, BadRequestException, ResourceConflictException, OrmException {
     if (Strings.isNullOrEmpty(in.path)) {
       throw new BadRequestException("path must be non-empty");
@@ -60,9 +60,9 @@
         rsrc.getAccountId(),
         Url.decode(in.inReplyTo));
     c.setStatus(Status.DRAFT);
-    c.setSide(in.side == GetDraft.Side.PARENT ? (short) 0 : (short) 1);
+    c.setSide(in.side == CommentInfo.Side.PARENT ? (short) 0 : (short) 1);
     c.setMessage(in.message.trim());
     db.get().patchComments().insert(Collections.singleton(c));
-    return Response.created(new GetDraft.Comment(c));
+    return Response.created(new CommentInfo(c, null));
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetComment.java
new file mode 100644
index 0000000..68b0435
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetComment.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.account.AccountInfo;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+class GetComment implements RestReadView<CommentResource> {
+
+  private final AccountInfo.Loader.Factory accountLoaderFactory;
+
+  @Inject
+  GetComment(AccountInfo.Loader.Factory accountLoaderFactory) {
+    this.accountLoaderFactory = accountLoaderFactory;
+  }
+
+  @Override
+  public Object apply(CommentResource rsrc) throws OrmException {
+    AccountInfo.Loader accountLoader = accountLoaderFactory.create(true);
+    CommentInfo ci = new CommentInfo(rsrc.getComment(), accountLoader);
+    accountLoader.fill();
+    return ci;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDetail.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDetail.java
index ae0d9e92..b3cd813 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDetail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDetail.java
@@ -27,7 +27,8 @@
     this.json = json
         .addOption(ListChangesOption.LABELS)
         .addOption(ListChangesOption.DETAILED_LABELS)
-        .addOption(ListChangesOption.DETAILED_ACCOUNTS);
+        .addOption(ListChangesOption.DETAILED_ACCOUNTS)
+        .addOption(ListChangesOption.MESSAGES);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDraft.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDraft.java
index 596659d..6b36048 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDraft.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDraft.java
@@ -14,49 +14,15 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.common.base.Strings;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.extensions.restapi.Url;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
-
-import java.sql.Timestamp;
 
 class GetDraft implements RestReadView<DraftResource> {
   @Override
   public Object apply(DraftResource rsrc) throws AuthException,
       BadRequestException, ResourceConflictException, Exception {
-    return new Comment(rsrc.getComment());
-  }
-
-  static enum Side {
-    PARENT, REVISION;
-  }
-
-  static class Comment {
-    final String kind = "gerritcodereview#comment";
-    String id;
-    String path;
-    Side side;
-    Integer line;
-    String inReplyTo;
-    String message;
-    Timestamp updated;
-
-    Comment(PatchLineComment c) {
-      id = Url.encode(c.getKey().get());
-      path = c.getKey().getParentKey().getFileName();
-      if (c.getSide() == 0) {
-        side = Side.PARENT;
-      }
-      if (c.getLine() > 0) {
-        line = c.getLine();
-      }
-      inReplyTo = Url.encode(c.getParentUuid());
-      message = Strings.emptyToNull(c.getMessage());
-      updated = c.getWrittenOn();
-    }
+    return new CommentInfo(rsrc.getComment(), null);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReview.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReview.java
index 997f5e7..08186fe 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReview.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReview.java
@@ -24,7 +24,8 @@
 
   @Inject
   GetReview(ChangeJson json) {
-    this.json = json.addOption(ListChangesOption.DETAILED_LABELS)
+    this.json = json
+        .addOption(ListChangesOption.DETAILED_LABELS)
         .addOption(ListChangesOption.DETAILED_ACCOUNTS);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListComments.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListComments.java
new file mode 100644
index 0000000..23a71e8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListComments.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.AccountInfo;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+class ListComments extends ListDrafts {
+  @Inject
+  ListComments(Provider<ReviewDb> db, AccountInfo.Loader.Factory alf) {
+    super(db, alf);
+  }
+
+  @Override
+  protected boolean includeAuthorInfo() {
+    return true;
+  }
+
+  protected Iterable<PatchLineComment> listComments(RevisionResource rsrc)
+      throws OrmException {
+    return db.get().patchComments()
+        .publishedByPatchSet(rsrc.getPatchSet().getId());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListDrafts.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListDrafts.java
index 564363e..47863ac 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListDrafts.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListDrafts.java
@@ -24,8 +24,10 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.change.GetDraft.Comment;
-import com.google.gerrit.server.change.GetDraft.Side;
+import com.google.gerrit.server.account.AccountInfo;
+import com.google.gerrit.server.change.CommentInfo;
+import com.google.gerrit.server.change.CommentInfo.Side;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
@@ -35,23 +37,36 @@
 import java.util.Map;
 
 class ListDrafts implements RestReadView<RevisionResource> {
-  private final Provider<ReviewDb> db;
+  protected final Provider<ReviewDb> db;
+  private final AccountInfo.Loader.Factory accountLoaderFactory;
 
   @Inject
-  ListDrafts(Provider<ReviewDb> db) {
+  ListDrafts(Provider<ReviewDb> db, AccountInfo.Loader.Factory alf) {
     this.db = db;
+    this.accountLoaderFactory = alf;
+  }
+
+  protected Iterable<PatchLineComment> listComments(RevisionResource rsrc)
+      throws OrmException {
+    return db.get().patchComments()
+        .draftByPatchSetAuthor(
+            rsrc.getPatchSet().getId(),
+            rsrc.getAccountId());
+  }
+
+  protected boolean includeAuthorInfo() {
+    return false;
   }
 
   @Override
   public Object apply(RevisionResource rsrc) throws AuthException,
       BadRequestException, ResourceConflictException, Exception {
-    Map<String, List<Comment>> out = Maps.newTreeMap();
-    for (PatchLineComment c : db.get().patchComments()
-        .draftByPatchSetAuthor(
-            rsrc.getPatchSet().getId(),
-            rsrc.getAccountId())) {
-      Comment o = new Comment(c);
-      List<Comment> list = out.get(o.path);
+    Map<String, List<CommentInfo>> out = Maps.newTreeMap();
+    AccountInfo.Loader accountLoader =
+        includeAuthorInfo() ? accountLoaderFactory.create(true) : null;
+    for (PatchLineComment c : listComments(rsrc)) {
+      CommentInfo o = new CommentInfo(c, accountLoader);
+      List<CommentInfo> list = out.get(o.path);
       if (list == null) {
         list = Lists.newArrayList();
         out.put(o.path, list);
@@ -59,10 +74,10 @@
       o.path = null;
       list.add(o);
     }
-    for (List<Comment> list : out.values()) {
-      Collections.sort(list, new Comparator<Comment>() {
+    for (List<CommentInfo> list : out.values()) {
+      Collections.sort(list, new Comparator<CommentInfo>() {
         @Override
-        public int compare(Comment a, Comment b) {
+        public int compare(CommentInfo a, CommentInfo b) {
           int c = firstNonNull(a.side, Side.REVISION).ordinal()
                 - firstNonNull(b.side, Side.REVISION).ordinal();
           if (c == 0) {
@@ -75,6 +90,9 @@
         }
       });
     }
+    if (accountLoader != null) {
+      accountLoader.fill();
+    }
     return out;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
index 7e175a7..690faf3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.change;
 
 import static com.google.gerrit.server.change.ChangeResource.CHANGE_KIND;
+import static com.google.gerrit.server.change.CommentResource.COMMENT_KIND;
 import static com.google.gerrit.server.change.DraftResource.DRAFT_KIND;
 import static com.google.gerrit.server.change.PatchResource.PATCH_KIND;
 import static com.google.gerrit.server.change.ReviewerResource.REVIEWER_KIND;
@@ -33,9 +34,11 @@
     bind(Revisions.class);
     bind(Reviewers.class);
     bind(Drafts.class);
+    bind(Comments.class);
     bind(Patches.class);
 
     DynamicMap.mapOf(binder(), CHANGE_KIND);
+    DynamicMap.mapOf(binder(), COMMENT_KIND);
     DynamicMap.mapOf(binder(), DRAFT_KIND);
     DynamicMap.mapOf(binder(), PATCH_KIND);
     DynamicMap.mapOf(binder(), REVIEWER_KIND);
@@ -70,6 +73,9 @@
     put(DRAFT_KIND).to(PutDraft.class);
     delete(DRAFT_KIND).to(DeleteDraft.class);
 
+    child(REVISION_KIND, "comments").to(Comments.class);
+    get(COMMENT_KIND).to(GetComment.class);
+
     child(REVISION_KIND, "files").to(Patches.class);
     put(PATCH_KIND, "reviewed").to(PutReviewed.class);
     delete(PATCH_KIND, "reviewed").to(DeleteReviewed.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
index 66b939b..9c14bcb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
@@ -92,7 +92,7 @@
 
   static class Comment {
     String id;
-    GetDraft.Side side;
+    CommentInfo.Side side;
     int line;
     String inReplyTo;
     String message;
@@ -132,6 +132,7 @@
       checkComments(input.comments);
     }
     if (input.notify == null) {
+      log.warn("notify = null; assuming notify = NONE");
       input.notify = NotifyHandling.NONE;
     }
 
@@ -288,7 +289,7 @@
         }
         e.setStatus(PatchLineComment.Status.PUBLISHED);
         e.setWrittenOn(timestamp);
-        e.setSide(c.side == GetDraft.Side.PARENT ? (short) 0 : (short) 1);
+        e.setSide(c.side == CommentInfo.Side.PARENT ? (short) 0 : (short) 1);
         e.setMessage(c.message);
         (create ? ins : upd).add(e);
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraft.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraft.java
index d5eaa9b..befb8d7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraft.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraft.java
@@ -23,7 +23,7 @@
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.change.GetDraft.Side;
+import com.google.gerrit.server.change.CommentInfo.Side;
 import com.google.gerrit.server.change.PutDraft.Input;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -85,12 +85,12 @@
     } else {
       db.get().patchComments().update(Collections.singleton(update(c, in)));
     }
-    return new GetDraft.Comment(c);
+    return new CommentInfo(c, null);
   }
 
   private PatchLineComment update(PatchLineComment e, Input in) {
     if (in.side != null) {
-      e.setSide(in.side == GetDraft.Side.PARENT ? (short) 0 : (short) 1);
+      e.setSide(in.side == CommentInfo.Side.PARENT ? (short) 0 : (short) 1);
     }
     if (in.line != null) {
       e.setLine(in.line);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
index c268530..154bd64 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
@@ -27,8 +27,6 @@
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.change.Revert.Input;
-import com.google.gerrit.server.config.CanonicalWebUrl;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.mail.RevertedSender;
@@ -42,8 +40,6 @@
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
 
-import javax.annotation.Nullable;
-
 public class Revert implements RestModifyView<ChangeResource, Input> {
   private final ChangeHooks hooks;
   private final RevertedSender.Factory revertedSenderFactory;
@@ -53,8 +49,7 @@
   private final GitRepositoryManager gitManager;
   private final PersonIdent myIdent;
   private final PatchSetInfoFactory patchSetInfoFactory;
-  private final GitReferenceUpdated gitRefUpdated;
-  private final String canonicalWebUrl;
+  private final ChangeInserter changeInserter;
 
   public static class Input {
     public String message;
@@ -68,9 +63,8 @@
       ChangeJson json,
       GitRepositoryManager gitManager,
       final PatchSetInfoFactory patchSetInfoFactory,
-      final GitReferenceUpdated gitRefUpdated,
       @GerritPersonIdent final PersonIdent myIdent,
-      @CanonicalWebUrl @Nullable final String canonicalWebUrl) {
+      final ChangeInserter changeInserter) {
     this.hooks = hooks;
     this.revertedSenderFactory = revertedSenderFactory;
     this.commitValidatorsFactory = commitValidatorsFactory;
@@ -78,9 +72,8 @@
     this.json = json;
     this.gitManager = gitManager;
     this.myIdent = myIdent;
-    this.gitRefUpdated = gitRefUpdated;
+    this.changeInserter = changeInserter;
     this.patchSetInfoFactory = patchSetInfoFactory;
-    this.canonicalWebUrl = canonicalWebUrl;
   }
 
   @Override
@@ -104,7 +97,7 @@
               commitValidators,
               Strings.emptyToNull(input.message), dbProvider.get(),
               revertedSenderFactory, hooks, git, patchSetInfoFactory,
-              gitRefUpdated, myIdent, canonicalWebUrl);
+              myIdent, changeInserter);
 
       return json.format(revertedChangeId);
     } catch (InvalidChangeOperationException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
index d774077..78202fc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
@@ -106,6 +106,10 @@
 
     checkSubmitRule(rsrc);
     change = submit(rsrc, caller);
+    if (change == null) {
+      throw new ResourceConflictException("change is "
+          + status(dbProvider.get().changes().get(rsrc.getChange().getId())));
+    }
 
     if (input.waitForMerge) {
       mergeQueue.merge(change.getDest());
@@ -123,19 +127,7 @@
       case MERGED:
         return new Output(Status.MERGED, change);
       case NEW:
-        // If the merge was attempted and it failed the system usually
-        // writes a comment as a ChangeMessage and sets status to NEW.
-        // Find the relevant message and report that as the conflict.
-        ChangeMessage msg = Iterables.getFirst(Iterables.filter(
-          Lists.reverse(dbProvider.get().changeMessages()
-              .byChange(change.getId())
-              .toList()),
-          new Predicate<ChangeMessage>() {
-            @Override
-            public boolean apply(ChangeMessage input) {
-              return input.getAuthor() == null;
-            }
-          }), null);
+        ChangeMessage msg = getConflictMessage(rsrc);
         if (msg != null) {
           throw new ResourceConflictException(msg.getMessage());
         }
@@ -144,8 +136,28 @@
     }
   }
 
-  private Change submit(RevisionResource rsrc, IdentifiedUser caller)
-      throws OrmException, ResourceConflictException {
+  /**
+   * If the merge was attempted and it failed the system usually writes a
+   * comment as a ChangeMessage and sets status to NEW. Find the relevant
+   * message and return it.
+   */
+  public ChangeMessage getConflictMessage(RevisionResource rsrc)
+      throws OrmException {
+    ChangeMessage msg = Iterables.getFirst(Iterables.filter(
+      Lists.reverse(dbProvider.get().changeMessages()
+          .byChange(rsrc.getChange().getId())
+          .toList()),
+      new Predicate<ChangeMessage>() {
+        @Override
+        public boolean apply(ChangeMessage input) {
+          return input.getAuthor() == null;
+        }
+      }), null);
+    return msg;
+  }
+
+  public Change submit(RevisionResource rsrc, IdentifiedUser caller)
+      throws OrmException {
     final Timestamp timestamp = new Timestamp(System.currentTimeMillis());
     Change change = rsrc.getChange();
     ReviewDb db = dbProvider.get();
@@ -167,8 +179,7 @@
           }
         });
       if (change == null) {
-        throw new ResourceConflictException("change is "
-            + status(db.changes().get(rsrc.getChange().getId())));
+        return null;
       }
       db.commit();
     } finally {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
index 89c507b..d7bf5a3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
@@ -16,7 +16,6 @@
 
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.ChangeHookRunner;
-import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
@@ -73,7 +72,6 @@
   private final GitReferenceUpdated gitRefUpdated;
   private final RebasedPatchSetSender.Factory rebasedPatchSetSenderFactory;
   private final ChangeHookRunner hooks;
-  private final ApprovalsUtil approvalsUtil;
   private final MergeUtil.Factory mergeUtilFactory;
   private final ProjectCache projectCache;
 
@@ -84,7 +82,7 @@
       final GitRepositoryManager gitManager,
       final GitReferenceUpdated gitRefUpdated,
       final RebasedPatchSetSender.Factory rebasedPatchSetSenderFactory,
-      final ChangeHookRunner hooks, final ApprovalsUtil approvalsUtil,
+      final ChangeHookRunner hooks,
       final MergeUtil.Factory mergeUtilFactory,
       final ProjectCache projectCache) {
     this.changeControlFactory = changeControlFactory;
@@ -95,7 +93,6 @@
     this.gitRefUpdated = gitRefUpdated;
     this.rebasedPatchSetSenderFactory = rebasedPatchSetSenderFactory;
     this.hooks = hooks;
-    this.approvalsUtil = approvalsUtil;
     this.mergeUtilFactory = mergeUtilFactory;
     this.projectCache = projectCache;
   }
@@ -384,10 +381,8 @@
             "Change %s was modified", change.getId()));
       }
 
-      final LabelTypes labelTypes =
-          projectCache.get(change.getProject()).getLabelTypes();
-      approvalsUtil.copyVetosToPatchSet(db, labelTypes,
-          change.currentPatchSetId());
+      ApprovalsUtil.copyLabels(db, projectCache.get(change.getProject())
+          .getLabelTypes(), patchSetId, change.currentPatchSetId());
 
       final ChangeMessage cmsg =
           new ChangeMessage(new ChangeMessage.Key(change.getId(),
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 1da611c..62c6863 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
@@ -28,6 +28,7 @@
 import com.google.gerrit.rules.PrologModule;
 import com.google.gerrit.rules.RulesCache;
 import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.FileTypeRegistry;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.InternalUser;
@@ -64,6 +65,7 @@
 import com.google.gerrit.server.auth.ldap.LdapModule;
 import com.google.gerrit.server.avatar.AvatarProvider;
 import com.google.gerrit.server.cache.CacheRemovalListener;
+import com.google.gerrit.server.change.ChangeInserter;
 import com.google.gerrit.server.events.EventFactory;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.ChangeCache;
@@ -93,6 +95,8 @@
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.AccessControlModule;
 import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.CommentLinkInfo;
+import com.google.gerrit.server.project.CommentLinkProvider;
 import com.google.gerrit.server.project.PerformCreateProject;
 import com.google.gerrit.server.project.PermissionCollection;
 import com.google.gerrit.server.project.ProjectCacheImpl;
@@ -112,6 +116,8 @@
 import org.apache.velocity.runtime.RuntimeInstance;
 import org.eclipse.jgit.lib.Config;
 
+import java.util.List;
+
 
 /** Starts global state with standard dependencies. */
 public class GerritGlobalModule extends FactoryModule {
@@ -211,6 +217,8 @@
     bind(EventFactory.class);
     bind(TransferConfig.class);
 
+    bind(ApprovalsUtil.class);
+    bind(ChangeInserter.class);
     bind(ChangeMergeQueue.class).in(SINGLETON);
     bind(MergeQueue.class).to(ChangeMergeQueue.class).in(SINGLETON);
     factory(ReloadSubmitQueueOp.Factory.class);
@@ -251,5 +259,8 @@
     bind(AccountManager.class);
     bind(ChangeUserName.CurrentUser.class);
     factory(ChangeUserName.Factory.class);
+
+    bind(new TypeLiteral<List<CommentLinkInfo>>() {})
+        .toProvider(CommentLinkProvider.class).in(SINGLETON);
   }
 }
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 ec14883..af14015 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
@@ -16,10 +16,8 @@
 
 import static com.google.inject.Scopes.SINGLETON;
 
-import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.RequestCleanup;
-import com.google.gerrit.server.change.ChangeInserter;
 import com.google.gerrit.server.changedetail.DeleteDraftPatchSet;
 import com.google.gerrit.server.changedetail.PublishDraft;
 import com.google.gerrit.server.git.BanCommit;
@@ -39,8 +37,6 @@
     bind(RequestCleanup.class).in(RequestScoped.class);
     bind(RequestScopedReviewDbProvider.class);
     bind(IdentifiedUser.RequestFactory.class).in(SINGLETON);
-    bind(ApprovalsUtil.class);
-    bind(ChangeInserter.class);
 
     bind(PerRequestProjectControlCache.class).in(RequestScoped.class);
     bind(ChangeControl.Factory.class).in(SINGLETON);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
index 8d76e90..2116c0c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
@@ -24,6 +24,10 @@
 /** Important paths within a {@link SitePath}. */
 @Singleton
 public final class SitePaths {
+  public static final String CSS_FILENAME = "GerritSite.css";
+  public static final String HEADER_FILENAME = "GerritSiteHeader.html";
+  public static final String FOOTER_FILENAME = "GerritSiteFooter.html";
+
   public final File site_path;
   public final File bin_dir;
   public final File etc_dir;
@@ -35,6 +39,7 @@
   public final File mail_dir;
   public final File hooks_dir;
   public final File static_dir;
+  public final File themes_dir;
 
   public final File gerrit_sh;
   public final File gerrit_war;
@@ -71,6 +76,7 @@
     mail_dir = new File(etc_dir, "mail");
     hooks_dir = new File(site_path, "hooks");
     static_dir = new File(site_path, "static");
+    themes_dir = new File(site_path, "themes");
 
     gerrit_sh = new File(bin_dir, "gerrit.sh");
     gerrit_war = new File(bin_dir, "gerrit.war");
@@ -85,9 +91,9 @@
     ssh_dsa = new File(etc_dir, "ssh_host_dsa_key");
     peer_keys = new File(etc_dir, "peer_keys");
 
-    site_css = new File(etc_dir, "GerritSite.css");
-    site_header = new File(etc_dir, "GerritSiteHeader.html");
-    site_footer = new File(etc_dir, "GerritSiteFooter.html");
+    site_css = new File(etc_dir, CSS_FILENAME);
+    site_header = new File(etc_dir, HEADER_FILENAME);
+    site_footer = new File(etc_dir, FOOTER_FILENAME);
     site_gitweb = new File(etc_dir, "gitweb_config.perl");
 
     if (site_path.exists()) {
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/data/AccountAttribute.java
similarity index 94%
rename from gerrit-server/src/main/java/com/google/gerrit/server/events/AccountAttribute.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/data/AccountAttribute.java
index 2d88b83..e5627c2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/AccountAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/AccountAttribute.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.events;
+package com.google.gerrit.server.data;
 
 public class AccountAttribute {
     public String name;
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/data/ApprovalAttribute.java
similarity index 94%
rename from gerrit-server/src/main/java/com/google/gerrit/server/events/ApprovalAttribute.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/data/ApprovalAttribute.java
index baa660c..3059be3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/ApprovalAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/ApprovalAttribute.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.events;
+package com.google.gerrit.server.data;
 
 public class ApprovalAttribute {
     public String type;
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/data/ChangeAttribute.java
similarity index 96%
rename from gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeAttribute.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/data/ChangeAttribute.java
index 5150b48..7339829 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/ChangeAttribute.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.events;
+package com.google.gerrit.server.data;
 
 import com.google.gerrit.reviewdb.client.Change;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/DependencyAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/DependencyAttribute.java
similarity index 94%
rename from gerrit-server/src/main/java/com/google/gerrit/server/events/DependencyAttribute.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/data/DependencyAttribute.java
index 47fbdac..4c796f2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/DependencyAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/DependencyAttribute.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.events;
+package com.google.gerrit.server.data;
 
 public class DependencyAttribute {
   public String id;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/MessageAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/MessageAttribute.java
similarity index 94%
rename from gerrit-server/src/main/java/com/google/gerrit/server/events/MessageAttribute.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/data/MessageAttribute.java
index 71b38b5..f18beba 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/MessageAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/MessageAttribute.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.events;
+package com.google.gerrit.server.data;
 
 public class MessageAttribute {
     public Long timestamp;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/PatchAttribute.java
similarity index 95%
rename from gerrit-server/src/main/java/com/google/gerrit/server/events/PatchAttribute.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/data/PatchAttribute.java
index 82f44a1..12ac30a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/PatchAttribute.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.events;
+package com.google.gerrit.server.data;
 
 import com.google.gerrit.reviewdb.client.Patch.ChangeType;
 
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/data/PatchSetAttribute.java
similarity index 96%
rename from gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetAttribute.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/data/PatchSetAttribute.java
index 1123e5f..79d82e3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/PatchSetAttribute.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.events;
+package com.google.gerrit.server.data;
 
 import java.util.List;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetCommentAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/PatchSetCommentAttribute.java
similarity index 94%
rename from gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetCommentAttribute.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/data/PatchSetCommentAttribute.java
index e0c8c13..7610068 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetCommentAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/PatchSetCommentAttribute.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.events;
+package com.google.gerrit.server.data;
 
 public class PatchSetCommentAttribute {
     public String file;
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/data/QueryStatsAttribute.java
similarity index 90%
rename from gerrit-server/src/main/java/com/google/gerrit/server/events/QueryStats.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/data/QueryStatsAttribute.java
index ecf2b9a..9897065 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/QueryStats.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/QueryStatsAttribute.java
@@ -12,9 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.events;
+package com.google.gerrit.server.data;
 
-public class QueryStats {
+public class QueryStatsAttribute {
   public final String type = "stats";
   public int rowCount;
   public long runTimeMilliseconds;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdateAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/RefUpdateAttribute.java
similarity index 94%
rename from gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdateAttribute.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/data/RefUpdateAttribute.java
index e4d715a..b3808d9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdateAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/RefUpdateAttribute.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.events;
+package com.google.gerrit.server.data;
 
 public class RefUpdateAttribute {
   public String oldRev;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/SubmitLabelAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/SubmitLabelAttribute.java
similarity index 94%
rename from gerrit-server/src/main/java/com/google/gerrit/server/events/SubmitLabelAttribute.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/data/SubmitLabelAttribute.java
index 99d0350..4c774c2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/SubmitLabelAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/SubmitLabelAttribute.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.events;
+package com.google.gerrit.server.data;
 
 public class SubmitLabelAttribute {
     public String label;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/SubmitRecordAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/SubmitRecordAttribute.java
similarity index 94%
rename from gerrit-server/src/main/java/com/google/gerrit/server/events/SubmitRecordAttribute.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/data/SubmitRecordAttribute.java
index 04b76e1..1ce2ce6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/SubmitRecordAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/SubmitRecordAttribute.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.events;
+package com.google.gerrit.server.data;
 
 import java.util.List;
 
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/data/TrackingIdAttribute.java
similarity index 94%
rename from gerrit-server/src/main/java/com/google/gerrit/server/events/TrackingIdAttribute.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/data/TrackingIdAttribute.java
index 7d55dd2..473ea43 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/TrackingIdAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/TrackingIdAttribute.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.events;
+package com.google.gerrit.server.data;
 
 public class TrackingIdAttribute {
   public String system;
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
index baaf30c..b0eb9c6 100644
--- 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
@@ -14,6 +14,10 @@
 
 package com.google.gerrit.server.events;
 
+import com.google.gerrit.server.data.AccountAttribute;
+import com.google.gerrit.server.data.ChangeAttribute;
+import com.google.gerrit.server.data.PatchSetAttribute;
+
 public class ChangeAbandonedEvent extends ChangeEvent {
     public final String type = "change-abandoned";
     public ChangeAttribute change;
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
index 0d5fc31..38996a5 100644
--- 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
@@ -14,6 +14,10 @@
 
 package com.google.gerrit.server.events;
 
+import com.google.gerrit.server.data.AccountAttribute;
+import com.google.gerrit.server.data.ChangeAttribute;
+import com.google.gerrit.server.data.PatchSetAttribute;
+
 public class ChangeMergedEvent extends ChangeEvent {
     public final String type = "change-merged";
     public ChangeAttribute change;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoredEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoredEvent.java
index 717e23c..e761190 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoredEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoredEvent.java
@@ -14,6 +14,10 @@
 
 package com.google.gerrit.server.events;
 
+import com.google.gerrit.server.data.AccountAttribute;
+import com.google.gerrit.server.data.ChangeAttribute;
+import com.google.gerrit.server.data.PatchSetAttribute;
+
 public class ChangeRestoredEvent extends ChangeEvent {
     public final String type = "change-restored";
     public ChangeAttribute change;
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
index f00caaf..52d7409 100644
--- 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
@@ -14,6 +14,11 @@
 
 package com.google.gerrit.server.events;
 
+import com.google.gerrit.server.data.AccountAttribute;
+import com.google.gerrit.server.data.ApprovalAttribute;
+import com.google.gerrit.server.data.ChangeAttribute;
+import com.google.gerrit.server.data.PatchSetAttribute;
+
 public class CommentAddedEvent extends ChangeEvent {
     public final String type = "comment-added";
     public ChangeAttribute change;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/DraftPublishedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/DraftPublishedEvent.java
index c90ac90..7fd033a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/DraftPublishedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/DraftPublishedEvent.java
@@ -14,6 +14,10 @@
 
 package com.google.gerrit.server.events;
 
+import com.google.gerrit.server.data.AccountAttribute;
+import com.google.gerrit.server.data.ChangeAttribute;
+import com.google.gerrit.server.data.PatchSetAttribute;
+
 public class DraftPublishedEvent extends ChangeEvent {
     public final String type = "draft-published";
     public ChangeAttribute change;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
index d556e73..63bfa71 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
@@ -33,6 +33,18 @@
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.data.AccountAttribute;
+import com.google.gerrit.server.data.ApprovalAttribute;
+import com.google.gerrit.server.data.ChangeAttribute;
+import com.google.gerrit.server.data.DependencyAttribute;
+import com.google.gerrit.server.data.MessageAttribute;
+import com.google.gerrit.server.data.PatchAttribute;
+import com.google.gerrit.server.data.PatchSetAttribute;
+import com.google.gerrit.server.data.PatchSetCommentAttribute;
+import com.google.gerrit.server.data.RefUpdateAttribute;
+import com.google.gerrit.server.data.SubmitLabelAttribute;
+import com.google.gerrit.server.data.SubmitRecordAttribute;
+import com.google.gerrit.server.data.TrackingIdAttribute;
 import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchListEntry;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/MergeFailedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/MergeFailedEvent.java
index e6ff525..599fe60 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/MergeFailedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/MergeFailedEvent.java
@@ -14,6 +14,10 @@
 
 package com.google.gerrit.server.events;
 
+import com.google.gerrit.server.data.AccountAttribute;
+import com.google.gerrit.server.data.ChangeAttribute;
+import com.google.gerrit.server.data.PatchSetAttribute;
+
 public class MergeFailedEvent extends ChangeEvent {
     public final String type = "merge-failed";
     public ChangeAttribute change;
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
index 15e3978..fbaf4ef 100644
--- 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
@@ -14,6 +14,10 @@
 
 package com.google.gerrit.server.events;
 
+import com.google.gerrit.server.data.AccountAttribute;
+import com.google.gerrit.server.data.ChangeAttribute;
+import com.google.gerrit.server.data.PatchSetAttribute;
+
 public class PatchSetCreatedEvent extends ChangeEvent {
     public final String type = "patchset-created";
     public ChangeAttribute change;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdatedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdatedEvent.java
index f90bc81..944c9ad 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdatedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdatedEvent.java
@@ -14,6 +14,9 @@
 
 package com.google.gerrit.server.events;
 
+import com.google.gerrit.server.data.AccountAttribute;
+import com.google.gerrit.server.data.RefUpdateAttribute;
+
 public class RefUpdatedEvent extends ChangeEvent {
   public final String type = "ref-updated";
   public AccountAttribute submitter;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ReviewerAddedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/ReviewerAddedEvent.java
index a881d8d..e00cc60 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/ReviewerAddedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/ReviewerAddedEvent.java
@@ -14,6 +14,10 @@
 
 package com.google.gerrit.server.events;
 
+import com.google.gerrit.server.data.AccountAttribute;
+import com.google.gerrit.server.data.ChangeAttribute;
+import com.google.gerrit.server.data.PatchSetAttribute;
+
 public class ReviewerAddedEvent extends ChangeEvent {
     public final String type = "reviewer-added";
     public ChangeAttribute change;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java
index 69dcb15..8595472 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.git;
 
-enum CommitMergeStatus {
+public enum CommitMergeStatus {
   /** */
   CLEAN_MERGE("Change has been successfully merged into the git repository."),
 
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 e93fe7e..3afbdd6 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
@@ -27,7 +27,6 @@
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.data.Capable;
-import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.SubmitTypeRecord;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
@@ -160,8 +159,7 @@
       final ProjectCache pc, final LabelNormalizer fs,
       final GitReferenceUpdated gru, final MergedSender.Factory msf,
       final MergeFailSender.Factory mfsf,
-      final LabelTypes labelTypes, final PatchSetInfoFactory psif,
-      final IdentifiedUser.GenericFactory iuf,
+      final PatchSetInfoFactory psif, final IdentifiedUser.GenericFactory iuf,
       final ChangeControl.GenericFactory changeControlFactory,
       final MergeQueue mergeQueue, @Assisted final Branch.NameKey branch,
       final ChangeHooks hooks, final AccountCache accountCache,
@@ -458,8 +456,7 @@
 
     try {
       for (final Ref r : repo.getAllRefs().values()) {
-        if (r.getName().startsWith(Constants.R_HEADS)
-            || r.getName().startsWith(Constants.R_TAGS)) {
+        if (r.getName().startsWith(Constants.R_HEADS)) {
           try {
             alreadyAccepted.add(rw.parseCommit(r.getObjectId()));
           } catch (IncorrectObjectTypeException iote) {
@@ -600,6 +597,11 @@
         try {
           if (rw.isMergedInto(commit, branchTip)) {
             commit.statusCode = CommitMergeStatus.ALREADY_MERGED;
+            try {
+              setMerged(chg, null);
+            } catch (OrmException e) {
+              log.error("Cannot mark change " + chg.getId() + " merged", e);
+            }
             continue;
           }
         } catch (IOException err) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
index 2e14dfb..bfaa97e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
@@ -14,12 +14,14 @@
 
 package com.google.gerrit.server.git;
 
+import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.gerrit.common.data.Permission.isPermission;
 
 import com.google.common.base.CharMatcher;
 import com.google.common.base.Joiner;
 import com.google.common.base.Objects;
 import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
@@ -45,6 +47,7 @@
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.mail.Address;
+import com.google.gerrit.server.project.CommentLinkInfo;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.CommitBuilder;
@@ -64,8 +67,16 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
 
 public class ProjectConfig extends VersionedMetaData {
+  public static final String COMMENTLINK = "commentlink";
+  private static final String KEY_MATCH = "match";
+  private static final String KEY_HTML = "html";
+  private static final String KEY_LINK = "link";
+  private static final String KEY_ENABLED = "enabled";
+
   private static final String PROJECT_CONFIG = "project.config";
   private static final String GROUP_LIST = "groups";
 
@@ -112,6 +123,7 @@
   private static final String KEY_ABBREVIATION = "abbreviation";
   private static final String KEY_FUNCTION = "function";
   private static final String KEY_COPY_MIN_SCORE = "copyMinScore";
+  private static final String KEY_COPY_MAX_SCORE = "copyMaxScore";
   private static final String KEY_VALUE = "value";
   private static final String KEY_CAN_OVERRIDE = "canOverride";
   private static final Set<String> LABEL_FUNCTIONS = ImmutableSet.of(
@@ -130,6 +142,7 @@
   private Map<String, ContributorAgreement> contributorAgreements;
   private Map<String, NotifyConfig> notifySections;
   private Map<String, LabelType> labelSections;
+  private List<CommentLinkInfo> commentLinkSections;
   private List<ValidationError> validationErrors;
   private ObjectId rulesId;
 
@@ -147,6 +160,42 @@
     return r;
   }
 
+  public static CommentLinkInfo buildCommentLink(Config cfg, String name,
+      boolean allowRaw) throws IllegalArgumentException {
+    String match = cfg.getString(COMMENTLINK, name, KEY_MATCH);
+    if (match != null) {
+      // Unfortunately this validation isn't entirely complete. Clients
+      // can have exceptions trying to evaluate the pattern if they don't
+      // support a token used, even if the server does support the token.
+      //
+      // At the minimum, we can trap problems related to unmatched groups.
+      Pattern.compile(match);
+    }
+
+    String link = cfg.getString(COMMENTLINK, name, KEY_LINK);
+    String html = cfg.getString(COMMENTLINK, name, KEY_HTML);
+    boolean hasHtml = !Strings.isNullOrEmpty(html);
+
+    String rawEnabled = cfg.getString(COMMENTLINK, name, KEY_ENABLED);
+    Boolean enabled;
+    if (rawEnabled != null) {
+      enabled = cfg.getBoolean(COMMENTLINK, name, KEY_ENABLED, true);
+    } else {
+      enabled = null;
+    }
+    checkArgument(allowRaw || !hasHtml, "Raw html replacement not allowed");
+
+    if (Strings.isNullOrEmpty(match) && Strings.isNullOrEmpty(link) && !hasHtml
+        && enabled != null) {
+      if (enabled) {
+        return new CommentLinkInfo.Enabled(name);
+      } else {
+        return new CommentLinkInfo.Disabled(name);
+      }
+    }
+    return new CommentLinkInfo(name, match, link, html, enabled);
+  }
+
   public ProjectConfig(Project.NameKey projectName) {
     this.projectName = projectName;
   }
@@ -232,6 +281,10 @@
     return labelSections;
   }
 
+  public Collection<CommentLinkInfo> getCommentLinkSections() {
+    return commentLinkSections;
+  }
+
   public GroupReference resolve(AccountGroup group) {
     return resolve(GroupReference.forGroup(group));
   }
@@ -332,6 +385,7 @@
     loadAccessSections(rc, groupsByName);
     loadNotifySections(rc, groupsByName);
     loadLabelSections(rc);
+    loadCommentLinkSections(rc);
   }
 
   private void loadAccountsSection(
@@ -582,12 +636,33 @@
       }
       label.setCopyMinScore(
           rc.getBoolean(LABEL, name, KEY_COPY_MIN_SCORE, false));
+      label.setCopyMaxScore(
+          rc.getBoolean(LABEL, name, KEY_COPY_MAX_SCORE, false));
       label.setCanOverride(
           rc.getBoolean(LABEL, name, KEY_CAN_OVERRIDE, true));
       labelSections.put(name, label);
     }
   }
 
+  private void loadCommentLinkSections(Config rc) {
+    Set<String> subsections = rc.getSubsections(COMMENTLINK);
+    commentLinkSections = Lists.newArrayListWithCapacity(subsections.size());
+    for (String name : subsections) {
+      try {
+        commentLinkSections.add(buildCommentLink(rc, name, false));
+      } catch (PatternSyntaxException e) {
+        error(new ValidationError(PROJECT_CONFIG, String.format(
+            "Invalid pattern \"%s\" in commentlink.%s.match: %s",
+            rc.getString(COMMENTLINK, name, KEY_MATCH), name, e.getMessage())));
+      } catch (IllegalArgumentException e) {
+        error(new ValidationError(PROJECT_CONFIG, String.format(
+            "Error in pattern \"%s\" in commentlink.%s.match: %s",
+            rc.getString(COMMENTLINK, name, KEY_MATCH), name, e.getMessage())));
+      }
+    }
+    commentLinkSections = ImmutableList.copyOf(commentLinkSections);
+  }
+
   private Map<String, GroupReference> readGroupList() throws IOException {
     groupsByUUID = new HashMap<AccountGroup.UUID, GroupReference>();
     Map<String, GroupReference> groupsByName =
@@ -849,6 +924,11 @@
       } else {
         rc.unset(LABEL, name, KEY_COPY_MIN_SCORE);
       }
+      if (label.isCopyMaxScore()) {
+        rc.setBoolean(LABEL, name, KEY_COPY_MAX_SCORE, true);
+      } else {
+        rc.unset(LABEL, name, KEY_COPY_MAX_SCORE);
+      }
       if (!label.canOverride()) {
         rc.setBoolean(LABEL, name, KEY_CAN_OVERRIDE, false);
       } else {
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 bcc30144..3e28b07 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
@@ -18,7 +18,6 @@
 import static com.google.gerrit.server.git.MultiProgressMonitor.UNKNOWN;
 import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromApprovals;
 import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromFooters;
-
 import static org.eclipse.jgit.lib.Constants.R_HEADS;
 import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
 import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;
@@ -30,12 +29,14 @@
 import com.google.common.base.Predicate;
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.BiMap;
+import com.google.common.collect.HashBiMap;
 import com.google.common.collect.HashMultimap;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.LinkedListMultimap;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
 import com.google.common.collect.SetMultimap;
 import com.google.common.collect.Sets;
 import com.google.common.util.concurrent.CheckedFuture;
@@ -63,6 +64,9 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.change.ChangeInserter;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.change.Submit;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.TrackingFooters;
@@ -91,6 +95,7 @@
 import com.google.gwtorm.server.ResultSet;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -130,6 +135,7 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -278,10 +284,13 @@
       new HashMap<RevCommit, ReplaceRequest>();
   private final Set<RevCommit> validCommits = new HashSet<RevCommit>();
 
+  private ListMultimap<Change.Id, Ref> refsByChange;
   private SetMultimap<ObjectId, Ref> refsById;
   private Map<String, Ref> allRefs;
 
   private final SubmoduleOp.Factory subOpFactory;
+  private final Provider<Submit> submitProvider;
+  private final MergeQueue mergeQueue;
 
   private final List<CommitValidationMessage> messages = new ArrayList<CommitValidationMessage>();
   private ListMultimap<Error, String> errors = LinkedListMultimap.create();
@@ -321,7 +330,9 @@
       ReceiveConfig config,
       @Assisted final ProjectControl projectControl,
       @Assisted final Repository repo,
-      final SubmoduleOp.Factory subOpFactory) throws IOException {
+      final SubmoduleOp.Factory subOpFactory,
+      final Provider<Submit> submitProvider,
+      final MergeQueue mergeQueue) throws IOException {
     this.currentUser = (IdentifiedUser) projectControl.getCurrentUser();
     this.db = db;
     this.schemaFactory = schemaFactory;
@@ -356,6 +367,8 @@
     this.rejectCommits = loadRejectCommitsMap();
 
     this.subOpFactory = subOpFactory;
+    this.submitProvider = submitProvider;
+    this.mergeQueue = mergeQueue;
 
     this.messageSender = new ReceivePackMessageSender();
 
@@ -998,6 +1011,10 @@
     RefControl ctl;
     Set<Account.Id> reviewer = Sets.newLinkedHashSet();
     Set<Account.Id> cc = Sets.newLinkedHashSet();
+    RevCommit baseCommit;
+
+    @Option(name = "--base", metaVar = "BASE", usage = "merge base of changes")
+    ObjectId base;
 
     @Option(name = "--topic", metaVar = "NAME", usage = "attach topic to changes")
     String topic;
@@ -1005,6 +1022,9 @@
     @Option(name = "--draft", usage = "mark new/updated changes as draft")
     boolean draft;
 
+    @Option(name = "--submit", usage = "immediately submit the change")
+    boolean submit;
+
     @Option(name = "-r", metaVar = "EMAIL", usage = "add reviewer to changes")
     void reviewer(Account.Id id) {
       reviewer.add(id);
@@ -1029,6 +1049,10 @@
       return draft;
     }
 
+    boolean isSubmit() {
+      return submit;
+    }
+
     MailRecipients getMailRecipients() {
       return new MailRecipients(reviewer, cc);
     }
@@ -1125,13 +1149,41 @@
       return;
     }
 
+    if (magicBranch.isDraft() && magicBranch.isSubmit()) {
+      reject(cmd, "cannot submit draft");
+      return;
+    }
+
+    if (magicBranch.isSubmit() && !projectControl.controlForRef(
+        MagicBranch.NEW_CHANGE + ref).canSubmit()) {
+      reject(cmd, "submit not allowed");
+    }
+
+    RevWalk walk = rp.getRevWalk();
+    if (magicBranch.base != null) {
+      try {
+        magicBranch.baseCommit = walk.parseCommit(magicBranch.base);
+      } catch (IncorrectObjectTypeException notCommit) {
+        reject(cmd, "base must be a commit");
+        return;
+      } catch (MissingObjectException e) {
+        reject(cmd, "base not found");
+        return;
+      } catch (IOException e) {
+        log.warn(String.format(
+            "Project %s cannot read %s",
+            project.getName(), magicBranch.base.name()), e);
+        reject(cmd, "internal server error");
+        return;
+      }
+    }
+
     // Validate that the new commits are connected with the target
     // branch.  If they aren't, we want to abort. We do this check by
     // looking to see if we can compute a merge base between the new
     // commits and the target branch head.
     //
     try {
-      final RevWalk walk = rp.getRevWalk();
       final RevCommit tip = walk.parseCommit(magicBranch.cmd.getNewId());
       Ref targetRef = rp.getAdvertisedRefs().get(magicBranch.ctl.getRefName());
       if (targetRef == null || targetRef.getObjectId() == null) {
@@ -1261,10 +1313,14 @@
     try {
       Set<ObjectId> existing = Sets.newHashSet();
       walk.markStart(walk.parseCommit(magicBranch.cmd.getNewId()));
-      markHeadsAsUninteresting(
-          walk,
-          existing,
-          magicBranch.ctl != null ? magicBranch.ctl.getRefName() : null);
+      if (magicBranch.baseCommit != null) {
+        walk.markUninteresting(magicBranch.baseCommit);
+      } else {
+        markHeadsAsUninteresting(
+            walk,
+            existing,
+            magicBranch.ctl != null ? magicBranch.ctl.getRefName() : null);
+      }
 
       List<ChangeLookup> pending = Lists.newArrayList();
       final Set<Change.Key> newChangeIds = new HashSet<Change.Key>();
@@ -1471,8 +1527,8 @@
       recipients.add(getRecipientsFromFooters(accountResolver, ps, footerLines));
       recipients.remove(me);
 
-      changeInserter.insertChange(db, change, ps, commit, labelTypes,
-          footerLines, info, recipients.getReviewers());
+      changeInserter.insertChange(db, change, ps, commit, labelTypes, info,
+          recipients.getReviewers());
 
       created = true;
 
@@ -1498,16 +1554,56 @@
           return "send-email newchange";
         }
       }));
+
+      if (magicBranch != null && magicBranch.isSubmit()) {
+        submit(projectControl.controlFor(change), ps);
+      }
+    }
+  }
+
+  private void submit(ChangeControl changeCtl, PatchSet ps) throws OrmException {
+    Submit submit = submitProvider.get();
+    RevisionResource rsrc = new RevisionResource(new ChangeResource(changeCtl), ps);
+    Change c = submit.submit(rsrc, currentUser);
+    if (c == null) {
+      addError("Submitting change " + changeCtl.getChange().getChangeId()
+          + " failed.");
+    } else {
+      addMessage("");
+      mergeQueue.merge(c.getDest());
+      c = db.changes().get(c.getId());
+      switch (c.getStatus()) {
+        case SUBMITTED:
+          addMessage("Change " + c.getChangeId() + " submitted.");
+          break;
+        case MERGED:
+          addMessage("Change " + c.getChangeId() + " merged.");
+          break;
+        case NEW:
+          ChangeMessage msg = submit.getConflictMessage(rsrc);
+          if (msg != null) {
+            addMessage("Change " + c.getChangeId() + ": " + msg.getMessage());
+            break;
+          }
+        default:
+          addMessage("change " + c.getChangeId() + " is "
+              + c.getStatus().name().toLowerCase());
+      }
     }
   }
 
   private void preparePatchSetsForReplace() {
     try {
       readChangesForReplace();
-      readPatchSetsForReplace();
-      for (ReplaceRequest req : replaceByChange.values()) {
+      for (Iterator<ReplaceRequest> itr = replaceByChange.values().iterator();
+          itr.hasNext();) {
+        ReplaceRequest req = itr.next();
         if (req.inputCommand.getResult() == NOT_ATTEMPTED) {
           req.validate(false);
+          if (req.skip && req.cmd == null) {
+            itr.remove();
+            replaceByCommit.remove(req.newCommit);
+          }
         }
       }
     } catch (OrmException err) {
@@ -1559,17 +1655,6 @@
     }
   }
 
-  private void readPatchSetsForReplace() throws OrmException {
-    Map<Change.Id, ResultSet<PatchSet>> results = Maps.newHashMap();
-    for (ReplaceRequest request : replaceByChange.values()) {
-      Change.Id id = request.ontoChange;
-      results.put(id, db.patchSets().byChange(id));
-    }
-    for (ReplaceRequest req : replaceByChange.values()) {
-      req.patchSets = results.get(req.ontoChange).toList();
-    }
-  }
-
   private class ReplaceRequest {
     final Change.Id ontoChange;
     final RevCommit newCommit;
@@ -1577,12 +1662,13 @@
     final boolean checkMergedInto;
     Change change;
     ChangeControl changeCtl;
-    List<PatchSet> patchSets;
+    BiMap<RevCommit, PatchSet.Id> revisions;
     PatchSet newPatchSet;
     ReceiveCommand cmd;
     PatchSetInfo info;
     ChangeMessage msg;
     String mergedIntoRef;
+    boolean skip;
     private PatchSet.Id priorPatchSet;
 
     ReplaceRequest(final Change.Id toChange, final RevCommit newCommit,
@@ -1591,20 +1677,39 @@
       this.newCommit = newCommit;
       this.inputCommand = cmd;
       this.checkMergedInto = checkMergedInto;
+
+      revisions = HashBiMap.create();
+      for (Ref ref : refs(toChange)) {
+        try {
+          revisions.forcePut(
+              rp.getRevWalk().parseCommit(ref.getObjectId()),
+              PatchSet.Id.fromRef(ref.getName()));
+        } catch (IOException err) {
+          log.warn(String.format(
+              "Project %s contains invalid change ref %s",
+              project.getName(), ref.getName()), err);
+        }
+      }
     }
 
     boolean validate(boolean autoClose) throws IOException {
       if (!autoClose && inputCommand.getResult() != NOT_ATTEMPTED) {
         return false;
-      }
-
-      if (change == null || patchSets == null) {
+      } else if (change == null) {
         reject(inputCommand, "change " + ontoChange + " not found");
         return false;
       }
 
-      if (change.getStatus().isClosed()) {
-        reject(inputCommand, "change " + ontoChange + " closed");
+      priorPatchSet = change.currentPatchSetId();
+      if (!revisions.containsValue(priorPatchSet)) {
+        reject(inputCommand, "change " + ontoChange + " missing revisions");
+        return false;
+      }
+
+      RevCommit priorCommit = revisions.inverse().get(priorPatchSet);
+      if (newCommit == priorCommit) {
+        // Ignore requests to make the change its current state.
+        skip = true;
         return false;
       }
 
@@ -1612,88 +1717,59 @@
       if (!changeCtl.canAddPatchSet()) {
         reject(inputCommand, "cannot replace " + ontoChange);
         return false;
+      } else if (change.getStatus().isClosed()) {
+        reject(inputCommand, "change " + ontoChange + " closed");
+        return false;
+      } else if (revisions.containsKey(newCommit)) {
+        reject(inputCommand, "commit already exists");
+        return false;
+      }
+
+      for (RevCommit prior : revisions.keySet()) {
+        // Don't allow a change to directly depend upon itself. This is a
+        // very common error due to users making a new commit rather than
+        // amending when trying to address review comments.
+        if (rp.getRevWalk().isMergedInto(prior, newCommit)) {
+          reject(inputCommand, "squash commits first");
+          return false;
+        }
       }
 
       rp.getRevWalk().parseBody(newCommit);
-
       if (!validCommit(changeCtl.getRefControl(), inputCommand, newCommit)) {
         return false;
       }
 
-      priorPatchSet = change.currentPatchSetId();
-      for (final PatchSet ps : patchSets) {
-        if (ps.getRevision() == null) {
-          log.warn("Patch set " + ps.getId() + " has no revision");
-          reject(inputCommand, "change state corrupt");
+      // Don't allow the same tree if the commit message is unmodified
+      // or no parents were updated (rebase), else warn that only part
+      // of the commit was modified.
+      if (newCommit.getTree() == priorCommit.getTree()) {
+        rp.getRevWalk().parseBody(priorCommit);
+        final boolean messageEq =
+            eq(newCommit.getFullMessage(), priorCommit.getFullMessage());
+        final boolean parentsEq = parentsEqual(newCommit, priorCommit);
+        final boolean authorEq = authorEqual(newCommit, priorCommit);
+
+        if (messageEq && parentsEq && authorEq && !autoClose) {
+          reject(inputCommand, "no changes made");
           return false;
-        }
-
-        final String revIdStr = ps.getRevision().get();
-        final ObjectId commitId;
-        try {
-          commitId = ObjectId.fromString(revIdStr);
-        } catch (IllegalArgumentException e) {
-          log.warn("Invalid revision in " + ps.getId() + ": " + revIdStr);
-          reject(inputCommand, "change state corrupt");
-          return false;
-        }
-
-        try {
-          final RevCommit prior = rp.getRevWalk().parseCommit(commitId);
-
-          // Don't allow the same commit to appear twice on the same change
-          //
-          if (newCommit == prior) {
-            reject(inputCommand, "commit already exists");
-            return false;
+        } else {
+          ObjectReader reader = rp.getRevWalk().getObjectReader();
+          StringBuilder msg = new StringBuilder();
+          msg.append("(W) ");
+          msg.append(reader.abbreviate(newCommit).name());
+          msg.append(":");
+          msg.append(" no files changed");
+          if (!authorEq) {
+            msg.append(", author changed");
           }
-
-          // Don't allow a change to directly depend upon itself. This is a
-          // very common error due to users making a new commit rather than
-          // amending when trying to address review comments.
-          //
-          if (rp.getRevWalk().isMergedInto(prior, newCommit)) {
-            reject(inputCommand, "squash commits first");
-            return false;
+          if (!messageEq) {
+            msg.append(", message updated");
           }
-
-          // Don't allow the same tree if the commit message is unmodified
-          // or no parents were updated (rebase), else warn that only part
-          // of the commit was modified.
-          //
-          if (priorPatchSet.equals(ps.getId()) && newCommit.getTree() == prior.getTree()) {
-            rp.getRevWalk().parseBody(prior);
-            final boolean messageEq =
-                eq(newCommit.getFullMessage(), prior.getFullMessage());
-            final boolean parentsEq = parentsEqual(newCommit, prior);
-            final boolean authorEq = authorEqual(newCommit, prior);
-
-            if (messageEq && parentsEq && authorEq && !autoClose) {
-              reject(inputCommand, "no changes made");
-              return false;
-            } else {
-              ObjectReader reader = rp.getRevWalk().getObjectReader();
-              StringBuilder msg = new StringBuilder();
-              msg.append("(W) ");
-              msg.append(reader.abbreviate(newCommit).name());
-              msg.append(":");
-              msg.append(" no files changed");
-              if (!authorEq) {
-                msg.append(", author changed");
-              }
-              if (!messageEq) {
-                msg.append(", message updated");
-              }
-              if (!parentsEq) {
-                msg.append(", was rebased");
-              }
-              addMessage(msg.toString());
-            }
+          if (!parentsEq) {
+            msg.append(", was rebased");
           }
-        } catch (IOException e) {
-          log.error("Change " + change.getId() + " missing " + revIdStr, e);
-          reject(inputCommand, "change state corrupt");
-          return false;
+          addMessage(msg.toString());
         }
       }
 
@@ -1770,10 +1846,12 @@
           mergedIntoRef = mergedInto != null ? mergedInto.getName() : null;
         }
 
-        List<PatchSetApproval> patchSetApprovals =
-            approvalsUtil.copyVetosToPatchSet(db, labelTypes, newPatchSet.getId());
-        final MailRecipients oldRecipients =
-            getRecipientsFromApprovals(patchSetApprovals);
+        List<PatchSetApproval> oldChangeApprovals =
+            db.patchSetApprovals().byChange(change.getId()).toList();
+        final MailRecipients oldRecipients = getRecipientsFromApprovals(
+            oldChangeApprovals);
+        ApprovalsUtil.copyLabels(db, labelTypes, oldChangeApprovals,
+            priorPatchSet, newPatchSet.getId());
         approvalsUtil.addReviewers(db, labelTypes, change, newPatchSet, info,
             recipients.getReviewers(), oldRecipients.getAll());
         recipients.add(oldRecipients);
@@ -1879,10 +1957,30 @@
           return "send-email newpatchset";
         }
       }));
+
+      if (magicBranch != null && magicBranch.isSubmit()) {
+        submit(changeCtl, newPatchSet);
+      }
+
       return newPatchSet.getId();
     }
   }
 
+  private List<Ref> refs(Change.Id changeId) {
+    if (refsByChange == null) {
+      int estRefsPerChange = 4;
+      refsByChange = ArrayListMultimap.create(
+          allRefs.size() / estRefsPerChange,
+          estRefsPerChange);
+      for (Ref ref : allRefs.values()) {
+        if (ref.getObjectId() != null && PatchSet.isRef(ref.getName())) {
+          refsByChange.put(Change.Id.fromRef(ref.getName()), ref);
+        }
+      }
+    }
+    return refsByChange.get(changeId);
+  }
+
   static boolean parentsEqual(RevCommit a, RevCommit b) {
     if (a.getParentCount() != b.getParentCount()) {
       return false;
@@ -2038,7 +2136,6 @@
           if (onto != null) {
             final ReplaceRequest req = new ReplaceRequest(onto, c, cmd, false);
             req.change = db.changes().get(onto);
-            req.patchSets = db.patchSets().byChange(onto).toList();
             toClose.add(req);
             break;
           }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReviewNoteHeaderFormatter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReviewNoteHeaderFormatter.java
deleted file mode 100644
index 71dbf87..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReviewNoteHeaderFormatter.java
+++ /dev/null
@@ -1,112 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.git;
-
-import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.Locale;
-import java.util.TimeZone;
-
-/**
- * Formatters for code review note headers.
- * <p>
- * This class provides a builder like interface for building the content of a
- * code review note. After instantiation, call as many as necessary
- * <code>append...(...)</code> methods and, at the end, call the
- * {@link #toString()} method to get the built note content.
- */
-class ReviewNoteHeaderFormatter {
-
-  private final DateFormat rfc2822DateFormatter;
-  private final String anonymousCowardName;
-  private final StringBuilder sb = new StringBuilder();
-
-  ReviewNoteHeaderFormatter(TimeZone tz, String anonymousCowardName) {
-    rfc2822DateFormatter =
-        new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US);
-    rfc2822DateFormatter.setCalendar(Calendar.getInstance(tz, Locale.US));
-    this.anonymousCowardName = anonymousCowardName;
-  }
-
-  void appendChangeId(Change.Key changeKey) {
-    sb.append("Change-Id: ").append(changeKey.get()).append("\n");
-  }
-
-  void appendApproval(LabelType type, short value, Account user) {
-    sb.append(type.getName());
-    sb.append(value < 0 ? "-" : "+").append(Math.abs(value)).append(": ");
-    appendUserData(user);
-    sb.append("\n");
-  }
-
-  private void appendUserData(Account user) {
-    boolean needSpace = false;
-    boolean wroteData = false;
-
-    if (user.getFullName() != null && ! user.getFullName().isEmpty()) {
-      sb.append(user.getFullName());
-      needSpace = true;
-      wroteData = true;
-    }
-
-    if (user.getPreferredEmail() != null && ! user.getPreferredEmail().isEmpty()) {
-      if (needSpace) {
-        sb.append(" ");
-      }
-      sb.append("<").append(user.getPreferredEmail()).append(">");
-      wroteData = true;
-    }
-
-    if (!wroteData) {
-      sb.append(anonymousCowardName).append(" #").append(user.getId());
-    }
-  }
-
-  void appendProject(String projectName) {
-    sb.append("Project: ").append(projectName).append("\n");
-  }
-
-  void appendBranch(Branch.NameKey branch) {
-    sb.append("Branch: ").append(branch.get()).append("\n");
-  }
-
-  void appendSubmittedBy(Account user) {
-    sb.append("Submitted-by: ");
-    appendUserData(user);
-    sb.append("\n");
-  }
-
-  void appendSubmittedAt(Date date) {
-    sb.append("Submitted-at: ").append(rfc2822DateFormatter.format(date))
-        .append("\n");
-  }
-
-  void appendReviewedOn(String canonicalWebUrl, Change.Id changeId) {
-    sb.append("Reviewed-on: ").append(canonicalWebUrl).append(changeId.get())
-        .append("\n");
-  }
-
-  @Override
-  public String toString() {
-    return sb.toString();
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
index 885f1aa..39d0ff8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
@@ -329,6 +329,11 @@
         add(RecipientType.CC, ap.getAccountId());
       }
     } catch (OrmException err) {
+      if (includeZero) {
+        log.warn("Cannot CC users that commented on updated change", err);
+      } else {
+        log.warn("Cannot CC users that reviewed updated change", err);
+      }
     }
   }
 
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 997bc03..ce50002 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
@@ -268,7 +268,7 @@
   protected boolean shouldSendMessage() {
     if (body.length() == 0) {
       // If we have no message body, don't send.
-      //
+      log.warn("Skipping delivery of email with no body");
       return false;
     }
 
@@ -276,7 +276,7 @@
       // 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.
-      //
+      log.info("Skipping delivery of email with no recipients");
       return false;
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkInfo.java
new file mode 100644
index 0000000..4035c7e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkInfo.java
@@ -0,0 +1,92 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.base.Strings;
+
+/** Info about a single commentlink section in a config. */
+public class CommentLinkInfo {
+  public static class Enabled extends CommentLinkInfo {
+    public Enabled(String name) {
+      super(name, true);
+    }
+
+    @Override
+    boolean isOverrideOnly() {
+      return true;
+    }
+  }
+
+  public static class Disabled extends CommentLinkInfo {
+    public Disabled(String name) {
+      super(name, false);
+    }
+
+    @Override
+    boolean isOverrideOnly() {
+      return true;
+    }
+  }
+
+  public final String match;
+  public final String link;
+  public final String html;
+  public final Boolean enabled; // null means true
+
+  public transient final String name;
+
+  public CommentLinkInfo(String name, String match, String link, String html,
+      Boolean enabled) {
+    checkArgument(name != null, "invalid commentlink.name");
+    checkArgument(!Strings.isNullOrEmpty(match),
+        "invalid commentlink.%s.match", name);
+    link = Strings.emptyToNull(link);
+    html = Strings.emptyToNull(html);
+    checkArgument(
+        (link != null && html == null) || (link == null && html != null),
+        "commentlink.%s must have either link or html", name);
+    this.name = name;
+    this.match = match;
+    this.link = link;
+    this.html = html;
+    this.enabled = enabled;
+  }
+
+  private CommentLinkInfo(CommentLinkInfo src, boolean enabled) {
+    this.name = src.name;
+    this.match = src.match;
+    this.link = src.link;
+    this.html = src.html;
+    this.enabled = enabled;
+  }
+
+  private CommentLinkInfo(String name, boolean enabled) {
+    this.name = name;
+    this.match = null;
+    this.link = null;
+    this.html = null;
+    this.enabled = enabled;
+  }
+
+  boolean isOverrideOnly() {
+    return false;
+  }
+
+  CommentLinkInfo inherit(CommentLinkInfo src) {
+    return new CommentLinkInfo(src, enabled);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkProvider.java
new file mode 100644
index 0000000..114ab90
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkProvider.java
@@ -0,0 +1,53 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.ProvisionException;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.util.List;
+import java.util.Set;
+
+public class CommentLinkProvider implements Provider<List<CommentLinkInfo>> {
+  private final Config cfg;
+
+  @Inject
+  CommentLinkProvider(@GerritServerConfig Config cfg) {
+    this.cfg = cfg;
+  }
+
+  @Override
+  public List<CommentLinkInfo> get() {
+    Set<String> subsections = cfg.getSubsections(ProjectConfig.COMMENTLINK);
+    List<CommentLinkInfo> cls =
+        Lists.newArrayListWithCapacity(subsections.size());
+    for (String name : subsections) {
+      CommentLinkInfo cl = ProjectConfig.buildCommentLink(cfg, name, true);
+      if (cl.isOverrideOnly()) {
+        throw new ProvisionException(
+            "commentlink " + name + " empty except for \"enabled\"");
+      }
+      cls.add(cl);
+    }
+    return ImmutableList.copyOf(cls);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java
new file mode 100644
index 0000000..cd5e5d7
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java
@@ -0,0 +1,94 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
+import com.google.gerrit.server.git.GitRepositoryManager;
+
+import java.util.Map;
+
+public class GetConfig implements RestReadView<ProjectResource> {
+
+  @Override
+  public ConfigInfo apply(ProjectResource resource) {
+    ConfigInfo result = new ConfigInfo();
+    RefControl refConfig = resource.getControl()
+        .controlForRef(GitRepositoryManager.REF_CONFIG);
+    ProjectState state = resource.getControl().getProjectState();
+    if (refConfig.isVisible()) {
+      InheritedBooleanInfo useContributorAgreements = new InheritedBooleanInfo();
+      InheritedBooleanInfo useSignedOffBy = new InheritedBooleanInfo();
+      InheritedBooleanInfo useContentMerge = new InheritedBooleanInfo();
+      InheritedBooleanInfo requireChangeId = new InheritedBooleanInfo();
+
+      useContributorAgreements.value = state.isUseContributorAgreements();
+      useSignedOffBy.value = state.isUseSignedOffBy();
+      useContentMerge.value = state.isUseContentMerge();
+      requireChangeId.value = state.isRequireChangeID();
+
+      Project p = state.getProject();
+      useContributorAgreements.configuredValue = p.getUseContributorAgreements();
+      useSignedOffBy.configuredValue = p.getUseSignedOffBy();
+      useContentMerge.configuredValue = p.getUseContentMerge();
+      requireChangeId.configuredValue = p.getRequireChangeID();
+
+      ProjectState parentState = Iterables.getFirst(state.parents(), null);
+      if (parentState != null) {
+        useContributorAgreements.inheritedValue = parentState.isUseContributorAgreements();
+        useSignedOffBy.inheritedValue = parentState.isUseSignedOffBy();
+        useContentMerge.inheritedValue = parentState.isUseContentMerge();
+        requireChangeId.inheritedValue = parentState.isRequireChangeID();
+      }
+
+      result.useContributorAgreements = useContributorAgreements;
+      result.useSignedOffBy = useSignedOffBy;
+      result.useContentMerge = useContentMerge;
+      result.requireChangeId = requireChangeId;
+    }
+
+    // commentlinks are visible to anyone, as they are used for linkification
+    // on the client side.
+    result.commentlinks = Maps.newLinkedHashMap();
+    for (CommentLinkInfo cl : state.getCommentLinks()) {
+      result.commentlinks.put(cl.name, cl);
+    }
+
+    // Themes are visible to anyone, as they are rendered client-side.
+    result.theme = state.getTheme();
+    return result;
+  }
+
+  public static class ConfigInfo {
+    public final String kind = "gerritcodereview#project_config";
+
+    public InheritedBooleanInfo useContributorAgreements;
+    public InheritedBooleanInfo useContentMerge;
+    public InheritedBooleanInfo useSignedOffBy;
+    public InheritedBooleanInfo requireChangeId;
+
+    public Map<String, CommentLinkInfo> commentlinks;
+    public ThemeInfo theme;
+  }
+
+  public static class InheritedBooleanInfo {
+    public Boolean value;
+    public InheritableBoolean configuredValue;
+    public Boolean inheritedValue;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
index 94e3162..1c61d96 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
@@ -51,5 +51,7 @@
     put(DASHBOARD_KIND).to(SetDashboard.class);
     delete(DASHBOARD_KIND).to(DeleteDashboard.class);
     install(new FactoryModuleBuilder().build(CreateProject.Factory.class));
+
+    get(PROJECT_KIND, "config").to(GetConfig.class);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
index 6150a3f..6f80841 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
@@ -14,11 +14,14 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.common.base.Charsets;
 import com.google.common.base.Function;
 import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.common.io.Files;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.LabelType;
@@ -34,6 +37,7 @@
 import com.google.gerrit.server.account.CapabilityCollection;
 import com.google.gerrit.server.account.GroupMembership;
 import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.inject.Inject;
@@ -44,7 +48,10 @@
 
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
+import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.ArrayList;
@@ -58,17 +65,22 @@
 
 /** Cached information on a project. */
 public class ProjectState {
+  private static final Logger log =
+      LoggerFactory.getLogger(ProjectState.class);
+
   public interface Factory {
     ProjectState create(ProjectConfig config);
   }
 
   private final boolean isAllProjects;
+  private final SitePaths sitePaths;
   private final AllProjectsName allProjectsName;
   private final ProjectCache projectCache;
   private final ProjectControl.AssistedFactory projectControlFactory;
   private final PrologEnvironment.Factory envFactory;
   private final GitRepositoryManager gitMgr;
   private final RulesCache rulesCache;
+  private final List<CommentLinkInfo> commentLinks;
 
   private final ProjectConfig config;
   private final Set<AccountGroup.UUID> localOwners;
@@ -82,18 +94,24 @@
   /** Local access sections, wrapped in SectionMatchers for faster evaluation. */
   private volatile List<SectionMatcher> localAccessSections;
 
+  /** Theme information loaded from site_path/themes. */
+  private volatile ThemeInfo theme;
+
   /** If this is all projects, the capabilities used by the server. */
   private final CapabilityCollection capabilities;
 
   @Inject
   public ProjectState(
+      final SitePaths sitePaths,
       final ProjectCache projectCache,
       final AllProjectsName allProjectsName,
       final ProjectControl.AssistedFactory projectControlFactory,
       final PrologEnvironment.Factory envFactory,
       final GitRepositoryManager gitMgr,
       final RulesCache rulesCache,
+      final List<CommentLinkInfo> commentLinks,
       @Assisted final ProjectConfig config) {
+    this.sitePaths = sitePaths;
     this.projectCache = projectCache;
     this.isAllProjects = config.getProject().getNameKey().equals(allProjectsName);
     this.allProjectsName = allProjectsName;
@@ -101,6 +119,7 @@
     this.envFactory = envFactory;
     this.gitMgr = gitMgr;
     this.rulesCache = rulesCache;
+    this.commentLinks = commentLinks;
     this.config = config;
     this.capabilities = isAllProjects
       ? new CapabilityCollection(config.getAccessSection(AccessSection.GLOBAL_CAPABILITIES))
@@ -293,6 +312,16 @@
   }
 
   /**
+   * @return an iterable that walks in-order from All-Projects through the
+   *     project hierarchy to this project.
+   */
+  public Iterable<ProjectState> treeInOrder() {
+    List<ProjectState> projects = Lists.newArrayList(tree());
+    Collections.reverse(projects);
+    return projects;
+  }
+
+  /**
    * @return an iterable that walks through the parents of this project. Starts
    *         from the immediate parent of this project and progresses up the
    *         hierarchy to All-Projects.
@@ -343,9 +372,7 @@
 
   public LabelTypes getLabelTypes() {
     Map<String, LabelType> types = Maps.newLinkedHashMap();
-    List<ProjectState> projects = Lists.newArrayList(tree());
-    Collections.reverse(projects);
-    for (ProjectState s : projects) {
+    for (ProjectState s : treeInOrder()) {
       for (LabelType type : s.getConfig().getLabelSections().values()) {
         String lower = type.getName().toLowerCase();
         LabelType old = types.get(lower);
@@ -363,6 +390,69 @@
     return new LabelTypes(Collections.unmodifiableList(all));
   }
 
+  public List<CommentLinkInfo> getCommentLinks() {
+    Map<String, CommentLinkInfo> cls = Maps.newLinkedHashMap();
+    for (CommentLinkInfo cl : commentLinks) {
+      cls.put(cl.name.toLowerCase(), cl);
+    }
+    for (ProjectState s : treeInOrder()) {
+      for (CommentLinkInfo cl : s.getConfig().getCommentLinkSections()) {
+        String name = cl.name.toLowerCase();
+        if (cl.isOverrideOnly()) {
+          CommentLinkInfo parent = cls.get(name);
+          if (parent == null) {
+            continue; // Ignore invalid overrides.
+          }
+          cls.put(name, cl.inherit(parent));
+        } else {
+          cls.put(name, cl);
+        }
+      }
+    }
+    return ImmutableList.copyOf(cls.values());
+  }
+
+  public ThemeInfo getTheme() {
+    ThemeInfo theme = this.theme;
+    if (theme == null) {
+      synchronized (this) {
+        theme = this.theme;
+        if (theme == null) {
+          theme = loadTheme();
+          this.theme = theme;
+        }
+      }
+    }
+    if (theme == ThemeInfo.INHERIT) {
+      ProjectState parent = Iterables.getFirst(parents(), null);
+      return parent != null ? parent.getTheme() : null;
+    }
+    return theme;
+  }
+
+  private ThemeInfo loadTheme() {
+    String name = getConfig().getProject().getName();
+    File dir = new File(sitePaths.themes_dir, name);
+    if (!dir.exists()) {
+      return ThemeInfo.INHERIT;
+    } else if (!dir.isDirectory()) {
+      log.warn("Bad theme for {}: not a directory", name);
+      return ThemeInfo.INHERIT;
+    }
+    try {
+      return new ThemeInfo(readFile(new File(dir, SitePaths.CSS_FILENAME)),
+          readFile(new File(dir, SitePaths.HEADER_FILENAME)),
+          readFile(new File(dir, SitePaths.FOOTER_FILENAME)));
+    } catch (IOException e) {
+      log.error("Error reading theme for " + name, e);
+      return ThemeInfo.INHERIT;
+    }
+  }
+
+  private String readFile(File f) throws IOException {
+    return f.exists() ? Files.toString(f, Charsets.UTF_8) : null;
+  }
+
   private boolean getInheritableBoolean(Function<Project, InheritableBoolean> func) {
     for (ProjectState s : tree()) {
       switch (func.apply(s.getProject())) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ThemeInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ThemeInfo.java
new file mode 100644
index 0000000..8362b572
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ThemeInfo.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.package com.google.gerrit.server.git;
+
+package com.google.gerrit.server.project;
+
+public class ThemeInfo {
+  static final ThemeInfo INHERIT = new ThemeInfo(null, null, null);
+
+  public final String css;
+  public final String header;
+  public final String footer;
+
+  ThemeInfo(String css, String header, String footer) {
+    this.css = css;
+    this.header = header;
+    this.footer = footer;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 7bbb073..e74172e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -367,7 +367,7 @@
 
     // If its not an account, maybe its a group?
     //
-    Collection<GroupReference> suggestions = args.groupBackend.suggest(who);
+    Collection<GroupReference> suggestions = args.groupBackend.suggest(who, null);
     if (!suggestions.isEmpty()) {
       HashSet<AccountGroup.UUID> ids = new HashSet<AccountGroup.UUID>();
       for (GroupReference ref : suggestions) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java
index f016c37..0ea280d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java
@@ -14,21 +14,12 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.query.OperatorPredicate;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.AnyObjectId;
-import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.revwalk.filter.MessageRevFilter;
@@ -42,78 +33,31 @@
  * Predicate to match changes that contains specified text in commit messages
  * body.
  */
-public class MessagePredicate extends OperatorPredicate<ChangeData> {
+public class MessagePredicate extends RevWalkPredicate {
 
   private static final Logger log =
       LoggerFactory.getLogger(MessagePredicate.class);
 
-  private final Provider<ReviewDb> db;
-  private final GitRepositoryManager repoManager;
   private final RevFilter rFilter;
 
   public MessagePredicate(Provider<ReviewDb> db,
       GitRepositoryManager repoManager, String text) {
-    super(ChangeQueryBuilder.FIELD_MESSAGE, text);
-    this.db = db;
-    this.repoManager = repoManager;
+    super(db, repoManager, ChangeQueryBuilder.FIELD_MESSAGE, text);
     this.rFilter = MessageRevFilter.create(text);
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
-    final PatchSet patchSet = object.currentPatchSet(db);
-
-    if (patchSet == null) {
-      return false;
-    }
-
-    final RevId revision = patchSet.getRevision();
-
-    if (revision == null) {
-      return false;
-    }
-
-    final AnyObjectId objectId = ObjectId.fromString(revision.get());
-
-    if (objectId == null) {
-      return false;
-    }
-
-    final Change change = object.change(db);
-
-    if (change == null) {
-      return false;
-    }
-
-    final Project.NameKey projectName = change.getProject();
-
-    if (projectName == null) {
-      return false;
-    }
-
+  public boolean match(Repository repo, RevWalk rw, Arguments args) {
     try {
-      final Repository repo = repoManager.openRepository(projectName);
-      try {
-        final RevWalk rw = new RevWalk(repo);
-        try {
-          return rFilter.include(rw, rw.parseCommit(objectId));
-        } finally {
-          rw.release();
-        }
-      } finally {
-        repo.close();
-      }
-    } catch (RepositoryNotFoundException e) {
-      log.error("Repository \"" + projectName.get() + "\" unknown.", e);
+      return rFilter.include(rw, rw.parseCommit(args.objectId));
     } catch (MissingObjectException e) {
-      log.error(projectName.get() + "\" commit does not exist.", e);
+      log.error(args.projectName.get() + "\" commit does not exist.", e);
     } catch (IncorrectObjectTypeException e) {
-      log.error(projectName.get() + "\" revision is not a commit.", e);
+      log.error(args.projectName.get() + "\" revision is not a commit.", e);
     } catch (IOException e) {
-      log.error("Could not search for commit message in \"" + projectName.get()
-          + "\" repository.", e);
+      log.error("Could not search for commit message in \"" +
+          args.projectName.get() + "\" repository.", e);
     }
-
     return false;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
index fc35df3..06d84c1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
@@ -21,10 +21,10 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.events.ChangeAttribute;
+import com.google.gerrit.server.data.ChangeAttribute;
+import com.google.gerrit.server.data.PatchSetAttribute;
+import com.google.gerrit.server.data.QueryStatsAttribute;
 import com.google.gerrit.server.events.EventFactory;
-import com.google.gerrit.server.events.PatchSetAttribute;
-import com.google.gerrit.server.events.QueryStats;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
@@ -267,7 +267,7 @@
       }
 
       try {
-        final QueryStats stats = new QueryStats();
+        final QueryStatsAttribute stats = new QueryStatsAttribute();
         stats.runTimeMilliseconds = System.currentTimeMillis();
 
         List<ChangeData> results = queryChanges(queryString);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RevWalkPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RevWalkPredicate.java
new file mode 100644
index 0000000..58c0fee
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RevWalkPredicate.java
@@ -0,0 +1,126 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+/**
+ * Predicate which creates Repository, RevWalk objects and properly
+ * closes them. Git based operators should extend this predicate.
+ *
+ */
+public abstract class RevWalkPredicate extends OperatorPredicate<ChangeData> {
+  private static final Logger log =
+      LoggerFactory.getLogger(RevWalkPredicate.class);
+
+  public static class Arguments {
+    public final PatchSet patchSet;
+    public final RevId revision;
+    public final AnyObjectId objectId;
+    public final Change change;
+    public final Project.NameKey projectName;
+
+    public Arguments(PatchSet patchSet,
+        RevId revision,
+        AnyObjectId objectId,
+        Change change,
+        Project.NameKey projectName) {
+      this.patchSet = patchSet;
+      this.revision = revision;
+      this.objectId = objectId;
+      this.change = change;
+      this.projectName = projectName;
+    }
+  }
+
+  public final Provider<ReviewDb> db;
+  public final GitRepositoryManager repoManager;
+
+  public RevWalkPredicate(Provider<ReviewDb> db,
+      GitRepositoryManager repoManager, String operator, String ref) {
+    super(operator, ref);
+    this.db = db;
+    this.repoManager = repoManager;
+  }
+
+  @Override
+  public boolean match(ChangeData object) throws OrmException {
+    final PatchSet patchSet = object.currentPatchSet(db);
+    if (patchSet == null) {
+      return false;
+    }
+
+    final RevId revision = patchSet.getRevision();
+    if (revision == null) {
+      return false;
+    }
+
+    final AnyObjectId objectId = ObjectId.fromString(revision.get());
+    if (objectId == null) {
+      return false;
+    }
+
+    Change change = object.change(db);
+    if (change == null) {
+      return false;
+    }
+
+    final Project.NameKey projectName = change.getProject();
+    if (projectName == null) {
+      return false;
+    }
+
+    Arguments args = new Arguments(patchSet, revision, objectId, change, projectName);
+
+    try {
+      final Repository repo = repoManager.openRepository(projectName);
+      try {
+        final RevWalk rw = new RevWalk(repo);
+        try {
+          return match(repo, rw, args);
+        } finally {
+          rw.release();
+        }
+      } finally {
+        repo.close();
+      }
+    } catch (RepositoryNotFoundException e) {
+      log.error("Repository \"" + projectName.get() + "\" unknown.", e);
+    } catch (IOException e) {
+      log.error(projectName.get() + " cannot be read as a repository", e);
+    }
+    return false;
+  }
+
+  public abstract boolean match(Repository repo, RevWalk rw, Arguments args);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java
new file mode 100644
index 0000000..93238a8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java
@@ -0,0 +1,224 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Version;
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.LabelValue;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.common.data.PermissionRule.Action;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+
+/** Creates the {@code All-Projects} repository and initial ACLs. */
+public class AllProjectsCreator {
+  private final GitRepositoryManager mgr;
+  private final AllProjectsName allProjectsName;
+  private final PersonIdent serverUser;
+
+  private GroupReference admin;
+  private GroupReference batch;
+  private GroupReference anonymous;
+  private GroupReference registered;
+  private GroupReference owners;
+
+  @Inject
+  AllProjectsCreator(
+      GitRepositoryManager mgr,
+      AllProjectsName allProjectsName,
+      @GerritPersonIdent PersonIdent serverUser) {
+    this.mgr = mgr;
+    this.allProjectsName = allProjectsName;
+    this.serverUser = serverUser;
+
+    this.anonymous = new GroupReference(
+        AccountGroup.ANONYMOUS_USERS,
+        "Anonymous Users");
+    this.registered = new GroupReference(
+        AccountGroup.REGISTERED_USERS,
+        "Registered Users");
+    this.owners = new GroupReference(
+        AccountGroup.PROJECT_OWNERS,
+        "Project Owners");
+  }
+
+  public AllProjectsCreator setAdministrators(GroupReference admin) {
+    this.admin = admin;
+    return this;
+  }
+
+  public AllProjectsCreator setBatchUsers(GroupReference batch) {
+    this.batch = batch;
+    return this;
+  }
+
+  public void create() throws IOException, ConfigInvalidException {
+    Repository git = null;
+    try {
+      git = mgr.openRepository(allProjectsName);
+      initAllProjects(git);
+    } catch (RepositoryNotFoundException notFound) {
+      // A repository may be missing if this project existed only to store
+      // inheritable permissions. For example 'All-Projects'.
+      try {
+        git = mgr.createRepository(allProjectsName);
+        initAllProjects(git);
+
+        RefUpdate u = git.updateRef(Constants.HEAD);
+        u.link(GitRepositoryManager.REF_CONFIG);
+      } catch (RepositoryNotFoundException err) {
+        String name = allProjectsName.get();
+        throw new IOException("Cannot create repository " + name, err);
+      }
+    } finally {
+      if (git != null) {
+        git.close();
+      }
+    }
+  }
+
+  private void initAllProjects(Repository git)
+      throws IOException, ConfigInvalidException {
+    MetaDataUpdate md = new MetaDataUpdate(
+        GitReferenceUpdated.DISABLED,
+        allProjectsName,
+        git);
+    md.getCommitBuilder().setAuthor(serverUser);
+    md.getCommitBuilder().setCommitter(serverUser);
+    md.setMessage("Initialized Gerrit Code Review " + Version.getVersion());
+
+    ProjectConfig config = ProjectConfig.read(md);
+    Project p = config.getProject();
+    p.setDescription("Access inherited by all other projects.");
+    p.setRequireChangeID(InheritableBoolean.TRUE);
+    p.setUseContentMerge(InheritableBoolean.TRUE);
+    p.setUseContributorAgreements(InheritableBoolean.FALSE);
+    p.setUseSignedOffBy(InheritableBoolean.FALSE);
+
+    AccessSection cap = config.getAccessSection(AccessSection.GLOBAL_CAPABILITIES, true);
+    AccessSection all = config.getAccessSection(AccessSection.ALL, true);
+    AccessSection heads = config.getAccessSection(AccessSection.HEADS, true);
+    AccessSection tags = config.getAccessSection("refs/tags/*", true);
+    AccessSection meta = config.getAccessSection(GitRepositoryManager.REF_CONFIG, true);
+    AccessSection magic = config.getAccessSection("refs/for/" + AccessSection.ALL, true);
+
+    grant(config, cap, GlobalCapability.ADMINISTRATE_SERVER, admin);
+    grant(config, all, Permission.READ, admin, anonymous);
+
+    if (batch != null) {
+      Permission priority = cap.getPermission(GlobalCapability.PRIORITY, true);
+      PermissionRule r = rule(config, batch);
+      r.setAction(Action.BATCH);
+      priority.add(r);
+
+      Permission stream = cap.getPermission(GlobalCapability.STREAM_EVENTS, true);
+      stream.add(rule(config, batch));
+    }
+
+    LabelType cr = initCodeReviewLabel(config);
+    grant(config, heads, cr, -1, 1, registered);
+    grant(config, heads, cr, -2, 2, admin, owners);
+    grant(config, heads, Permission.CREATE, admin, owners);
+    grant(config, heads, Permission.PUSH, admin, owners);
+    grant(config, heads, Permission.SUBMIT, admin, owners);
+    grant(config, heads, Permission.FORGE_AUTHOR, registered);
+    grant(config, heads, Permission.FORGE_COMMITTER, admin, owners);
+    grant(config, heads, Permission.EDIT_TOPIC_NAME, true, admin, owners);
+
+    grant(config, tags, Permission.PUSH_TAG, admin, owners);
+    grant(config, tags, Permission.PUSH_SIGNED_TAG, admin, owners);
+
+    grant(config, magic, Permission.PUSH, registered);
+    grant(config, magic, Permission.PUSH_MERGE, registered);
+
+    meta.getPermission(Permission.READ, true).setExclusiveGroup(true);
+    grant(config, meta, Permission.READ, admin, owners);
+    grant(config, meta, cr, -2, 2, admin, owners);
+    grant(config, meta, Permission.PUSH, admin, owners);
+    grant(config, meta, Permission.SUBMIT, admin, owners);
+
+    config.commit(md);
+  }
+
+  private void grant(ProjectConfig config, AccessSection section,
+      String permission, GroupReference... groupList) {
+    grant(config, section, permission, false, groupList);
+  }
+
+  private void grant(ProjectConfig config, AccessSection section,
+      String permission, boolean force, GroupReference... groupList) {
+    Permission p = section.getPermission(permission, true);
+    for (GroupReference group : groupList) {
+      if (group != null) {
+        PermissionRule r = rule(config, group);
+        r.setForce(force);
+        p.add(r);
+      }
+    }
+  }
+
+  private void grant(ProjectConfig config,
+      AccessSection section, LabelType type,
+      int min, int max, GroupReference... groupList) {
+    String name = Permission.LABEL + type.getName();
+    Permission p = section.getPermission(name, true);
+    for (GroupReference group : groupList) {
+      if (group != null) {
+        PermissionRule r = rule(config, group);
+        r.setRange(min, max);
+        p.add(r);
+      }
+    }
+  }
+
+  private PermissionRule rule(ProjectConfig config, GroupReference group) {
+    return new PermissionRule(config.resolve(group));
+  }
+
+  public static LabelType initCodeReviewLabel(ProjectConfig c) {
+    LabelType type = new LabelType("Code-Review", ImmutableList.of(
+        new LabelValue((short) 2, "Looks good to me, approved"),
+        new LabelValue((short) 1, "Looks good to me, but someone else must approve"),
+        new LabelValue((short) 0, "No score"),
+        new LabelValue((short) -1, "I would prefer that you didn't submit this"),
+        new LabelValue((short) -2, "Do not submit")));
+    type.setAbbreviation("CR");
+    type.setCopyMinScore(true);
+    c.getLabelSections().put(type.getName(), type);
+    return type;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
index 4286ba0..0e89a75 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
@@ -14,41 +14,23 @@
 
 package com.google.gerrit.server.schema;
 
-import com.google.common.collect.ImmutableList;
-import com.google.gerrit.common.Version;
-import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.LabelValue;
-import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupName;
 import com.google.gerrit.reviewdb.client.CurrentSchemaVersion;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
 import com.google.gerrit.reviewdb.client.SystemConfig;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.account.GroupUUID;
-import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gwtorm.jdbc.JdbcExecutor;
 import com.google.gwtorm.jdbc.JdbcSchema;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.Repository;
 
 import java.io.File;
 import java.io.IOException;
@@ -59,8 +41,7 @@
   private final @SitePath
   File site_path;
 
-  private final GitRepositoryManager mgr;
-  private final AllProjectsName allProjectsName;
+  private final AllProjectsCreator allProjectsCreator;
   private final PersonIdent serverUser;
   private final DataSourceType dataSourceType;
 
@@ -70,26 +51,24 @@
   private AccountGroup anonymous;
   private AccountGroup registered;
   private AccountGroup owners;
+  private AccountGroup batch;
 
   @Inject
   public SchemaCreator(SitePaths site,
       @Current SchemaVersion version,
-      GitRepositoryManager mgr,
-      AllProjectsName allProjectsName,
+      AllProjectsCreator ap,
       @GerritPersonIdent PersonIdent au,
       DataSourceType dst) {
-    this(site.site_path, version, mgr, allProjectsName, au, dst);
+    this(site.site_path, version, ap, au, dst);
   }
 
   public SchemaCreator(@SitePath File site,
       @Current SchemaVersion version,
-      GitRepositoryManager gitMgr,
-      AllProjectsName ap,
+      AllProjectsCreator ap,
       @GerritPersonIdent PersonIdent au,
       DataSourceType dst) {
     site_path = site;
-    mgr = gitMgr;
-    allProjectsName = ap;
+    allProjectsCreator = ap;
     serverUser = au;
     dataSourceType = dst;
     versionNbr = version.getVersionNbr();
@@ -110,7 +89,10 @@
     db.schemaVersion().insert(Collections.singleton(sVer));
 
     initSystemConfig(db);
-    initAllProjects();
+    allProjectsCreator
+      .setAdministrators(GroupReference.forGroup(admin))
+      .setBatchUsers(GroupReference.forGroup(batch))
+      .create();
     dataSourceType.getIndexScript().run(db);
   }
 
@@ -151,13 +133,13 @@
     c.accountGroupNames().insert(
         Collections.singleton(new AccountGroupName(registered)));
 
-    final AccountGroup batchUsers = newGroup(c, "Non-Interactive Users", null);
-    batchUsers.setDescription("Users who perform batch actions on Gerrit");
-    batchUsers.setOwnerGroupUUID(admin.getGroupUUID());
-    batchUsers.setType(AccountGroup.Type.INTERNAL);
-    c.accountGroups().insert(Collections.singleton(batchUsers));
+    batch = newGroup(c, "Non-Interactive Users", null);
+    batch.setDescription("Users who perform batch actions on Gerrit");
+    batch.setOwnerGroupUUID(admin.getGroupUUID());
+    batch.setType(AccountGroup.Type.INTERNAL);
+    c.accountGroups().insert(Collections.singleton(batch));
     c.accountGroupNames().insert(
-        Collections.singleton(new AccountGroupName(batchUsers)));
+        Collections.singleton(new AccountGroupName(batch)));
 
     owners = newGroup(c, "Project Owners", AccountGroup.PROJECT_OWNERS);
     owners.setDescription("Any owner of the project");
@@ -176,128 +158,4 @@
     c.systemConfig().insert(Collections.singleton(s));
     return s;
   }
-
-  private void initAllProjects() throws IOException, ConfigInvalidException {
-    Repository git = null;
-    try {
-      git = mgr.openRepository(allProjectsName);
-      initAllProjects(git);
-    } catch (RepositoryNotFoundException notFound) {
-      // A repository may be missing if this project existed only to store
-      // inheritable permissions. For example 'All-Projects'.
-      try {
-        git = mgr.createRepository(allProjectsName);
-        initAllProjects(git);
-        final RefUpdate u = git.updateRef(Constants.HEAD);
-        u.link(GitRepositoryManager.REF_CONFIG);
-      } catch (RepositoryNotFoundException err) {
-        final String name = allProjectsName.get();
-        throw new IOException("Cannot create repository " + name, err);
-      }
-    } finally {
-      if (git != null) {
-        git.close();
-      }
-    }
-  }
-
-  private void initAllProjects(Repository git) throws IOException,
-      ConfigInvalidException {
-      MetaDataUpdate md =
-          new MetaDataUpdate(GitReferenceUpdated.DISABLED, allProjectsName, git);
-      md.getCommitBuilder().setAuthor(serverUser);
-      md.getCommitBuilder().setCommitter(serverUser);
-
-      ProjectConfig config = ProjectConfig.read(md);
-      Project p = config.getProject();
-      p.setDescription("Access inherited by all other projects.");
-      p.setRequireChangeID(InheritableBoolean.TRUE);
-      p.setUseContentMerge(InheritableBoolean.TRUE);
-      p.setUseContributorAgreements(InheritableBoolean.FALSE);
-      p.setUseSignedOffBy(InheritableBoolean.FALSE);
-
-      AccessSection cap = config.getAccessSection(AccessSection.GLOBAL_CAPABILITIES, true);
-      AccessSection all = config.getAccessSection(AccessSection.ALL, true);
-      AccessSection heads = config.getAccessSection(AccessSection.HEADS, true);
-      AccessSection tags = config.getAccessSection("refs/tags/*", true);
-      AccessSection meta = config.getAccessSection(GitRepositoryManager.REF_CONFIG, true);
-      AccessSection magic = config.getAccessSection("refs/for/" + AccessSection.ALL, true);
-
-      grant(config, cap, GlobalCapability.ADMINISTRATE_SERVER, admin);
-      grant(config, all, Permission.READ, admin, anonymous);
-
-      LabelType cr = initCodeReviewLabel(config);
-      grant(config, heads, cr, -1, 1, registered);
-      grant(config, heads, cr, -2, 2, admin, owners);
-      grant(config, heads, Permission.CREATE, admin, owners);
-      grant(config, heads, Permission.PUSH, admin, owners);
-      grant(config, heads, Permission.SUBMIT, admin, owners);
-      grant(config, heads, Permission.FORGE_AUTHOR, registered);
-      grant(config, heads, Permission.FORGE_COMMITTER, admin, owners);
-      grant(config, heads, Permission.EDIT_TOPIC_NAME, true, admin, owners);
-
-      grant(config, tags, Permission.PUSH_TAG, admin, owners);
-      grant(config, tags, Permission.PUSH_SIGNED_TAG, admin, owners);
-
-      grant(config, magic, Permission.PUSH, registered);
-      grant(config, magic, Permission.PUSH_MERGE, registered);
-
-      meta.getPermission(Permission.READ, true).setExclusiveGroup(true);
-      grant(config, meta, Permission.READ, admin, owners);
-      grant(config, meta, cr, -2, 2, admin, owners);
-      grant(config, meta, Permission.PUSH, admin, owners);
-      grant(config, meta, Permission.SUBMIT, admin, owners);
-
-      md.setMessage("Initialized Gerrit Code Review " + Version.getVersion());
-      config.commit(md);
-  }
-
-  private PermissionRule grant(ProjectConfig config, AccessSection section,
-      String permission, AccountGroup group1, AccountGroup... groupList) {
-    return grant(config, section, permission, false, group1, groupList);
-  }
-
-  private PermissionRule grant(ProjectConfig config, AccessSection section,
-      String permission, boolean force, AccountGroup group1,
-      AccountGroup... groupList) {
-    Permission p = section.getPermission(permission, true);
-    PermissionRule rule = rule(config, group1);
-    rule.setForce(force);
-    p.add(rule);
-    for (AccountGroup group : groupList) {
-      rule = rule(config, group);
-      rule.setForce(force);
-      p.add(rule);
-    }
-    return rule;
-  }
-
-  private void grant(ProjectConfig config,
-      AccessSection section, LabelType type,
-      int min, int max, AccountGroup... groupList) {
-    String name = Permission.LABEL + type.getName();
-    Permission p = section.getPermission(name, true);
-    for (AccountGroup group : groupList) {
-      PermissionRule r = rule(config, group);
-      r.setRange(min, max);
-      p.add(r);
-    }
-  }
-
-  private PermissionRule rule(ProjectConfig config, AccountGroup group) {
-    return new PermissionRule(config.resolve(group));
-  }
-
-  public static LabelType initCodeReviewLabel(ProjectConfig c) {
-    LabelType type = new LabelType("Code-Review", ImmutableList.of(
-        new LabelValue((short) 2, "Looks good to me, approved"),
-        new LabelValue((short) 1, "Looks good to me, but someone else must approve"),
-        new LabelValue((short) 0, "No score"),
-        new LabelValue((short) -1, "I would prefer that you didn't submit this"),
-        new LabelValue((short) -2, "Do not submit")));
-    type.setAbbreviation("CR");
-    type.setCopyMinScore(true);
-    c.getLabelSections().put(type.getName(), type);
-    return type;
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
index 4de3888..53930ac 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
@@ -32,7 +32,7 @@
 /** A version of the database schema. */
 public abstract class SchemaVersion {
   /** The current schema version. */
-  public static final Class<Schema_77> C = Schema_77.class;
+  public static final Class<Schema_79> C = Schema_79.class;
 
   public static class Module extends AbstractModule {
     @Override
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/schema/Schema_78.java
similarity index 65%
copy from gerrit-server/src/main/java/com/google/gerrit/server/events/QueryStats.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_78.java
index ecf2b9a..18ae8b4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/QueryStats.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_78.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 The Android Open Source Project
+// Copyright (C) 2013 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,11 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.events;
+package com.google.gerrit.server.schema;
 
-public class QueryStats {
-  public final String type = "stats";
-  public int rowCount;
-  public long runTimeMilliseconds;
-  public String resumeSortKey;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class Schema_78 extends SchemaVersion {
+
+  @Inject
+  Schema_78(Provider<Schema_77> prior) {
+    super(prior);
+  }
 }
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/schema/Schema_79.java
similarity index 65%
copy from gerrit-server/src/main/java/com/google/gerrit/server/events/QueryStats.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_79.java
index ecf2b9a..c5087ea 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/QueryStats.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_79.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 The Android Open Source Project
+// Copyright (C) 2013 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,11 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.events;
+package com.google.gerrit.server.schema;
 
-public class QueryStats {
-  public final String type = "stats";
-  public int rowCount;
-  public long runTimeMilliseconds;
-  public String resumeSortKey;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class Schema_79 extends SchemaVersion {
+
+  @Inject
+  Schema_79(Provider<Schema_78> prior) {
+    super(prior);
+  }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java b/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java
index 98b0b4a..9b122eb 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java
@@ -80,7 +80,7 @@
       for (LabelType label : labelTypes.getLabelTypes()) {
         config.getLabelSections().put(label.getName(), label);
       }
-      allProjects = new ProjectState(this, allProjectsName, null,
+      allProjects = new ProjectState(null, this, allProjectsName, null, null,
           null, null, null, config);
     }
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/change/ChangeJsonTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/change/ChangeJsonTest.java
new file mode 100644
index 0000000..14e0ebf
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/change/ChangeJsonTest.java
@@ -0,0 +1,233 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import static org.easymock.EasyMock.anyBoolean;
+import static org.easymock.EasyMock.anyObject;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.replay;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.changes.ListChangesOption;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ChangeAccess;
+import com.google.gerrit.reviewdb.server.ChangeMessageAccess;
+import com.google.gerrit.reviewdb.server.PatchSetAccess;
+import com.google.gerrit.reviewdb.server.PatchSetApprovalAccess;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.AccountByEmailCache;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountInfo;
+import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.account.Realm;
+import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
+import com.google.gerrit.server.change.ChangeJson.ChangeMessageInfo;
+import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.ListResultSet;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
+import com.google.inject.Binder;
+import com.google.inject.Guice;
+import com.google.inject.Module;
+
+import junit.framework.TestCase;
+
+import org.easymock.EasyMock;
+import org.easymock.IAnswer;
+import org.eclipse.jgit.lib.Config;
+
+import java.sql.Timestamp;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+public class ChangeJsonTest extends TestCase {
+
+  public void testFormatChangeMessages() throws OrmException {
+
+    // create mocks
+    final CurrentUser currentUser = createMock(CurrentUser.class);
+    final GitRepositoryManager grm = createMock(GitRepositoryManager.class);
+    final AccountByEmailCache abec = createMock(AccountByEmailCache.class);
+    final AccountCache ac = createMock(AccountCache.class);
+    final AccountInfo.Loader.Factory alf =
+        createMock(AccountInfo.Loader.Factory.class);
+    final CapabilityControl.Factory ccf =
+        createMock(CapabilityControl.Factory.class);
+    final GroupBackend gb = createMock(GroupBackend.class);
+    final Realm r = createMock(Realm.class);
+    final PatchListCache plc = createMock(PatchListCache.class);
+    final ProjectCache pc = createMock(ProjectCache.class);
+    final Config config = new Config();  // unable to mock
+    final ReviewDb rdb = createMock(ReviewDb.class);
+    final ChangeAccess ca = createMock(ChangeAccess.class);
+    final PatchSetAccess psa = createMock(PatchSetAccess.class);
+    final PatchSetApprovalAccess psaa =
+        createMock(PatchSetApprovalAccess.class);
+    final ChangeMessageAccess cma = createMock(ChangeMessageAccess.class);
+    AccountInfo.Loader accountLoader = createMock(AccountInfo.Loader.class);
+
+    // create ChangeJson instance
+    Module mod = new Module() {
+      @Override
+      public void configure(Binder binder) {
+        binder.bind(CurrentUser.class).toInstance(currentUser);
+        binder.bind(GitRepositoryManager.class).toInstance(grm);
+        binder.bind(AccountByEmailCache.class).toInstance(abec);
+        binder.bind(AccountCache.class).toInstance(ac);
+        binder.bind(AccountInfo.Loader.Factory.class).toInstance(alf);
+        binder.bind(CapabilityControl.Factory.class).toInstance(ccf);
+        binder.bind(GroupBackend.class).toInstance(gb);
+        binder.bind(Realm.class).toInstance(r);
+        binder.bind(PatchListCache.class).toInstance(plc);
+        binder.bind(ProjectCache.class).toInstance(pc);
+        binder.bind(ReviewDb.class).toInstance(rdb);
+        binder.bind(Config.class).annotatedWith(GerritServerConfig.class)
+            .toInstance(config);
+        binder.bind(String.class).annotatedWith(CanonicalWebUrl.class)
+            .toInstance("");
+        binder.bind(String.class).annotatedWith(AnonymousCowardName.class)
+            .toInstance("");
+      }
+    };
+    ChangeJson json = Guice.createInjector(mod).getInstance(ChangeJson.class);
+
+    // define mock behavior for tests
+    expect(alf.create(anyBoolean())).andReturn(accountLoader).anyTimes();
+
+    Project.NameKey proj = new Project.NameKey("ProjectNameKey");
+    Branch.NameKey forBranch = new Branch.NameKey(proj, "BranchNameKey");
+
+    Change.Key changeKey123 = new Change.Key("ChangeKey123");
+    Change.Id changeId123 = new Change.Id(123);
+    Change change123 = new Change(changeKey123, changeId123, null, forBranch);
+
+    Change.Key changeKey234 = new Change.Key("ChangeKey234");
+    Change.Id changeId234 = new Change.Id(234);
+    Change change234 = new Change(changeKey234, changeId234, null, forBranch);
+
+    expect(ca.get(Sets.newHashSet(changeId123)))
+        .andAnswer(results(Change.class, change123)).anyTimes();
+    expect(ca.get(changeId123)).andReturn(change123).anyTimes();
+    expect(ca.get(Sets.newHashSet(changeId234)))
+        .andAnswer(results(Change.class, change234));
+    expect(ca.get(changeId234)).andReturn(change234);
+    expect(rdb.changes()).andReturn(ca).anyTimes();
+
+    expect(psa.get(EasyMock.<Iterable<PatchSet.Id>>anyObject()))
+        .andAnswer(results(PatchSet.class)).anyTimes();
+    expect(rdb.patchSets()).andReturn(psa).anyTimes();
+
+    expect(psaa.byPatchSet(anyObject(PatchSet.Id.class)))
+        .andAnswer(results(PatchSetApproval.class)).anyTimes();
+    expect(rdb.patchSetApprovals()).andReturn(psaa).anyTimes();
+
+    expect(currentUser.getStarredChanges())
+        .andReturn(Collections.<Change.Id>emptySet()).anyTimes();
+
+    long timeBase = System.currentTimeMillis();
+    ChangeMessage changeMessage1 =changeMessage(
+        changeId123, "cm1", 111, timeBase, 1111, "first message");
+    ChangeMessage changeMessage2 = changeMessage(
+        changeId123, "cm2", 222, timeBase + 1000, 1111, "second message");
+    expect(cma.byChange(changeId123))
+        .andAnswer(results(ChangeMessage.class, changeMessage2, changeMessage1))
+        .anyTimes();
+    expect(cma.byChange(changeId234)).andAnswer(results(ChangeMessage.class));
+    expect(rdb.changeMessages()).andReturn(cma).anyTimes();
+
+    expect(accountLoader.get(anyObject(Account.Id.class)))
+        .andAnswer(accountForId()).anyTimes();
+    accountLoader.fill();
+    expectLastCall().anyTimes();
+
+    replay(rdb, ca, psa, psaa, alf, currentUser, cma, accountLoader);
+
+    // test 1: messages not returned by default
+    ChangeInfo ci = json.format(new ChangeData(changeId123));
+    assertNull(ci.messages);
+
+    json.addOption(ListChangesOption.MESSAGES);
+
+    // test 2: two change messages, in chronological order
+    ci = json.format(new ChangeData(changeId123));
+    assertNotNull(ci.messages);
+    assertEquals(2, ci.messages.size());
+    Iterator<ChangeMessageInfo> cmis = ci.messages.iterator();
+    assertEquals(changeMessage1, cmis.next());
+    assertEquals(changeMessage2, cmis.next());
+
+    // test 3: no change messages
+    ci = json.format(new ChangeData(changeId234));
+    assertNotNull(ci.messages);
+    assertEquals(0, ci.messages.size());
+  }
+
+  private static IAnswer<AccountInfo> accountForId() {
+    return new IAnswer<AccountInfo>() {
+      @Override
+      public AccountInfo answer() throws Throwable {
+        Account.Id id = (Account.Id) EasyMock.getCurrentArguments()[0];
+        AccountInfo ai = new AccountInfo(id);
+        return ai;
+      }};
+  }
+
+  private static <T> IAnswer<ResultSet<T>> results(Class<T> type, T... items) {
+    final List<T> list = Lists.newArrayList(items);
+    return new IAnswer<ResultSet<T>>() {
+      @Override
+      public ResultSet<T> answer() throws Throwable {
+        return new ListResultSet<T>(list);
+      }};
+  }
+
+  private static void assertEquals(ChangeMessage cm, ChangeMessageInfo cmi) {
+    assertEquals(cm.getPatchSetId().get(), (int) cmi._revisionNumber);
+    assertEquals(cm.getMessage(), cmi.message);
+    assertEquals(cm.getKey().get(), cmi.id);
+    assertEquals(cm.getWrittenOn(), cmi.date);
+    assertNotNull(cmi.author);
+    assertEquals(cm.getAuthor(), cmi.author._id);
+  }
+
+  private static ChangeMessage changeMessage(Change.Id changeId,
+      String uuid, int accountId, long time, int psId, String message) {
+    ChangeMessage.Key key = new ChangeMessage.Key(changeId, uuid);
+    Account.Id author = new Account.Id(accountId);
+    Timestamp updated = new Timestamp(time);
+    PatchSet.Id ps = new PatchSet.Id(changeId, psId);
+    ChangeMessage changeMessage = new ChangeMessage(key, author, updated, ps);
+    changeMessage.setMessage(message);
+    return changeMessage;
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/change/CommentsTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/change/CommentsTest.java
new file mode 100644
index 0000000..21d1ce4
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/change/CommentsTest.java
@@ -0,0 +1,224 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.replay;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.PatchLineCommentAccess;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.AccountInfo;
+import com.google.gerrit.server.change.CommentInfo.Side;
+import com.google.gwtorm.server.ListResultSet;
+import com.google.gwtorm.server.ResultSet;
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.TypeLiteral;
+
+import junit.framework.TestCase;
+
+import org.easymock.IAnswer;
+
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+public class CommentsTest extends TestCase {
+
+  private Injector injector;
+  private RevisionResource revRes1;
+  private RevisionResource revRes2;
+  private PatchLineComment plc1;
+  private PatchLineComment plc2;
+  private PatchLineComment plc3;
+
+  @Override
+  protected void setUp() throws Exception {
+    @SuppressWarnings("unchecked")
+    final DynamicMap<RestView<CommentResource>> views =
+        createMock(DynamicMap.class);
+    final TypeLiteral<DynamicMap<RestView<CommentResource>>> viewsType =
+        new TypeLiteral<DynamicMap<RestView<CommentResource>>>() {};
+    final AccountInfo.Loader.Factory alf =
+        createMock(AccountInfo.Loader.Factory.class);
+    final ReviewDb db = createMock(ReviewDb.class);
+
+    AbstractModule mod = new AbstractModule() {
+      @Override
+      protected void configure() {
+        bind(viewsType).toInstance(views);
+        bind(AccountInfo.Loader.Factory.class).toInstance(alf);
+        bind(ReviewDb.class).toInstance(db);
+      }};
+
+    Account.Id account1 = new Account.Id(1);
+    Account.Id account2 = new Account.Id(2);
+    AccountInfo.Loader accountLoader = createMock(AccountInfo.Loader.class);
+    accountLoader.fill();
+    expectLastCall().anyTimes();
+    expect(accountLoader.get(account1))
+        .andReturn(new AccountInfo(account1)).anyTimes();
+    expect(accountLoader.get(account2))
+        .andReturn(new AccountInfo(account2)).anyTimes();
+    expect(alf.create(true)).andReturn(accountLoader).anyTimes();
+    replay(accountLoader, alf);
+
+    revRes1 = createMock(RevisionResource.class);
+    revRes2 = createMock(RevisionResource.class);
+
+    PatchLineCommentAccess plca = createMock(PatchLineCommentAccess.class);
+    expect(db.patchComments()).andReturn(plca).anyTimes();
+
+    Change.Id changeId = new Change.Id(123);
+    PatchSet.Id psId1 = new PatchSet.Id(changeId, 1);
+    PatchSet ps1 = new PatchSet(psId1);
+    expect(revRes1.getPatchSet()).andReturn(ps1).anyTimes();
+    PatchSet.Id psId2 = new PatchSet.Id(changeId, 2);
+    PatchSet ps2 = new PatchSet(psId2);
+    expect(revRes2.getPatchSet()).andReturn(ps2);
+
+    long timeBase = System.currentTimeMillis();
+    plc1 = newPatchLineComment(psId1, "Comment1", null,
+        "FileOne.txt", Side.REVISION, 1, account1, timeBase,
+        "First Comment");
+    plc2 = newPatchLineComment(psId1, "Comment2", "Comment1",
+        "FileOne.txt", Side.REVISION, 1, account2, timeBase + 1000,
+        "Reply to First Comment");
+    plc3 = newPatchLineComment(psId1, "Comment3", "Comment1",
+        "FileOne.txt", Side.PARENT, 1, account1, timeBase + 2000,
+        "First Parent Comment");
+
+    expect(plca.publishedByPatchSet(psId1))
+        .andAnswer(results(plc1, plc2, plc3)).anyTimes();
+    expect(plca.publishedByPatchSet(psId2))
+        .andAnswer(results()).anyTimes();
+
+    replay(db, revRes1, revRes2, plca);
+    injector = Guice.createInjector(mod);
+  }
+
+  public void testListComments() throws Exception {
+    // test ListComments for patch set 1
+    assertListComments(injector, revRes1, ImmutableMap.of(
+        "FileOne.txt", Lists.newArrayList(plc3, plc1, plc2)));
+
+    // test ListComments for patch set 2
+    assertListComments(injector, revRes2,
+        Collections.<String, ArrayList<PatchLineComment>>emptyMap());
+  }
+
+  public void testGetComment() throws Exception {
+    // test GetComment for existing comment
+    assertGetComment(injector, revRes1, plc1, plc1.getKey().get());
+
+    // test GetComment for non-existent comment
+    assertGetComment(injector, revRes1, null, "BadComment");
+  }
+
+  private static IAnswer<ResultSet<PatchLineComment>> results(
+      final PatchLineComment... comments) {
+    return new IAnswer<ResultSet<PatchLineComment>>() {
+      @Override
+      public ResultSet<PatchLineComment> answer() throws Throwable {
+        return new ListResultSet<PatchLineComment>(Lists.newArrayList(comments));
+      }};
+  }
+
+  private static void assertGetComment(Injector inj, RevisionResource res,
+      PatchLineComment expected, String uuid) throws Exception {
+    GetComment getComment = inj.getInstance(GetComment.class);
+    Comments comments = inj.getInstance(Comments.class);
+    try {
+      CommentResource commentRes = comments.parse(res, IdString.fromUrl(uuid));
+      if (expected == null) {
+        fail("Expected no comment");
+      }
+      CommentInfo actual = (CommentInfo) getComment.apply(commentRes);
+      assertComment(expected, actual);
+    } catch (ResourceNotFoundException e) {
+      if (expected != null) {
+        fail("Expected to find comment");
+      }
+    }
+  }
+
+  private static void assertListComments(Injector inj, RevisionResource res,
+      Map<String, ArrayList<PatchLineComment>> expected) throws Exception {
+    Comments comments = inj.getInstance(Comments.class);
+    RestReadView<RevisionResource> listView =
+        (RestReadView<RevisionResource>) comments.list();
+    @SuppressWarnings("unchecked")
+    Map<String, List<CommentInfo>> actual =
+        (Map<String, List<CommentInfo>>) listView.apply(res);
+    assertNotNull(actual);
+    assertEquals(expected.size(), actual.size());
+    assertEquals(expected.keySet(), actual.keySet());
+    for (String filename : expected.keySet()) {
+      List<PatchLineComment> expectedComments = expected.get(filename);
+      List<CommentInfo> actualComments = actual.get(filename);
+      assertNotNull(actualComments);
+      assertEquals(expectedComments.size(), actualComments.size());
+      for (int i = 0; i < expectedComments.size(); i++) {
+        assertComment(expectedComments.get(i), actualComments.get(i));
+      }
+    }
+  }
+
+  private static void assertComment(PatchLineComment plc, CommentInfo ci) {
+    assertEquals(plc.getKey().get(), ci.id);
+    assertEquals(plc.getParentUuid(), ci.inReplyTo);
+    assertEquals("gerritcodereview#comment", ci.kind);
+    assertEquals(plc.getMessage(), ci.message);
+    assertNotNull(ci.author);
+    assertEquals(plc.getAuthor(), ci.author._id);
+    assertEquals(plc.getLine(), (int) ci.line);
+    assertEquals(plc.getSide() == 0 ? Side.PARENT : Side.REVISION,
+        Objects.firstNonNull(ci.side, Side.REVISION));
+    assertEquals(plc.getWrittenOn(), ci.updated);
+  }
+
+  private static PatchLineComment newPatchLineComment(PatchSet.Id psId,
+      String uuid, String inReplyToUuid, String filename, Side side, int line,
+      Account.Id authorId, long millis, String message) {
+    Patch.Key p = new Patch.Key(psId, filename);
+    PatchLineComment.Key id = new PatchLineComment.Key(p, uuid);
+    PatchLineComment plc =
+        new PatchLineComment(id, line, authorId, inReplyToUuid);
+    plc.setMessage(message);
+    plc.setSide(side == CommentInfo.Side.PARENT ? (short) 0 : (short) 1);
+    plc.setStatus(Status.PUBLISHED);
+    plc.setWrittenOn(new Timestamp(millis));
+    return plc;
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
index 0a2dcc4..a64039c 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
@@ -540,11 +540,11 @@
     ProjectControl.AssistedFactory projectControlFactory = null;
     RulesCache rulesCache = null;
     all.put(local.getProject().getNameKey(), new ProjectState(
-        projectCache, allProjectsName, projectControlFactory,
-        envFactory, mgr, rulesCache, local));
+        null, projectCache, allProjectsName, projectControlFactory,
+        envFactory, mgr, rulesCache, null, local));
     all.put(parent.getProject().getNameKey(), new ProjectState(
-        projectCache, allProjectsName, projectControlFactory,
-        envFactory, mgr, rulesCache, parent));
+        null, projectCache, allProjectsName, projectControlFactory,
+        envFactory, mgr, rulesCache, null, parent));
     return all.get(local.getProject().getNameKey());
   }
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/HookTestCase.java b/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/HookTestCase.java
index d9b237b..b517de7 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/HookTestCase.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/HookTestCase.java
@@ -52,16 +52,22 @@
 
 import static org.junit.Assert.fail;
 
+import com.google.common.io.ByteStreams;
+
 import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
 import org.eclipse.jgit.lib.Repository;
+import org.junit.After;
 import org.junit.Before;
 
 import java.io.File;
-import java.net.URISyntaxException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
 import java.net.URL;
 
 public abstract class HookTestCase extends LocalDiskRepositoryTestCase {
   protected Repository repository;
+  private File hooksh;
 
   @Override
   @Before
@@ -70,22 +76,48 @@
     repository = createWorkRepository();
   }
 
-  protected File getHook(final String name) {
+  @Override
+  @After
+  public void tearDown() throws Exception {
+    if (hooksh != null) {
+      if (!hooksh.delete()) {
+        hooksh.deleteOnExit();
+      }
+      hooksh = null;
+    }
+  }
+
+  protected File getHook(final String name) throws IOException {
     final String scproot = "com/google/gerrit/server/tools/root";
     final String path = scproot + "/hooks/" + name;
-    final URL url = cl().getResource(path);
+    URL url = cl().getResource(path);
     if (url == null) {
       fail("Cannot locate " + path + " in CLASSPATH");
     }
 
     File hook;
-    try {
-      hook = new File(url.toURI());
-    } catch (URISyntaxException e) {
+    if ("file".equals(url.getProtocol())) {
       hook = new File(url.getPath());
-    }
-    if (!hook.isFile()) {
-      fail("Cannot locate " + path + " in CLASSPATH");
+      if (!hook.isFile()) {
+        fail("Cannot locate " + path + " in CLASSPATH");
+      }
+    } else if ("jar".equals(url.getProtocol())) {
+      hooksh = File.createTempFile("hook_", ".sh");
+      InputStream in = url.openStream();
+      try {
+        FileOutputStream out = new FileOutputStream(hooksh);
+        try {
+          ByteStreams.copy(in, out);
+        } finally {
+          out.close();
+        }
+      } finally {
+        in.close();
+      }
+      hook = hooksh;
+    } else {
+      fail("Cannot invoke " + url);
+      hook = null;
     }
 
     // The hook was copied out of our source control system into the
diff --git a/gerrit-sshd/pom.xml b/gerrit-sshd/pom.xml
index f026920..ab9bd43 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.6</version>
+    <version>2.7</version>
   </parent>
 
   <artifactId>gerrit-sshd</artifactId>
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
index 9a2f0d1..8dc3f2c 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
@@ -15,18 +15,14 @@
 package com.google.gerrit.sshd;
 
 import com.google.gerrit.reviewdb.client.AccountSshKey;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PeerDaemonUser;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.sshd.SshScope.Context;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
 import org.apache.commons.codec.binary.Base64;
-import org.apache.sshd.common.future.CloseFuture;
-import org.apache.sshd.common.future.SshFutureListener;
 import org.apache.sshd.common.KeyPairProvider;
 import org.apache.sshd.common.SshException;
 import org.apache.sshd.common.util.Buffer;
@@ -104,7 +100,7 @@
       if (myHostKeys.contains(suppliedKey)
           || getPeerKeys().contains(suppliedKey)) {
         PeerDaemonUser user = peerFactory.create(sd.getRemoteAddress());
-        return success(username, session, sd, user);
+        return SshUtil.success(username, session, sshScope, sshLog, sd, user);
 
       } else {
         sd.authenticationError(username, "no-matching-key");
@@ -144,12 +140,14 @@
       }
     }
 
-    if (!createUser(sd, key).getAccount().isActive()) {
+    if (!SshUtil.createUser(sd, userFactory, key.getAccount())
+        .getAccount().isActive()) {
       sd.authenticationError(username, "inactive-account");
       return false;
     }
 
-    return success(username, session, sd, createUser(sd, key));
+    return SshUtil.success(username, session, sshScope, sshLog, sd,
+        SshUtil.createUser(sd, userFactory, key.getAccount()));
   }
 
   private Set<PublicKey> getPeerKeys() {
@@ -161,47 +159,6 @@
     return p.keys;
   }
 
-  private boolean success(final String username, final ServerSession session,
-      final SshSession sd, final CurrentUser user) {
-    if (sd.getCurrentUser() == null) {
-      sd.authenticationSuccess(username, user);
-
-      // 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 = sshScope.newContext(null, sd, null);
-      Context old = sshScope.set(ctx);
-      try {
-        sshLog.onLogin();
-      } finally {
-        sshScope.set(old);
-      }
-
-      GerritServerSession s = (GerritServerSession)session;
-      s.addCloseSessionListener(
-          new SshFutureListener<CloseFuture>() {
-            @Override
-            public void operationComplete(CloseFuture future) {
-              Context ctx = sshScope.newContext(null, sd, null);
-              Context old = sshScope.set(ctx);
-              try {
-                sshLog.onLogout();
-              } finally {
-                sshScope.set(old);
-              }
-            }
-          });
-    }
-
-    return true;
-  }
-
-  private IdentifiedUser createUser(final SshSession sd,
-      final SshKeyCacheEntry key) {
-    return userFactory.create(sd.getRemoteAddress(), key.getAccount());
-  }
-
   private SshKeyCacheEntry find(final Iterable<SshKeyCacheEntry> keyList,
       final PublicKey suppliedKey) {
     for (final SshKeyCacheEntry k : keyList) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/GerritGSSAuthenticator.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/GerritGSSAuthenticator.java
new file mode 100644
index 0000000..17db6b9
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/GerritGSSAuthenticator.java
@@ -0,0 +1,68 @@
+// Copyright (C) 2013 Goldman Sachs
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.client.Account;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.IdentifiedUser.GenericFactory;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountState;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.apache.sshd.server.auth.gss.GSSAuthenticator;
+import org.apache.sshd.server.session.ServerSession;
+
+/**
+ * Authenticates users with kerberos (gssapi-with-mic).
+ */
+@Singleton
+class GerritGSSAuthenticator extends GSSAuthenticator {
+  private final AccountCache accounts;
+  private final SshScope sshScope;
+  private final SshLog sshLog;
+  private final GenericFactory userFactory;
+
+  @Inject
+  GerritGSSAuthenticator(final AccountCache accounts, final SshScope sshScope,
+      final SshLog sshLog, final IdentifiedUser.GenericFactory userFactory) {
+    this.accounts = accounts;
+    this.sshScope = sshScope;
+    this.sshLog = sshLog;
+    this.userFactory = userFactory;
+  }
+
+  @Override
+  public boolean validateIdentity(final ServerSession session,
+      final String identity) {
+    final SshSession sd = session.getAttribute(SshSession.KEY);
+    int at = identity.indexOf('@');
+    String username;
+    if (at == -1) {
+      username = identity;
+    } else {
+      username = identity.substring(0, at);
+    }
+    AccountState state = accounts.getByUsername(username);
+    Account account = state == null ? null : state.getAccount();
+    boolean active = account != null && account.isActive();
+    if (active) {
+      return SshUtil.success(username, session, sshScope, sshLog, sd,
+          SshUtil.createUser(sd, userFactory, account.getId()));
+    } else {
+      return false;
+    }
+  }
+}
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 1d03953..bafc388 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
@@ -144,6 +144,7 @@
   @Inject
   SshDaemon(final CommandFactory commandFactory, final NoShell noShell,
       final PublickeyAuthenticator userAuth,
+      final GerritGSSAuthenticator kerberosAuth,
       final KeyPairProvider hostKeyProvider, final IdGenerator idGenerator,
       @GerritServerConfig final Config cfg, final SshLog sshLog,
       @SshListenAddresses final List<SocketAddress> listen,
@@ -179,6 +180,11 @@
           String.valueOf(maxConnectionsPerUser));
     }
 
+    final String kerberosKeytab = cfg.getString(
+        "sshd", null, "kerberosKeytab");
+    final String kerberosPrincipal = cfg.getString(
+        "sshd", null, "kerberosPrincipal");
+
     SshSessionBackend backend = cfg.getEnum(
         "sshd", null, "backend", SshSessionBackend.MINA);
 
@@ -200,7 +206,7 @@
     initFileSystemFactory();
     initSubsystems();
     initCompression();
-    initUserAuth(userAuth);
+    initUserAuth(userAuth, kerberosAuth, kerberosKeytab, kerberosPrincipal);
     setKeyPairProvider(hostKeyProvider);
     setCommandFactory(commandFactory);
     setShellFactory(noShell);
@@ -494,10 +500,36 @@
     setSubsystemFactories(Collections.<NamedFactory<Command>> emptyList());
   }
 
-  @SuppressWarnings("unchecked")
-  private void initUserAuth(final PublickeyAuthenticator pubkey) {
-    setUserAuthFactories(Arrays
-        .<NamedFactory<UserAuth>> asList(new UserAuthPublicKey.Factory()));
+  private void initUserAuth(final PublickeyAuthenticator pubkey,
+      final GSSAuthenticator kerberosAuthenticator,
+      String kerberosKeytab, String kerberosPrincipal) {
+    List<NamedFactory<UserAuth>> authFactories = Lists.newArrayList();
+    if (kerberosKeytab != null) {
+      authFactories.add(new UserAuthGSS.Factory());
+      log.info("Enabling kerberos with keytab " + kerberosKeytab);
+      if (!new File(kerberosKeytab).canRead()) {
+        log.error("Keytab " + kerberosKeytab +
+            " does not exist or is not readable; further errors are possible");
+      }
+      kerberosAuthenticator.setKeytabFile(kerberosKeytab);
+      if (kerberosPrincipal == null) {
+        try {
+          kerberosPrincipal = "host/" +
+              InetAddress.getLocalHost().getCanonicalHostName();
+        } catch(UnknownHostException e) {
+          kerberosPrincipal = "host/localhost";
+        }
+      }
+      log.info("Using kerberos principal " + kerberosPrincipal);
+      if (!kerberosPrincipal.startsWith("host/")) {
+        log.warn("Host principal does not start with host/ " +
+            "which most SSH clients will supply automatically");
+      }
+      kerberosAuthenticator.setServicePrincipalName(kerberosPrincipal);
+      setGSSAuthenticator(kerberosAuthenticator);
+    }
+    authFactories.add(new UserAuthPublicKey.Factory());
+    setUserAuthFactories(authFactories);
     setPublickeyAuthenticator(pubkey);
   }
 
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 6fad42b..2e42c23 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
@@ -42,6 +42,7 @@
 import org.apache.sshd.common.KeyPairProvider;
 import org.apache.sshd.server.CommandFactory;
 import org.apache.sshd.server.PublickeyAuthenticator;
+import org.apache.sshd.server.auth.gss.GSSAuthenticator;
 import org.eclipse.jgit.lib.Config;
 
 import java.net.SocketAddress;
@@ -84,6 +85,7 @@
         .toProvider(StreamCommandExecutorProvider.class).in(SINGLETON);
     bind(QueueProvider.class).to(CommandExecutorQueueProvider.class).in(SINGLETON);
 
+    bind(GSSAuthenticator.class).to(GerritGSSAuthenticator.class);
     bind(PublickeyAuthenticator.class).to(DatabasePubKeyAuth.class);
     bind(KeyPairProvider.class).toProvider(HostKeyProvider.class).in(SINGLETON);
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshUtil.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshUtil.java
index da245a3..74e6d1b 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshUtil.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshUtil.java
@@ -14,12 +14,19 @@
 
 package com.google.gerrit.sshd;
 
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountSshKey;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.sshd.SshScope.Context;
 
 import org.apache.commons.codec.binary.Base64;
+import org.apache.sshd.common.future.CloseFuture;
+import org.apache.sshd.common.future.SshFutureListener;
 import org.apache.sshd.common.KeyPairProvider;
 import org.apache.sshd.common.SshException;
 import org.apache.sshd.common.util.Buffer;
+import org.apache.sshd.server.session.ServerSession;
 import org.eclipse.jgit.lib.Constants;
 
 import java.io.BufferedReader;
@@ -112,4 +119,47 @@
       return keyStr;
     }
   }
+
+  public static boolean success(final String username, final ServerSession session,
+      final SshScope sshScope, final SshLog sshLog,
+      final SshSession sd, final CurrentUser user) {
+    if (sd.getCurrentUser() == null) {
+      sd.authenticationSuccess(username, user);
+
+      // 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 = sshScope.newContext(null, sd, null);
+      Context old = sshScope.set(ctx);
+      try {
+        sshLog.onLogin();
+      } finally {
+        sshScope.set(old);
+      }
+
+      GerritServerSession s = (GerritServerSession) session;
+      s.addCloseSessionListener(
+          new SshFutureListener<CloseFuture>() {
+            @Override
+            public void operationComplete(CloseFuture future) {
+              final Context ctx = sshScope.newContext(null, sd, null);
+              final Context old = sshScope.set(ctx);
+              try {
+                sshLog.onLogout();
+              } finally {
+                sshScope.set(old);
+              }
+            }
+          });
+    }
+
+    return true;
+  }
+
+  public static IdentifiedUser createUser(final SshSession sd,
+      final IdentifiedUser.GenericFactory userFactory,
+      final Account.Id account) {
+    return userFactory.create(sd.getRemoteAddress(), account);
+  }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
index a3e9d6e..c938891 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
@@ -136,6 +136,13 @@
     boolean accountUpdated = false;
     boolean sshKeysUpdated = false;
 
+    ResultSet<AccountExternalId> ids = db.accountExternalIds().byAccount(id);
+    for (AccountExternalId extId : ids) {
+      if (extId.isScheme(AccountExternalId.SCHEME_USERNAME)) {
+        account.setUserName(extId.getSchemeRest());
+      }
+    }
+
     for (String email : addEmails) {
       link(id, email);
     }
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 1b81a47..99d4baa 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
@@ -16,6 +16,8 @@
 
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.ChangeListener;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.events.ChangeEvent;
 import com.google.gerrit.server.git.WorkQueue;
@@ -33,6 +35,7 @@
 import java.util.concurrent.Future;
 import java.util.concurrent.LinkedBlockingQueue;
 
+@RequiresCapability(GlobalCapability.STREAM_EVENTS)
 @CommandMetaData(name = "stream-events", descr = "Monitor events occurring in real time")
 final class StreamEvents extends BaseCommand {
   /** Maximum number of events that may be queued up for each connection. */
diff --git a/gerrit-util-cli/pom.xml b/gerrit-util-cli/pom.xml
index 6acf274..9a897c7 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.6</version>
+    <version>2.7</version>
   </parent>
 
   <artifactId>gerrit-util-cli</artifactId>
diff --git a/gerrit-util-ssl/pom.xml b/gerrit-util-ssl/pom.xml
index 04744aa..e687912 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.6</version>
+    <version>2.7</version>
   </parent>
 
   <artifactId>gerrit-util-ssl</artifactId>
diff --git a/gerrit-war/pom.xml b/gerrit-war/pom.xml
index 690860d..b6c90e1 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.6</version>
+    <version>2.7</version>
   </parent>
 
   <artifactId>gerrit-war</artifactId>
diff --git a/plugins/commit-message-length-validator b/plugins/commit-message-length-validator
index a2aaf1a..23bde32 160000
--- a/plugins/commit-message-length-validator
+++ b/plugins/commit-message-length-validator
@@ -1 +1 @@
-Subproject commit a2aaf1a0151959e69e8f578d2f60f6a608052e9a
+Subproject commit 23bde32a6f67ef39f2a368851e3abbedc50db710
diff --git a/plugins/replication b/plugins/replication
index e7b4bce..df8cb39 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit e7b4bce71dabfe1cc2115b00791607e68d61acb8
+Subproject commit df8cb393676880b6e27b42d4b49492be0804c0ee
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index 5fd0585..8d2ab72 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit 5fd0585ece92abfb3f31b82d3e3846b33f161fff
+Subproject commit 8d2ab72281506ab88307f69c433187fea31b6e89
diff --git a/pom.xml b/pom.xml
index daf1b87..7f6cc87 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.6</version>
+  <version>2.7</version>
 
   <name>Gerrit Code Review - Parent</name>
   <url>http://code.google.com/p/gerrit/</url>
@@ -49,7 +49,6 @@
     <jgitVersion>2.3.1.201302201838-r.209-g18030f9</jgitVersion>
     <gwtormVersion>1.6</gwtormVersion>
     <gwtjsonrpcVersion>1.3</gwtjsonrpcVersion>
-    <gwtexpuiVersion>1.3.2</gwtexpuiVersion>
     <gwtVersion>2.5.0</gwtVersion>
     <bouncyCastleVersion>140</bouncyCastleVersion>
     <slf4jVersion>1.6.1</slf4jVersion>
@@ -77,6 +76,7 @@
     <module>gerrit-common</module>
     <module>gerrit-cache-h2</module>
     <module>gerrit-httpd</module>
+    <module>gerrit-gwtexpui</module>
     <module>gerrit-gwtui</module>
     <module>gerrit-launcher</module>
     <module>gerrit-main</module>
@@ -531,18 +531,6 @@
       </dependency>
 
       <dependency>
-        <groupId>gwtexpui</groupId>
-        <artifactId>gwtexpui</artifactId>
-        <version>${gwtexpuiVersion}</version>
-      </dependency>
-      <dependency>
-        <groupId>gwtexpui</groupId>
-        <artifactId>gwtexpui</artifactId>
-        <version>${gwtexpuiVersion}</version>
-        <classifier>sources</classifier>
-      </dependency>
-
-      <dependency>
         <groupId>org.openid4java</groupId>
         <artifactId>openid4java</artifactId>
         <version>0.9.8</version>
@@ -916,20 +904,5 @@
       <id>jgit-repository</id>
       <url>http://download.eclipse.org/jgit/maven</url>
     </repository>
-
-    <repository>
-      <id>java.net-repository</id>
-      <url>http://download.java.net/maven/2/</url>
-    </repository>
-
-    <repository>
-      <id>clojars-repo</id>
-      <url>http://clojars.org/repo</url>
-    </repository>
-
-    <repository>
-      <id>scala-tools</id>
-      <url>http://scala-tools.org/repo-releases</url>
-    </repository>
   </repositories>
 </project>
diff --git a/tools/gwtui_dbg.launch b/tools/gwtui_dbg.launch
index f007da4..8a873be 100644
--- a/tools/gwtui_dbg.launch
+++ b/tools/gwtui_dbg.launch
@@ -25,7 +25,7 @@
 <listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry internalArchive=&quot;/gerrit-common/src/main/java&quot; path=&quot;3&quot; type=&quot;2&quot;/&gt;&#10;"/>
 <listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry internalArchive=&quot;/gerrit-gwtui/src/main/java&quot; path=&quot;3&quot; type=&quot;2&quot;/&gt;&#10;"/>
 <listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry containerPath=&quot;org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER&quot; path=&quot;3&quot; type=&quot;4&quot;/&gt;&#10;"/>
-<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry internalArchive=&quot;/gwtexpui/src/main/java&quot; path=&quot;3&quot; type=&quot;2&quot;/&gt;&#10;"/>
+<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry internalArchive=&quot;/gerrit-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;"/>
diff --git a/tools/release.sh b/tools/release.sh
index 88e4a00..b8f3156 100755
--- a/tools/release.sh
+++ b/tools/release.sh
@@ -15,7 +15,7 @@
 		;;
 	--no-tests|--without-tests)
 		flags="$flags -Dgerrit.acceptance-tests.skip=true"
-		flags="$flags -Dmaven.tests.skip=true"
+		flags="$flags -Dmaven.test.skip=true"
 		shift
 		;;
 	*)