Merge "Add refs/for/<branch>%submit to auto-merge during push"
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index 1012e5a..e38df75 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -134,6 +134,17 @@
 members of `Foo` have submit rights on a project, and the members of
 `Foo-admin` typically do not need to have such rights.
 
+[[ldap_groups]]
+LDAP Groups
+-----------
+
+LDAP groups are Account Groups that are maintained inside of your
+LDAP instance. If you are using LDAP to manage your groups they will
+not appear in the Groups list. However you can use them just like
+regular Account Groups by prefixing your group with "ldap/" in the
+Access Control for a project. For example "ldap/foo-project" will
+add the LDAP "foo-project" group to the access list.
+
 
 Project Access Control Lists
 ----------------------------
@@ -1170,6 +1181,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 f9138a9..28e252a 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -648,8 +648,11 @@
 Maximum number of milliseconds to wait for intraline difference data
 before giving up and disabling it for a particular file pair.  This is
 a work around for an infinite loop bug in the intraline difference
-implementation.  If computation takes longer than the timeout the
-worker thread is terminated and no intraline difference is displayed.
+implementation.
++
+If computation takes longer than the timeout, the worker thread is
+terminated, an error message is shown, and no intraline difference is
+displayed for the file pair.
 +
 Values should use common unit suffixes to express their setting:
 +
@@ -741,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
@@ -776,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
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1300,6 +1323,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
 ~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -2740,7 +2773,7 @@
 +
 Other files support site customization.
 +
-* link:config-headerfooter.html[Site Header/Footer]
+* link:config-themes.html[Themes]
 
 GERRIT
 ------
diff --git a/Documentation/config-reverseproxy.txt b/Documentation/config-reverseproxy.txt
index 7161c4a..0857442 100644
--- a/Documentation/config-reverseproxy.txt
+++ b/Documentation/config-reverseproxy.txt
@@ -28,33 +28,34 @@
 Apache 2 Configuration
 ----------------------
 
-To run Gerrit behind an Apache server using 'mod_proxy', enable the
+To run Gerrit behind an Apache server we cannot use 'mod_proxy'
+directly, as Gerrit relies on getting unmodified escaped forward
+slashes. Depending on the setting of 'AllowEncodedSlashes',
+'mod_proxy' would either decode encoded slashes, or encode them once
+again. Hence, we resort to using 'mod_rewrite'. To enable the
 necessary Apache2 modules:
 
 ----
-  a2enmod proxy_http
+  a2enmod rewrite
   a2enmod ssl          ; # optional, needed for HTTPS / SSL
 ----
 
-Configure an Apache VirtualHost to proxy to the Gerrit daemon,
-setting the 'ProxyPass' line to use the 'http://' URL configured
-above.  Ensure the path of ProxyPass and httpd.listenUrl match,
-or links will redirect to incorrect locations.
+Configure an Apache VirtualHost to proxy to the Gerrit daemon, setting
+the 'RewriteRule' line to use the 'http://' URL configured above.
+Ensure the path of 'RewriteRule' (the part before '$1') and
+httpd.listenUrl match, or links will redirect to incorrect locations.
+
+Note that this configuration allows to pass encoded characters to the
+virtual host, which is potentially dangerous. Be sure to read up on
+this topic and that you understand the risks.
 
 ----
 	<VirtualHost *>
 	  ServerName review.example.com
 
-	  ProxyRequests Off
-	  ProxyVia Off
-	  ProxyPreserveHost On
-
-	  <Proxy *>
-		Order deny,allow
-		Allow from all
-	  </Proxy>
-
-	  ProxyPass /r/ http://127.0.0.1:8081/r/
+	  AllowEncodedSlashes NoDecode
+	  RewriteEngine On
+	  RewriteRule ^/r/(.*) http://localhost:8081/r/$1 [NE,P]
 	</VirtualHost>
 ----
 
diff --git a/Documentation/config-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/database-setup.txt b/Documentation/database-setup.txt
index 0465d62..c559b0e 100644
--- a/Documentation/database-setup.txt
+++ b/Documentation/database-setup.txt
@@ -33,7 +33,7 @@
 full rights on the newly created database:
 
 ----
-  $ createuser --username=postgres -S -R -D -P -E gerrit2
+  $ createuser --username=postgres -RDIElPS gerrit2
   $ createdb --username=postgres -E UTF-8 -O gerrit2 reviewdb
 ----
 
diff --git a/Documentation/dev-eclipse.txt b/Documentation/dev-eclipse.txt
index d49bc5b..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
 
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 d429a1cc..7f7b489 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`
diff --git a/Documentation/index.txt b/Documentation/index.txt
index 707a7ce..573bdc9 100644
--- a/Documentation/index.txt
+++ b/Documentation/index.txt
@@ -39,7 +39,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 c672709..02b7a3d 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-projects.txt b/Documentation/rest-api-projects.txt
index 9aba0e9..f207d43 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -408,6 +408,42 @@
   }
 ----
 
+[[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": false,
+    "use_content_merge": true,
+    "use_signed_off_by": false,
+    "require_change_id": true,
+    "commentlinks": {}
+  }
+----
+
 [[run-gc]]
 Run GC
 ~~~~~~
@@ -896,6 +932,39 @@
 |`size_of_packed_objects`  |Size of packed objects in bytes.
 |======================================
 
+[[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*`|
+If set, authors must complete a contributor agreement on the site
+before pushing any commits or changes to this project.
+|`use_content_merge*`|
+If set, 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*`|
+If set, each change must contain a Signed-off-by line from either the
+author or the uploader in the commit message.
+|`require_change_id*`|
+If set, require a valid link:user-changeid.html[Change-Id] footer in any
+commit uploaded for review. This does not apply to commits pushed
+directly to a branch or tag.
+|`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`.
+|======================================
+
 
 GERRIT
 ------
diff --git a/Documentation/user-changeid.txt b/Documentation/user-changeid.txt
index 0b67205..a4224bd 100644
--- a/Documentation/user-changeid.txt
+++ b/Documentation/user-changeid.txt
@@ -10,10 +10,10 @@
 Gerrit can automatically associate a new version of a change back
 to its original review, even across cherry-picks and rebases.
 
-To be picked up by Gerrit, a Change-Id line must be in the bottom
-portion (last paragraph) of a commit message, and may be mixed
-together with the Signed-off-by, Acked-by, or other such footers.
-For example:
+To be picked up by Gerrit, a Change-Id line must be in the footer
+(last paragraph) of a commit message, and may be mixed
+together with link:user-signedoffby.html[Signed-off-by], Acked-by,
+or other such lines. For example:
 
 ----
   $ git log -1
diff --git a/ReleaseNotes/ReleaseNotes-2.6.txt b/ReleaseNotes/ReleaseNotes-2.6.txt
index 4612aaa..f73c306 100644
--- a/ReleaseNotes/ReleaseNotes-2.6.txt
+++ b/ReleaseNotes/ReleaseNotes-2.6.txt
@@ -66,12 +66,9 @@
 
 * Always show 'Working ...' message
 +
-The 'Working ...' message is now not anymore sticked to the Gerrit page
-top border so that it is now always displayed, even if the user has
-scrolled down the page and the top border is not visible.
-+
-This change make 'Working ...' message relatively positioned to
-browser top border, so that it is always visible for users.
+The 'Working ...' message is relatively positioned from the top of
+the browser, so that the message is always visible, even if the user
+has scrolled down the page.
 
 * link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/config-gerrit.html#suggest.from[
   suggest.from] configures a minimum number of characters before
@@ -430,6 +427,12 @@
 HTML thanks to Gson encoding HTML control characters using Unicode
 character escapes within JSON strings.
 
+* Apache reverse proxies need `AllowEncodedSlashes NoDecode`
++
+When Apache is used as a reverse proxy the NoDecode option
+must be set for AllowEncodedSlashes to prevent Apache from
+mangling Gerrit REST API URLs.
+
 Project Dashboards
 ~~~~~~~~~~~~~~~~~~
 * link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/user-dashboards.html#project-dashboards[
diff --git a/contrib/trivial_rebase.py b/contrib/trivial_rebase.py
index 215f2b1..ff90b6c 100755
--- a/contrib/trivial_rebase.py
+++ b/contrib/trivial_rebase.py
@@ -38,187 +38,208 @@
 import json
 import re
 import subprocess
+import sys
 
-class CheckCallError(OSError):
-  """CheckCall() returned non-0."""
-  def __init__(self, command, cwd, retcode, stdout, stderr=None):
-    OSError.__init__(self, command, cwd, retcode, stdout, stderr)
-    self.command = command
-    self.cwd = cwd
-    self.retcode = retcode
-    self.stdout = stdout
-    self.stderr = stderr
+class TrivialRebase:
+  def __init__(self):
+    usage = "%(prog)s <required options> [--server-port=PORT]"
+    parser = argparse.ArgumentParser(usage=usage)
+    parser.add_argument("--change-url", dest="changeUrl", help="Change URL")
+    parser.add_argument("--project", help="Project path in Gerrit")
+    parser.add_argument("--commit", help="Git commit-ish for this patchset")
+    parser.add_argument("--patchset", type=int, help="The patchset number")
+    parser.add_argument("--private-key-path", dest="private_key_path",
+                        help="Full path to Gerrit SSH daemon's private host key")
+    parser.add_argument("--server", default='localhost',
+                        help="Gerrit SSH server [default: %(default)s]")
+    parser.add_argument("--server-port", dest="port", default='29418',
+                        help="Port to connect to Gerrit's SSH daemon "
+                             "[default: %(default)s]")
+    parser.add_argument("--ssh", default="ssh", help="SSH executable")
+    parser.add_argument("--ssh-port-flag", dest="ssh_port_flag", default="-p", help="SSH port flag")
 
-def CheckCall(command, cwd=None):
-  """Like subprocess.check_call() but returns stdout.
+    args = parser.parse_known_args()[0]
+    if None in [args.changeUrl, args.project, args.commit, args.patchset]:
+      parser.error("Incomplete arguments")
+    try:
+      self.changeId = re.search(r'\d+$', args.changeUrl).group()
+    except AttributeError:
+      parser.error("Invalid changeId")
+    self.project = args.project
+    self.commit = args.commit
+    self.patchset = args.patchset
+    self.private_key_path = args.private_key_path
+    self.server = args.server
+    self.port = args.port
+    self.ssh = args.ssh
+    self.ssh_port_flag = args.ssh_port_flag
 
-  Works on python 2.4
-  """
-  try:
-    process = subprocess.Popen(command, cwd=cwd, stdout=subprocess.PIPE)
-    std_out, std_err = process.communicate()
-  except OSError, e:
-    raise CheckCallError(command, cwd, e.errno, None)
-  if process.returncode:
-    raise CheckCallError(command, cwd, process.returncode, std_out, std_err)
-  return std_out, std_err
+  class CheckCallError(OSError):
+    """CheckCall() returned non-0."""
+    def __init__(self, command, cwd, retcode, stdout, stderr=None):
+      OSError.__init__(self, command, cwd, retcode, stdout, stderr)
+      self.command = command
+      self.cwd = cwd
+      self.retcode = retcode
+      self.stdout = stdout
+      self.stderr = stderr
 
-def GsqlQuery(sql_query, server, port):
-  """Runs a gerrit gsql query and returns the result"""
-  gsql_cmd = ['ssh', '-p', port, server, 'gerrit', 'gsql', '--format',
-              'JSON', '-c', sql_query]
-  try:
-    (gsql_out, _gsql_stderr) = CheckCall(gsql_cmd)
-  except CheckCallError, e:
-    print "return code is %s" % e.retcode
-    print "stdout and stderr is\n%s%s" % (e.stdout, e.stderr)
-    raise
+  def CheckCall(self, command, cwd=None):
+    """Like subprocess.check_call() but returns stdout.
 
-  new_out = gsql_out.replace('}}\n', '}}\nsplit here\n')
-  return new_out.split('split here\n')
+    Works on python 2.4
+    """
+    try:
+      process = subprocess.Popen(command, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+      std_out, std_err = process.communicate()
+    except OSError, e:
+      raise self.CheckCallError(command, cwd, e.errno, None)
+    if process.returncode:
+      raise self.CheckCallError(command, cwd, process.returncode, std_out, std_err)
+    return std_out, std_err
 
-def FindPrevRev(changeId, patchset, server, port):
-  """Finds the revision of the previous patch set on the change"""
-  sql_query = ("\"SELECT revision FROM patch_sets WHERE "
-               "change_id = %s AND patch_set_id = %s\"" % (changeId, (patchset - 1)))
-  revisions = GsqlQuery(sql_query, server, port)
+  def GsqlQuery(self, sql_query):
+    """Runs a gerrit gsql query and returns the result"""
+    gsql_cmd = [self.ssh, self.ssh_port_flag, self.port, self.server, 'gerrit', 'gsql',
+                '--format', 'JSON', '-c', sql_query]
+    try:
+      (gsql_out, _gsql_stderr) = self.CheckCall(gsql_cmd)
+    except self.CheckCallError, e:
+      print "return code is %s" % e.retcode
+      print "stdout and stderr is\n%s%s" % (e.stdout, e.stderr)
+      raise
 
-  json_dict = json.loads(revisions[0], strict=False)
-  return json_dict["columns"]["revision"]
+    new_out = gsql_out.replace('}}\n', '}}\nsplit here\n')
+    return new_out.split('split here\n')
 
-def GetApprovals(changeId, patchset, server, port):
-  """Get all the approvals on a specific patch set
+  def FindPrevRev(self):
+    """Finds the revision of the previous patch set on the change"""
+    sql_query = ("\"SELECT revision FROM patch_sets WHERE "
+                 "change_id = %s AND patch_set_id = %s\"" %
+                 (self.changeId, (self.patchset - 1)))
+    revisions = self.GsqlQuery(sql_query)
 
-  Returns a list of approval dicts"""
-  sql_query = ("\"SELECT value,account_id,category_id FROM patch_set_approvals "
-               "WHERE change_id = %s AND patch_set_id = %s AND value != 0\""
-               % (changeId, (patchset - 1)))
-  gsql_out = GsqlQuery(sql_query, server, port)
-  approvals = []
-  for json_str in gsql_out:
-    data = json.loads(json_str, strict=False)
-    if data["type"] == "row":
-      approvals.append(data["columns"])
-  return approvals
+    json_dict = json.loads(revisions[0], strict=False)
+    return json_dict["columns"]["revision"]
 
-def GetEmailFromAcctId(account_id, server, port):
-  """Returns the preferred email address associated with the account_id"""
-  sql_query = ("\"SELECT preferred_email FROM accounts WHERE account_id = %s\""
-               % account_id)
-  email_addr = GsqlQuery(sql_query, server, port)
+  def GetApprovals(self):
+    """Get all the approvals on a specific patch set
 
-  json_dict = json.loads(email_addr[0], strict=False)
-  return json_dict["columns"]["preferred_email"]
+    Returns a list of approval dicts"""
+    sql_query = ("\"SELECT value,account_id,category_id FROM patch_set_approvals "
+                 "WHERE change_id = %s AND patch_set_id = %s AND value != 0\""
+                 % (self.changeId, (self.patchset - 1)))
+    gsql_out = self.GsqlQuery(sql_query)
+    approvals = []
+    for json_str in gsql_out:
+      data = json.loads(json_str, strict=False)
+      if data["type"] == "row":
+        approvals.append(data["columns"])
+    return approvals
 
-def GetPatchId(revision):
-  git_show_cmd = ['git', 'show', revision]
-  patch_id_cmd = ['git', 'patch-id']
-  patch_id_process = subprocess.Popen(patch_id_cmd, stdout=subprocess.PIPE,
-                                      stdin=subprocess.PIPE)
-  git_show_process = subprocess.Popen(git_show_cmd, stdout=subprocess.PIPE)
-  return patch_id_process.communicate(git_show_process.communicate()[0])[0]
+  def AppendAcctApproval(self, account_id, value):
+    try:
+      newval = self.acct_approvals[account_id] + ' ' + value
+    except KeyError:
+      newval = value
+    self.acct_approvals[account_id] = newval
 
-def SuExec(server, port, private_key, as_user, cmd):
-  suexec_cmd = ['ssh', '-l', "Gerrit Code Review", '-p', port, server, '-i',
-                private_key, 'suexec', '--as', as_user, '--', cmd]
-  CheckCall(suexec_cmd)
+  def GetEmailFromAcctId(self, account_id):
+    """Returns the preferred email address associated with the account_id"""
+    sql_query = ("\"SELECT preferred_email FROM accounts WHERE account_id = %s\""
+                 % account_id)
+    email_addr = self.GsqlQuery(sql_query)
 
-def DiffCommitMessages(commit1, commit2):
-  log_cmd1 = ['git', 'log', '--pretty=format:"%an %ae%n%s%n%b"',
-              commit1 + '^!']
-  commit1_log = CheckCall(log_cmd1)
-  log_cmd2 = ['git', 'log', '--pretty=format:"%an %ae%n%s%n%b"',
-              commit2 + '^!']
-  commit2_log = CheckCall(log_cmd2)
-  if commit1_log != commit2_log:
-    return True
-  return False
+    json_dict = json.loads(email_addr[0], strict=False)
+    return json_dict["columns"]["preferred_email"]
 
-def Main():
-  server = 'localhost'
-  usage = "%(prog)s <required options> [--server-port=PORT]"
-  parser = argparse.ArgumentParser(usage=usage)
-  parser.add_argument("--change-url", dest="changeUrl", help="Change URL")
-  parser.add_argument("--project", help="Project path in Gerrit")
-  parser.add_argument("--commit", help="Git commit-ish for this patchset")
-  parser.add_argument("--patchset", type=int, help="The patchset number")
-  parser.add_argument("--private-key-path", dest="private_key_path",
-                      help="Full path to Gerrit SSH daemon's private host key")
-  parser.add_argument("--server-port", dest="port", default='29418',
-                      help="Port to connect to Gerrit's SSH daemon "
-                           "[default: %(default)s]")
+  def GetPatchId(self, revision):
+    git_show_cmd = ['git', 'show', revision]
+    patch_id_cmd = ['git', 'patch-id']
+    git_show_process = subprocess.Popen(git_show_cmd, stdout=subprocess.PIPE)
+    patch_id_process = subprocess.Popen(patch_id_cmd, stdout=subprocess.PIPE,
+                                        stdin=git_show_process.stdout)
+    res = patch_id_process.communicate()[0] or '0'
+    return res.split()[0]
 
-  args = parser.parse_known_args()[0]
-  try:
-    changeId = re.search(r'\d+', args.changeUrl).group()
-  except:
-    parser.print_help()
-    exit(0)
+  def SuExec(self, as_user, cmd):
+    suexec_cmd = [self.ssh, '-l', "Gerrit Code Review", self.ssh_port_flag, self.port, self.server]
+    if self.private_key_path:
+      suexec_cmd += ['-i', self.private_key_path]
+    suexec_cmd += ['suexec', '--as', as_user, '--', cmd]
+    self.CheckCall(suexec_cmd)
 
-  if args.patchset == 1:
-    # Nothing to detect on first patchset
-    exit(0)
-  prev_revision = None
-  prev_revision = FindPrevRev(changeId, args.patchset, server, args.port)
-  if not prev_revision:
-    # Couldn't find a previous revision
-    exit(0)
-  prev_patch_id = GetPatchId(prev_revision)
-  cur_patch_id = GetPatchId(args.commit)
-  if not (prev_patch_id and cur_patch_id):
-    if not prev_patch_id:
-      print "GetPatchId failed for commit %s" % (prev_revision)
-    if not cur_patch_id:
-      print "GetPatchId failed for commit %s" % (options.commit)
-    exit(0)
-  if cur_patch_id.split()[0] != prev_patch_id.split()[0]:
-    # patch-ids don't match
-    exit(0)
-  # Patch ids match. This is a trivial rebase.
-  # In addition to patch-id we should check if the commit message changed. Most
-  # approvers would want to re-review changes when the commit message changes.
-  changed = DiffCommitMessages(prev_revision, args.commit)
-  if changed:
-    # Insert a comment into the change letting the approvers know only the
-    # commit message changed
-    comment_msg = ("\'--message=New patchset patch-id matches previous patchset"
-                   ", but commit message has changed.'")
-    comment_cmd = ['ssh', '-p', args.port, server, 'gerrit', 'approve',
-                   '--project', args.project, comment_msg, args.commit]
-    CheckCall(comment_cmd)
-    exit(0)
+  def DiffCommitMessages(self, prev_commit):
+    log_cmd1 = ['git', 'log', '--pretty=format:"%an %ae%n%s%n%b"',
+                prev_commit + '^!']
+    commit1_log = self.CheckCall(log_cmd1)
+    log_cmd2 = ['git', 'log', '--pretty=format:"%an %ae%n%s%n%b"',
+                self.commit + '^!']
+    commit2_log = self.CheckCall(log_cmd2)
+    if commit1_log != commit2_log:
+      return True
+    return False
 
-  # Need to get all approvals on prior patch set, then suexec them onto
-  # this patchset.
-  approvals = GetApprovals(changeId, args.patchset, server, args.port)
-  gerrit_approve_msg = ("\'Automatically re-added by Gerrit trivial rebase "
-                        "detection script.\'")
-  for approval in approvals:
-    # Note: Sites with different 'copy_min_score' values in the
-    # approval_categories DB table might want different behavior here.
-    # Additional categories should also be added if desired.
-    if approval["category_id"] == "CRVW":
-      approve_category = '--code-review'
-    elif approval["category_id"] == "VRIF":
-      # Don't re-add verifies
-      #approve_category = '--verified'
-      continue
-    elif approval["category_id"] == "SUBM":
-      # We don't care about previous submit attempts
-      continue
-    else:
-      print "Unsupported category: %s" % approval
-      exit(0)
+  def Run(self):
+    if self.patchset == 1:
+      # Nothing to detect on first patchset
+      return
+    prev_revision = self.FindPrevRev()
+    assert prev_revision, "Previous revision not found"
+    prev_patch_id = self.GetPatchId(prev_revision)
+    cur_patch_id = self.GetPatchId(self.commit)
+    if prev_patch_id == '0' and cur_patch_id == '0':
+      print "commits %s and %s are both empty or merge commits" % (prev_revision, self.commit)
+      return
+    if cur_patch_id != prev_patch_id:
+      # patch-ids don't match
+      return
+    # Patch ids match. This is a trivial rebase.
+    # In addition to patch-id we should check if the commit message changed. Most
+    # approvers would want to re-review changes when the commit message changes.
+    changed = self.DiffCommitMessages(prev_revision)
+    if changed:
+      # Insert a comment into the change letting the approvers know only the
+      # commit message changed
+      comment_msg = ("\'--message=New patchset patch-id matches previous patchset"
+                     ", but commit message has changed.'")
+      comment_cmd = [self.ssh, self.ssh_port_flag, self.port, self.server, 'gerrit',
+                     'review', '--project', self.project, comment_msg, self.commit]
+      self.CheckCall(comment_cmd)
+      return
 
-    score = approval["value"]
-    gerrit_approve_cmd = ['gerrit', 'approve', '--project', args.project,
-                          '--message', gerrit_approve_msg, approve_category,
-                          score, args.commit]
-    email_addr = GetEmailFromAcctId(approval["account_id"], server,
-                                    args.port)
-    SuExec(server, args.port, args.private_key_path, email_addr,
-           ' '.join(gerrit_approve_cmd))
-  exit(0)
+    # Need to get all approvals on prior patch set, then suexec them onto
+    # this patchset.
+    approvals = self.GetApprovals()
+    self.acct_approvals = dict()
+    for approval in approvals:
+      # Note: Sites with different 'copy_min_score' values in the
+      # approval_categories DB table might want different behavior here.
+      # Additional categories should also be added if desired.
+      if approval["category_id"] == "Code-Review" and approval['value'] != '-2':
+        self.AppendAcctApproval(approval['account_id'], '--code-review %s' % approval['value'])
+      elif approval["category_id"] == "Verified":
+        # Don't re-add verifies
+        # self.AppendAcctApproval(approval['account_id'], '--verified %s' % approval['value'])
+        continue
+      elif approval["category_id"] == "SUBM":
+        # We don't care about previous submit attempts
+        continue
+      else:
+        self.AppendAcctApproval(acct_approvals, approval['account_id'], '--%s %s' %
+                                (approval['category_id'].lower().replace(' ', '-'),
+                                 approval['value']))
+
+    gerrit_review_msg = ("\'Automatically re-added by Gerrit trivial rebase "
+                          "detection script.\'")
+    for acct, flags in self.acct_approvals.items():
+      gerrit_review_cmd = ['gerrit', 'review', '--project', self.project,
+                            '--message', gerrit_review_msg, flags, self.commit]
+      email_addr = self.GetEmailFromAcctId(acct)
+      self.SuExec(email_addr, ' '.join(gerrit_review_cmd))
 
 if __name__ == "__main__":
-  Main()
+  try:
+    TrivialRebase().Run()
+  except AssertionError, e:
+    print >> sys.stderr, e
diff --git a/gerrit-common/pom.xml b/gerrit-common/pom.xml
index d9173f0..8f133e9 100644
--- a/gerrit-common/pom.xml
+++ b/gerrit-common/pom.xml
@@ -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/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/PatchScript.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
index 2308b77..fecbb76 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
@@ -53,6 +53,7 @@
   protected boolean hugeFile;
   protected boolean intralineDifference;
   protected boolean intralineFailure;
+  protected boolean intralineTimeout;
 
   public PatchScript(final Change.Key ck, final ChangeType ct, final String on,
       final String nn, final FileMode om, final FileMode nm,
@@ -60,7 +61,7 @@
       final SparseFileContent ca, final SparseFileContent cb,
       final List<Edit> e, final DisplayMethod ma, final DisplayMethod mb,
       final CommentDetail cd, final List<Patch> hist, final boolean hf,
-      final boolean id, final boolean idf) {
+      final boolean id, final boolean idf, final boolean idt) {
     changeId = ck;
     changeType = ct;
     oldName = on;
@@ -79,6 +80,7 @@
     hugeFile = hf;
     intralineDifference = id;
     intralineFailure = idf;
+    intralineTimeout = idt;
   }
 
   protected PatchScript() {
@@ -152,6 +154,10 @@
     return intralineFailure;
   }
 
+  public boolean hasIntralineTimeout() {
+    return intralineTimeout;
+  }
+
   public boolean isExpandAllComments() {
     return diffPrefs.isExpandAllComments();
   }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/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/errors/NoSuchEntityException.java b/gerrit-common/src/main/java/com/google/gerrit/common/errors/NoSuchEntityException.java
index c47cf07..1829c8b 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/errors/NoSuchEntityException.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/errors/NoSuchEntityException.java
@@ -23,4 +23,8 @@
   public NoSuchEntityException() {
     super(MESSAGE);
   }
+
+  public NoSuchEntityException(String message) {
+    super(message);
+  }
 }
diff --git a/gerrit-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..86845d3
--- /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-SNAPSHOT</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-gwtexpui/src/main/java/com/google/gwtexpui/clippy/Clippy.gwt.xml b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/Clippy.gwt.xml
new file mode 100644
index 0000000..0e9b072
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/Clippy.gwt.xml
@@ -0,0 +1,20 @@
+<!--
+ 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.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/MessageAttribute.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/MessageAttribute.java
copy to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyCss.java
index 71b38b5..68495e8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/MessageAttribute.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyCss.java
@@ -1,4 +1,4 @@
-// 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.
@@ -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 MessageAttribute {
-    public Long timestamp;
-    public AccountAttribute reviewer;
-    public String message;
+import com.google.gwt.resources.client.CssResource;
+
+public interface ClippyCss extends CssResource {
+  String label();
+  String control();
 }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyResources.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyResources.java
new file mode 100644
index 0000000..4c2b8981
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyResources.java
@@ -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.
+
+package com.google.gwtexpui.clippy.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.resources.client.ClientBundle;
+
+public interface ClippyResources extends ClientBundle {
+  public static final ClippyResources I = GWT.create(ClippyResources.class);
+
+  @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-gwtexpui/src/main/java/com/google/gwtexpui/css/CSS.gwt.xml b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/css/CSS.gwt.xml
new file mode 100644
index 0000000..b385987
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/css/CSS.gwt.xml
@@ -0,0 +1,19 @@
+<!--
+ 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>
+  <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-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/GlobalKey.gwt.xml b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/GlobalKey.gwt.xml
new file mode 100644
index 0000000..771050f
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/GlobalKey.gwt.xml
@@ -0,0 +1,20 @@
+<!--
+ 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.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-server/src/main/java/com/google/gerrit/server/events/ApprovalAttribute.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyResources.java
similarity index 60%
copy from gerrit-server/src/main/java/com/google/gerrit/server/events/ApprovalAttribute.java
copy to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyResources.java
index baa660c..a52ca2a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/ApprovalAttribute.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,13 +12,14 @@
 // 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 ApprovalAttribute {
-    public String type;
-    public String description;
-    public String value;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.resources.client.ClientBundle;
 
-    public Long grantedOn;
-    public AccountAttribute by;
+public interface KeyResources extends ClientBundle {
+  public static final KeyResources I = GWT.create(KeyResources.class);
+
+  @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-gwtexpui/src/main/java/com/google/gwtexpui/linker/ServerPlannedIFrameLinker.gwt.xml b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/ServerPlannedIFrameLinker.gwt.xml
new file mode 100644
index 0000000..a6978ab
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/ServerPlannedIFrameLinker.gwt.xml
@@ -0,0 +1,19 @@
+<!--
+ 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>
+  <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-gwtexpui/src/main/java/com/google/gwtexpui/progress/Progress.gwt.xml b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/Progress.gwt.xml
new file mode 100644
index 0000000..0df8928
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/Progress.gwt.xml
@@ -0,0 +1,19 @@
+<!--
+ 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.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/ApprovalAttribute.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/ApprovalAttribute.java
copy to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressCss.java
index baa660c..9de2748 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/ApprovalAttribute.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,13 +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 ApprovalAttribute {
-    public String type;
-    public String description;
-    public String value;
+import com.google.gwt.resources.client.CssResource;
 
-    public Long grantedOn;
-    public AccountAttribute by;
+public interface ProgressCss extends CssResource {
+  String container();
+  String text();
+  String bar();
 }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressResources.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressResources.java
new file mode 100644
index 0000000..0276e9a
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressResources.java
@@ -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.
+
+package com.google.gwtexpui.progress.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.resources.client.ClientBundle;
+
+public interface ProgressResources extends ClientBundle {
+  public static final ProgressResources I = GWT.create(ProgressResources.class);
+
+  @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-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/SafeHtml.gwt.xml b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/SafeHtml.gwt.xml
new file mode 100644
index 0000000..0df8928
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/SafeHtml.gwt.xml
@@ -0,0 +1,19 @@
+<!--
+ 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.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/ApprovalAttribute.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/ApprovalAttribute.java
copy to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/Buffer.java
index baa660c..d79c580 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/ApprovalAttribute.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,13 +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 ApprovalAttribute {
-    public String type;
-    public String description;
-    public String value;
+interface Buffer {
+  void append(boolean v);
 
-    public Long grantedOn;
-    public AccountAttribute by;
+  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/MessageAttribute.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/MessageAttribute.java
copy to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlCss.java
index 71b38b5..f6836a0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/MessageAttribute.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlCss.java
@@ -1,4 +1,4 @@
-// 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.
@@ -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.safehtml.client;
 
-public class MessageAttribute {
-    public Long timestamp;
-    public AccountAttribute reviewer;
-    public String message;
+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/ApprovalAttribute.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/ApprovalAttribute.java
copy to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlResources.java
index baa660c..e3f5724 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/ApprovalAttribute.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,13 +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 ApprovalAttribute {
-    public String type;
-    public String description;
-    public String value;
+import com.google.gwt.resources.client.ClientBundle;
 
-    public Long grantedOn;
-    public AccountAttribute by;
+public interface SafeHtmlResources extends ClientBundle {
+  @Source("safehtml.css")
+  SafeHtmlCss css();
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ApprovalAttribute.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/ApprovalAttribute.java
copy to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlString.java
index baa660c..a229421 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/ApprovalAttribute.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,13 +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 ApprovalAttribute {
-    public String type;
-    public String description;
-    public String value;
+class SafeHtmlString extends SafeHtml {
+  private final String html;
 
-    public Long grantedOn;
-    public AccountAttribute by;
+  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/pom.xml b/gerrit-gwtui/pom.xml
index 7f72ea4..5446ea0 100644
--- a/gerrit-gwtui/pom.xml
+++ b/gerrit-gwtui/pom.xml
@@ -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>
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/FormatUtil.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
index ee9fa4f..37c3358 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
@@ -124,13 +124,8 @@
     return RelativeDateFormatter.format(dt);
   }
 
-  @Deprecated
-  public static String nameEmail(com.google.gerrit.common.data.AccountInfo acct) {
-    return nameEmail(asInfo(acct));
-  }
-
   /**
-   * Formats an account as an name and an email address.
+   * Formats an account as a name and an email address.
    * <p>
    * Example output:
    * <ul>
@@ -142,7 +137,7 @@
    */
   public static String nameEmail(AccountInfo info) {
     String name = info.name();
-    if (name == null) {
+    if (name == null || name.trim().isEmpty()) {
       name = Gerrit.getConfig().getAnonymousCowardName();
     }
 
@@ -165,11 +160,6 @@
     return name(asInfo(acct));
   }
 
-  @Deprecated
-  public static String name(com.google.gerrit.common.data.AccountInfo acct) {
-    return name(asInfo(acct));
-  }
-
   /**
    * Formats an account name.
    * <p>
@@ -177,7 +167,7 @@
    * returns a longer form that includes the email address.
    */
   public static String name(AccountInfo ai) {
-    if (ai.name() != null) {
+    if (ai.name() != null && !ai.name().trim().isEmpty()) {
       return ai.name();
     }
     String email = ai.email();
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 3804c96..2963846 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
@@ -95,6 +95,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;
@@ -552,9 +553,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) {
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..031f248 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
@@ -176,6 +176,7 @@
   String patchSetLink();
   String patchSetRevision();
   String patchSetUserIdentity();
+  String patchSetWithDraft();
   String patchSizeCell();
   String pluginsTable();
   String posscore();
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/Themer.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Themer.java
new file mode 100644
index 0000000..8221668
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Themer.java
@@ -0,0 +1,80 @@
+// 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) {
+    set(theme.css() != null ? theme.css() : cssText,
+        theme.header() != null ? theme.header() : headerHtml,
+        theme.footer() != null ? theme.footer() : footerHtml);
+  }
+
+  public void clear() {
+    set(cssText, headerHtml, footerHtml);
+  }
+
+  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/account/ContactPanelShort.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
index cba2f0b..f35bd4b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
@@ -348,7 +348,7 @@
 
   void doSave(final AsyncCallback<Account> onSave) {
     String newName = canEditFullName() ? nameTxt.getText() : null;
-    if ("".equals(newName)) {
+    if (newName != null && newName.trim().isEmpty()) {
       newName = null;
     }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java
index 13de8e7..a528912 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java
@@ -196,7 +196,7 @@
   }
 
   protected void doAddNew() {
-    final String projectName = nameTxt.getText();
+    final String projectName = nameTxt.getText().trim();
     if ("".equals(projectName)) {
       return;
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
index 1637919..ce27780 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/admin/GroupListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
index 07f25f4..dac0b6a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
@@ -18,7 +18,7 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.groups.GroupMap;
-import com.google.gerrit.client.rpc.ScreenLoadCallback;
+import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.AccountScreen;
 import com.google.gerrit.client.ui.FilteredUserInterface;
 import com.google.gerrit.client.ui.IgnoreOutdatedFilterResultsCallbackWrapper;
@@ -54,6 +54,7 @@
   @Override
   protected void onLoad() {
     super.onLoad();
+    display();
     refresh();
   }
 
@@ -62,9 +63,9 @@
         : ADMIN_GROUPS + "?filter=" + URL.encodeQueryString(subname));
     GroupMap.match(subname,
         new IgnoreOutdatedFilterResultsCallbackWrapper<GroupMap>(this,
-            new ScreenLoadCallback<GroupMap>(this) {
+            new GerritCallback<GroupMap>() {
               @Override
-              protected void preDisplay(final GroupMap result) {
+              public void onSuccess(GroupMap result) {
                 groups.display(result, subname);
                 groups.finishDisplay();
               }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
index 6af437c..ee58420 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
@@ -21,7 +21,7 @@
 import com.google.gerrit.client.GitwebLink;
 import com.google.gerrit.client.projects.ProjectInfo;
 import com.google.gerrit.client.projects.ProjectMap;
-import com.google.gerrit.client.rpc.ScreenLoadCallback;
+import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.FilteredUserInterface;
 import com.google.gerrit.client.ui.HighlightingInlineHyperlink;
 import com.google.gerrit.client.ui.IgnoreOutdatedFilterResultsCallbackWrapper;
@@ -63,6 +63,7 @@
   @Override
   protected void onLoad() {
     super.onLoad();
+    display();
     refresh();
   }
 
@@ -71,9 +72,9 @@
         : ADMIN_PROJECTS + "?filter=" + URL.encodeQueryString(subname));
     ProjectMap.match(subname,
         new IgnoreOutdatedFilterResultsCallbackWrapper<ProjectMap>(this,
-            new ScreenLoadCallback<ProjectMap>(this) {
+            new GerritCallback<ProjectMap>() {
               @Override
-              protected void preDisplay(final ProjectMap result) {
+              public void onSuccess(ProjectMap result) {
                 projects.display(result);
               }
             }));
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 5cd6cdb..e5c8dcf 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.SubmitTypeRecord;
 import com.google.gerrit.reviewdb.client.Change;
@@ -37,10 +38,11 @@
   }
 
   public void display(Change 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.currentPatchSetId(), starred,
-      canEditCommitMessage,  info.getMessage());
+        canEditCommitMessage, info.getMessage(), commentLinkProcessor);
   }
 }
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 c219fa3..e6ad24a 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;
@@ -79,6 +82,7 @@
   private PatchSetsBlock patchSetsBlock;
 
   private Panel comments;
+  private CommentLinkProcessor commentLinkProcessor;
 
   private KeyCommandSet keysNavigation;
   private KeyCommandSet keysAction;
@@ -260,10 +264,26 @@
   @Override
   public void onValueChange(final ValueChangeEvent<ChangeDetail> event) {
     if (isAttached()) {
-      // 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(
@@ -271,7 +291,7 @@
               changeInfo = result;
               display(event.getValue());
             }
-          });
+          }));
     }
   }
 
@@ -292,7 +312,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);
@@ -411,8 +432,8 @@
         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) {
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..0c94081 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
@@ -91,7 +91,7 @@
     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 +110,11 @@
     } else {
       addOpenHandler(this);
     }
+
+    if(ps.getHasDraftComments()) {
+      addStyleName(Gerrit.RESOURCES.css().patchSetWithDraft());
+    }
+
   }
 
   public void setDiffBaseId(PatchSet.Id diffBaseId) {
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 5791f68..b2e8b01 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
@@ -244,8 +244,7 @@
 
     Key thisKey = patch.getKey();
     PatchLink link;
-    if (patchType == PatchScreen.Type.SIDE_BY_SIDE
-        && patch.getPatchType() == Patch.PatchType.UNIFIED) {
+    if (patchType == PatchScreen.Type.SIDE_BY_SIDE) {
       link = new PatchLink.SideBySide("", base, thisKey, index, detail, this);
     } else {
       link = new PatchLink.Unified("", base, thisKey, index, detail, this);
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 97949e3..2e488ee 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;
@@ -78,6 +79,7 @@
   private boolean saveStateOnUnload = true;
   private List<CommentEditorPanel> commentEditors;
   private ChangeInfo change;
+  private CommentLinkProcessor commentLinkProcessor;
 
   public PublishCommentScreen(final PatchSet.Id psi) {
     patchSetId = psi;
@@ -148,8 +150,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();
@@ -166,7 +168,7 @@
           @Override
           protected void preDisplay(final PatchSetPublishDetail result) {
             send.setEnabled(true);
-            display(result);
+            PublishCommentScreen.this.preDisplay(result, this);
           }
 
           @Override
@@ -176,6 +178,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();
@@ -281,7 +301,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)
@@ -301,7 +321,7 @@
     setPageTitle(Util.M.publishComments(r.getChange().getKey().abbreviate(),
         patchSetId.get()));
     descBlock.display(r.getChange(), null, false, r.getPatchSetInfo(), r.getAccounts(),
-        r.getSubmitTypeRecord());
+       r.getSubmitTypeRecord(), commentLinkProcessor);
 
     if (r.getChange().getStatus().isOpen()) {
       initApprovals(approvalPanel);
@@ -337,7 +357,8 @@
           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());
         } else {
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 255412d..93db54c 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;
@@ -514,6 +514,8 @@
   padding-right: 5px;
   border-right: 1px solid trimColor;
   border-bottom: 1px solid trimColor;
+  vertical-align: middle;
+  height: 20px;
 }
 
 .changeTable a.gwt-InlineHyperlink {
@@ -870,6 +872,10 @@
   font-size: 8pt;
 }
 
+.patchSetWithDraft .header td {
+  color: #ff5555;
+}
+
 .changeScreen .gwt-DisclosurePanel .content {
   margin-bottom: 10px;
 }
@@ -1158,7 +1164,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/patches/AbstractPatchContentTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java
index 20cc2c1..9b92b7a 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;
@@ -86,6 +87,7 @@
   private HandlerRegistration regComment;
   private final KeyCommandSet keysOpenByEnter;
   private HandlerRegistration regOpenByEnter;
+  private CommentLinkProcessor commentLinkProcessor;
   boolean isDisplayBinary;
 
   protected AbstractPatchContentTable() {
@@ -241,6 +243,10 @@
     render(s, d);
   }
 
+  void setCommentLinkProcessor(CommentLinkProcessor commentLinkProcessor) {
+    this.commentLinkProcessor = commentLinkProcessor;
+  }
+
   protected boolean hasDifferences(PatchScript script) {
     return hasEdits(script) || hasMeta(script);
   }
@@ -292,17 +298,17 @@
   protected SparseHtmlFile getSparseHtmlFileB(PatchScript s) {
     AccountDiffPreference dp = new AccountDiffPreference(s.getDiffPrefs());
 
+    SparseFileContent b = s.getB();
     PrettyFormatter f = ClientSideFormatter.FACTORY.get();
     f.setDiffPrefs(dp);
-    f.setFileName(s.getB().getPath());
+    f.setFileName(b.getPath());
     f.setEditFilter(PrettyFormatter.B);
     f.setEditList(s.getEdits());
 
-    if (dp.isSyntaxHighlighting() && s.getA().isWholeFile() && !s.getB().isWholeFile()) {
-      f.format(s.getB().apply(s.getA(), s.getEdits()));
-    } else {
-      f.format(s.getB());
+    if (s.getA().isWholeFile() && !b.isWholeFile()) {
+      b = b.apply(s.getA(), s.getEdits());
     }
+    f.format(b);
     return f;
   }
 
@@ -553,7 +559,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;
@@ -690,7 +697,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);
@@ -864,7 +872,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 ad022b5..63d7d8e 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,6 +16,7 @@
 
 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;
@@ -25,10 +26,10 @@
 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;
@@ -58,7 +59,9 @@
   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());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java
index 9d4a481..8c9c56b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java
@@ -36,6 +36,7 @@
   String patchHistoryTitle();
   String disabledOnLargeFiles();
   String intralineFailure();
+  String intralineTimeout();
   String illegalNumberOfColumns();
 
   String upToChange();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties
index e9fec4b..5acdb5f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties
@@ -18,6 +18,7 @@
 patchSet = Patch Set
 disabledOnLargeFiles = Disabled on very large source files.
 intralineFailure = Intraline difference not available due to server error.
+intralineTimeout = Intraline difference not available due to timeout.
 illegalNumberOfColumns = The number of columns cannot be zero or negative
 
 upToChange = Up to change
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 297be6b..b18946b 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,8 +21,11 @@
 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;
@@ -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;
@@ -120,6 +125,7 @@
   private HandlerRegistration regNavigation;
   private HandlerRegistration regAction;
   private boolean intralineFailure;
+  private boolean intralineTimeout;
 
   /**
    * How this patch should be displayed in the patch screen.
@@ -365,23 +371,40 @@
     if (isFirst && fileList != null) {
       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) {
@@ -397,7 +420,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,
@@ -405,7 +429,8 @@
             @Override
             public void onSuccess(PatchSetDetail result) {
               commitMessageBlock.setVisible(true);
-              commitMessageBlock.display(result.getInfo().getMessage());
+              commitMessageBlock.display(result.getInfo().getMessage(),
+                  commentLinkProcessor);
             }
           });
     }
@@ -432,6 +457,7 @@
       contentTable.removeFromParent();
       contentTable = new UnifiedDiffTable();
       contentTable.fileList = fileList;
+      contentTable.setCommentLinkProcessor(commentLinkProcessor);
       contentPanel.add(contentTable);
       setToken(Dispatcher.toPatchUnified(idSideA, patchKey));
     }
@@ -468,6 +494,7 @@
     }
 
     intralineFailure = isFirst && script.hasIntralineFailure();
+    intralineTimeout = isFirst && script.hasIntralineTimeout();
   }
 
   @Override
@@ -476,6 +503,9 @@
     if (intralineFailure) {
       intralineFailure = false;
       new ErrorDialog(PatchUtil.C.intralineFailure()).show();
+    } else if (intralineTimeout) {
+      intralineTimeout = false;
+      new ErrorDialog(PatchUtil.C.intralineTimeout()).show();
     }
     if (topView != null && prefs.get().isRetainHeader()) {
       setTopView(topView);
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 15ab951..b10637a 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
@@ -39,6 +39,7 @@
 import com.google.gwt.user.client.ui.UIObject;
 import com.google.gwtexpui.safehtml.client.SafeHtml;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+
 import org.eclipse.jgit.diff.Edit;
 
 import java.util.ArrayList;
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..a4a089e 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
@@ -32,8 +32,8 @@
 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;
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/AccountSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java
index b113a3b..0bf0ea9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java
@@ -53,11 +53,11 @@
     }
 
     public String getDisplayString() {
-      return FormatUtil.nameEmail(info);
+      return FormatUtil.nameEmail(FormatUtil.asInfo(info));
     }
 
     public String getReplacementString() {
-      return FormatUtil.nameEmail(info);
+      return FormatUtil.nameEmail(FormatUtil.asInfo(info));
     }
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/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..cb4dc7d 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
@@ -53,11 +53,13 @@
   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));
@@ -68,7 +70,8 @@
     fmt.getElement(0, 2).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());
@@ -118,7 +121,7 @@
 
     messageSummary.setText(summarize(message));
     SafeHtml buf = new SafeHtmlBuilder().append(message).wikify();
-    buf = CommentLinkProcessor.apply(buf);
+    buf = commentLinkProcessor.apply(buf);
     SafeHtml.set(messageText, buf);
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ReviewerSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ReviewerSuggestOracle.java
index f3b1439..3f1de2b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ReviewerSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ReviewerSuggestOracle.java
@@ -65,7 +65,7 @@
     public String getDisplayString() {
       final AccountInfo accountInfo = reviewerInfo.getAccountInfo();
       if (accountInfo != null) {
-        return FormatUtil.nameEmail(accountInfo);
+        return FormatUtil.nameEmail(FormatUtil.asInfo(accountInfo));
       }
       return reviewerInfo.getGroup().getName() + " ("
           + Util.C.suggestedGroupLabel() + ")";
@@ -74,7 +74,7 @@
     public String getReplacementString() {
       final AccountInfo accountInfo = reviewerInfo.getAccountInfo();
       if (accountInfo != null) {
-        return FormatUtil.nameEmail(accountInfo);
+        return FormatUtil.nameEmail(FormatUtil.asInfo(accountInfo));
       }
       return reviewerInfo.getGroup().getName();
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java
index e7c2d84..ccc6a56 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.set(null);
+    }
+  }
+
   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-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/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/BaseServiceImplementation.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java
index b124824..3277992 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java
@@ -72,7 +72,11 @@
     } catch (InvalidQueryException e) {
       callback.onFailure(e);
     } catch (NoSuchProjectException e) {
-      callback.onFailure(new NoSuchEntityException());
+      if (e.getMessage() != null) {
+        callback.onFailure(new NoSuchEntityException(e.getMessage()));
+      } else {
+        callback.onFailure(new NoSuchEntityException());
+      }
     } catch (NoSuchGroupException e) {
       callback.onFailure(new NoSuchEntityException());
     } catch (OrmRuntimeException e) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java
index 120b9af..08bcf88 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
@@ -178,6 +178,12 @@
     ResultSet<PatchSet> source = db.patchSets().byChange(changeId);
     List<PatchSet> patches = new ArrayList<PatchSet>();
     for (PatchSet ps : source) {
+      final CurrentUser user = control.getCurrentUser();
+      if (user instanceof IdentifiedUser) {
+        final Account.Id me = ((IdentifiedUser) user).getAccountId();
+        ps.setHasDraftComments(db.patchComments()
+            .draftByPatchSetAuthor(ps.getId(), me).iterator().hasNext());
+      }
       if (control.isPatchVisible(ps, db)) {
         patches.add(ps);
       }
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/java/com/google/gerrit/httpd/rpc/patch/PatchScriptBuilder.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptBuilder.java
index 816bbef..5019403 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptBuilder.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptBuilder.java
@@ -133,6 +133,7 @@
       throws IOException {
     boolean intralineDifferenceIsPossible = true;
     boolean intralineFailure = false;
+    boolean intralineTimeout = false;
 
     a.path = oldName(content);
     b.path = newName(content);
@@ -160,10 +161,14 @@
             break;
 
           case ERROR:
-          case TIMEOUT:
             intralineDifferenceIsPossible = false;
             intralineFailure = true;
             break;
+
+          case TIMEOUT:
+            intralineDifferenceIsPossible = false;
+            intralineTimeout = true;
+            break;
         }
       } else {
         intralineDifferenceIsPossible = false;
@@ -198,10 +203,10 @@
         context = diffPrefs.getContext();
         hugeFile = true;
 
-      } else if (diffPrefs.isSyntaxHighlighting()) {
-        // In order to syntax highlight the file properly we need to
-        // give the client the complete file contents. So force our
-        // context temporarily to the complete file size.
+      } else {
+        // In order to expand the skipped common lines or syntax highlight the
+        // file properly we need to give the client the complete file contents.
+        // So force our context temporarily to the complete file size.
         //
         context = MAX_CONTEXT;
       }
@@ -212,7 +217,7 @@
         content.getOldName(), content.getNewName(), a.fileMode, b.fileMode,
         content.getHeaderLines(), diffPrefs, a.dst, b.dst, edits,
         a.displayMethod, b.displayMethod, comments, history, hugeFile,
-        intralineDifferenceIsPossible, intralineFailure);
+        intralineDifferenceIsPossible, intralineFailure, intralineTimeout);
   }
 
   private static boolean isModify(PatchListEntry content) {
diff --git a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/container/ConfigurationError.html b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/container/ConfigurationError.html
index 7294012..0bc3369 100644
--- a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/container/ConfigurationError.html
+++ b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/container/ConfigurationError.html
@@ -49,22 +49,16 @@
 &lt;VirtualHost <span class='ServerName'>review.example.com</span><span class='ServerPort'>:80</span>&gt;
     ServerName <span class='ServerName'>review.example.com</span>
 
-    ProxyRequests Off
-    ProxyVia Off
-    ProxyPreserveHost On
-
-    &lt;Proxy *&gt;
-          Order deny,allow
-          Allow from all
-    &lt;/Proxy&gt;
-
 <div class='apache_auth'>    &lt;Location <span class='ContextPath'>/r</span>/login/&gt;
       AuthType Basic
       AuthName "Gerrit Code Review"
       Require valid-user
       ...
     &lt;/Location&gt;</div>
-    ProxyPass <span class='ContextPath'>/r</span>/ http://...<span class='ContextPath'>/r</span>/
+
+    AllowEncodedSlashes NoDecode
+    RewriteEngine On
+    RewriteRule ^<span class='ContextPath'>/r</span>/(.*) http://...<span class='ContextPath'>/r</span>/$1 [NE,P]
 &lt;/VirtualHost&gt;
     </pre>
   </body>
diff --git a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/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-package-plugins/pom.xml b/gerrit-package-plugins/pom.xml
index f7fd22c..ff403c26 100644
--- a/gerrit-package-plugins/pom.xml
+++ b/gerrit-package-plugins/pom.xml
@@ -49,13 +49,13 @@
     <dependency>
       <groupId>com.googlesource.gerrit.plugins.reviewnotes</groupId>
       <artifactId>reviewnotes</artifactId>
-      <version>1.0-rc0</version>
+      <version>1.0-rc1</version>
       <scope>provided</scope>
     </dependency>
     <dependency>
       <groupId>com.googlesource.gerrit.plugins.validators</groupId>
       <artifactId>commit-message-length-validator</artifactId>
-      <version>1.0-rc0</version>
+      <version>1.0-rc1</version>
       <scope>provided</scope>
     </dependency>
   </dependencies>
diff --git a/gerrit-plugin-api/pom.xml b/gerrit-plugin-api/pom.xml
index 0abe81f..569ed50 100644
--- a/gerrit-plugin-api/pom.xml
+++ b/gerrit-plugin-api/pom.xml
@@ -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-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-prettify/pom.xml b/gerrit-prettify/pom.xml
index 2479adc..fa4ae55 100644
--- a/gerrit-prettify/pom.xml
+++ b/gerrit-prettify/pom.xml
@@ -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-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSet.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSet.java
index 54c556d..4afddfd 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSet.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSet.java
@@ -136,6 +136,9 @@
   @Column(id = 5)
   protected boolean draft;
 
+  /** Not persisted in the database */
+  protected boolean hasDraftComments;
+
   protected PatchSet() {
   }
 
@@ -187,6 +190,14 @@
     return id.toRefName();
   }
 
+  public boolean getHasDraftComments() {
+    return hasDraftComments;
+  }
+
+  public void setHasDraftComments(boolean hasDraftComments) {
+    this.hasDraftComments = hasDraftComments;
+  }
+
   @Override
   public String toString() {
     return "[PatchSet " + getId().toString() + "]";
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java
index 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/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/server/ChangeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
index 70cf586..2d1707c 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
@@ -304,8 +304,7 @@
 
       LabelTypes labelTypes = refControl.getProjectControl().getLabelTypes();
       changeInserter.insertChange(db, change, cmsg, ps, revertCommit,
-          labelTypes, revertCommit.getFooterLines(), info,
-          Collections.<Account.Id> emptySet());
+          labelTypes, info, Collections.<Account.Id> emptySet());
 
       final RevertedSender cm = revertedSenderFactory.create(change);
       cm.setFrom(user.getAccountId());
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/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/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/change/ChangeInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
index d444633..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
@@ -30,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 {
@@ -54,22 +52,22 @@
   }
 
   public void insertChange(ReviewDb db, Change change, PatchSet ps,
-      RevCommit commit, LabelTypes labelTypes, List<FooterLine> footerLines,
-      PatchSetInfo info, Set<Account.Id> reviewers) throws OrmException {
-    insertChange(db, change, null, ps, commit, labelTypes, footerLines, info, reviewers);
+      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, List<FooterLine> footerLines, PatchSetInfo info,
-      Set<Account.Id> reviewers) throws OrmException {
+      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) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitRule.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitRule.java
index 170f8e8..db18f0d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitRule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitRule.java
@@ -129,10 +129,9 @@
     return out;
   }
 
-  @SuppressWarnings("unchecked")
   private static List<Term> eval(SubmitRuleEvaluator evaluator)
       throws RuleEvalException {
-    return evaluator.evaluate().toJava();
+    return evaluator.evaluate();
   }
 
   static class Record {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitType.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitType.java
index f58fe75..e7e1f32 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitType.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitType.java
@@ -30,12 +30,12 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
-import com.googlecode.prolog_cafe.lang.ListTerm;
 import com.googlecode.prolog_cafe.lang.Term;
 
 import org.kohsuke.args4j.Option;
 
 import java.io.ByteArrayInputStream;
+import java.util.List;
 
 public class TestSubmitType implements RestModifyView<RevisionResource, Input> {
   private final ReviewDb db;
@@ -76,7 +76,7 @@
           ? new ByteArrayInputStream(input.rule.getBytes(Charsets.UTF_8))
           : null);
 
-    ListTerm results;
+    List<Term> results;
     try {
       results = evaluator.evaluate();
     } catch (RuleEvalException e) {
@@ -84,12 +84,12 @@
           "rule failed with exception: %s",
           e.getMessage()));
     }
-    if (results.isNil()) {
+    if (results.isEmpty()) {
       throw new BadRequestException(String.format(
           "rule %s has no solution",
           evaluator.getSubmitRule()));
     }
-    Term type = results.car();
+    Term type = results.get(0);
     if (!type.isSymbol()) {
       throw new BadRequestException(String.format(
           "rule %s produced invalid result: %s",
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 00a5d2e..660344a 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
@@ -94,6 +94,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;
@@ -113,6 +115,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 {
@@ -253,5 +257,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/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/MergeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
index c47084a..862eb2b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
@@ -315,11 +315,11 @@
   }
 
   private static boolean isCodeReview(LabelId id) {
-    return "CRVW".equals(id.get()) || "Code-Review".equalsIgnoreCase(id.get());
+    return "Code-Review".equalsIgnoreCase(id.get());
   }
 
   private static boolean isVerified(LabelId id) {
-    return "VRIF".equals(id.get()) || "Verified".equalsIgnoreCase(id.get());
+    return "Verified".equalsIgnoreCase(id.get());
   }
 
   public List<PatchSetApproval> getApprovalsForCommit(final CodeReviewCommit n) {
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 852b5e7..db70b29 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";
 
@@ -131,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;
 
@@ -148,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;
   }
@@ -233,6 +281,10 @@
     return labelSections;
   }
 
+  public Collection<CommentLinkInfo> getCommentLinkSections() {
+    return commentLinkSections;
+  }
+
   public GroupReference resolve(AccountGroup group) {
     return resolve(GroupReference.forGroup(group));
   }
@@ -333,6 +385,7 @@
     loadAccessSections(rc, groupsByName);
     loadNotifySections(rc, groupsByName);
     loadLabelSections(rc);
+    loadCommentLinkSections(rc);
   }
 
   private void loadAccountsSection(
@@ -590,6 +643,25 @@
     }
   }
 
+  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 =
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 2d8c4c5..45c6e5a 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
@@ -1493,8 +1493,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;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
index 97af5ac..8abe501 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -486,7 +486,8 @@
         sb.append('\n');
         sb.append(changeId).append(" I").append(c.name());
         sb.append('\n');
-        sb.append("Hint: A potential Change-Id was found, but it was not in the footer of the commit message.");
+        sb.append("Hint: A potential Change-Id was found, but it was not in the ");
+        sb.append("footer (last paragraph) of the commit message.");
       }
     }
     sb.append('\n');
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/ChangeControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
index 48b0731..6397b96 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
@@ -36,6 +36,7 @@
 import com.googlecode.prolog_cafe.lang.ListTerm;
 import com.googlecode.prolog_cafe.lang.Prolog;
 import com.googlecode.prolog_cafe.lang.StructureTerm;
+import com.googlecode.prolog_cafe.lang.SymbolTerm;
 import com.googlecode.prolog_cafe.lang.Term;
 
 import org.slf4j.Logger;
@@ -327,7 +328,6 @@
     return canSubmit(db, patchSet, null, false, false, false);
   }
 
-  @SuppressWarnings("unchecked")
   public List<SubmitRecord> canSubmit(ReviewDb db, PatchSet patchSet,
       @Nullable ChangeData cd, boolean fastEvalLabels, boolean allowClosed,
       boolean allowDraft) {
@@ -355,7 +355,7 @@
           fastEvalLabels,
           "locate_submit_rule", "can_submit",
           "locate_submit_filter", "filter_submit_results");
-      results = evaluator.evaluate().toJava();
+      results = evaluator.evaluate();
     } catch (RuleEvalException e) {
       return logRuleError(e.getMessage(), e);
     }
@@ -475,7 +475,6 @@
     return getSubmitTypeRecord(db, patchSet, null);
   }
 
-  @SuppressWarnings("unchecked")
   public SubmitTypeRecord getSubmitTypeRecord(ReviewDb db, PatchSet patchSet,
       @Nullable ChangeData cd) {
     try {
@@ -492,7 +491,7 @@
           err);
     }
 
-    List<String> results;
+    List<Term> results;
     SubmitRuleEvaluator evaluator;
     try {
       evaluator = new SubmitRuleEvaluator(db, patchSet,
@@ -500,7 +499,7 @@
           false,
           "locate_submit_type", "get_submit_type",
           "locate_submit_type_filter", "filter_submit_type_results");
-      results = evaluator.evaluate().toJava();
+      results = evaluator.evaluate();
     } catch (RuleEvalException e) {
       return logTypeRuleError(e.getMessage(), e);
     }
@@ -513,10 +512,15 @@
       return typeRuleError("Project submit rule has no solution");
     }
 
-    // Take only the first result and convert it to SubmitTypeRecord
-    // This logic will need to change once we support multiple submit types
-    // in the UI
-    String typeName = results.get(0);
+    Term typeTerm = results.get(0);
+    if (!typeTerm.isSymbol()) {
+      log.error("Submit rule '" + evaluator.getSubmitRule() + "' for change "
+          + change.getId() + " of " + getProject().getName()
+          + " did not return a symbol.");
+      return typeRuleError("Project submit rule has invalid solution");
+    }
+
+    String typeName = ((SymbolTerm)typeTerm).name();
     try {
       return SubmitTypeRecord.OK(
           Project.SubmitType.valueOf(typeName.toUpperCase()));
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..3ae78b9
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java
@@ -0,0 +1,60 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.common.collect.Maps;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.git.GitRepositoryManager;
+
+import java.util.Map;
+
+public class GetConfig implements RestReadView<ProjectResource> {
+  public static class ConfigInfo {
+    public final String kind = "gerritcodereview#project_config";
+
+    public Boolean useContributorAgreements;
+    public Boolean useContentMerge;
+    public Boolean useSignedOffBy;
+    public Boolean requireChangeId;
+
+    public Map<String, CommentLinkInfo> commentlinks;
+    public ThemeInfo theme;
+  }
+
+  @Override
+  public ConfigInfo apply(ProjectResource resource) {
+    ConfigInfo result = new ConfigInfo();
+    RefControl refConfig = resource.getControl()
+        .controlForRef(GitRepositoryManager.REF_CONFIG);
+    ProjectState project = resource.getControl().getProjectState();
+    if (refConfig.isVisible()) {
+      result.useContributorAgreements = project.isUseContributorAgreements();
+      result.useContentMerge = project.isUseContentMerge();
+      result.useSignedOffBy = project.isUseSignedOffBy();
+      result.requireChangeId = project.isRequireChangeID();
+    }
+
+    // commentlinks are visible to anyone, as they are used for linkification
+    // on the client side.
+    result.commentlinks = Maps.newLinkedHashMap();
+    for (CommentLinkInfo cl : project.getCommentLinks()) {
+      result.commentlinks.put(cl.name, cl);
+    }
+
+    // Themes are visible to anyone, as they are rendered client-side.
+    result.theme = project.getTheme();
+    return result;
+  }
+}
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/SubmitRuleEvaluator.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
index bcea2c9..349567c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.common.collect.Lists;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -30,6 +31,7 @@
 
 import java.io.InputStream;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 import javax.annotation.Nullable;
@@ -124,8 +126,7 @@
    * @return List of {@link Term} objects returned from the evaluated rules.
    * @throws RuleEvalException
    */
-  public ListTerm evaluate() throws RuleEvalException {
-    List<Term> results = new ArrayList<Term>();
+  public List<Term> evaluate() throws RuleEvalException {
     PrologEnvironment env = getPrologEnvironment();
     try {
       submitRule = env.once("gerrit", userRuleLocatorName, new VariableTerm());
@@ -133,6 +134,7 @@
         env.once("gerrit", "assume_range_from_label");
       }
 
+      List<Term> results = new ArrayList<Term>();
       try {
         for (Term[] template : env.all("gerrit", userRuleWrapperName,
             submitRule, new VariableTerm())) {
@@ -152,7 +154,16 @@
       if (!skipFilters) {
         resultsTerm = runSubmitFilters(resultsTerm, env);
       }
-      return (ListTerm) resultsTerm;
+      if (resultsTerm.isList()) {
+        List<Term> r = Lists.newArrayList();
+        for (Term t = resultsTerm; t.isList();) {
+          ListTerm l = (ListTerm) t;
+          r.add(l.car().dereference());
+          t = l.cdr().dereference();
+        }
+        return r;
+      }
+      return Collections.emptyList();
     } finally {
       env.close();
     }
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/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/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/project/RefControlTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
index 51ea5a2..f1bb7de 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-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
index 244028c..ab70395 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
@@ -21,6 +21,8 @@
 
 import org.apache.sshd.server.Environment;
 
+import java.util.List;
+
 @CommandMetaData(name = "ls-projects", descr = "List projects visible to the caller")
 final class ListProjectsCommand extends BaseCommand {
   @Inject
@@ -33,7 +35,8 @@
       public void run() throws Exception {
         parseCommandLine(impl);
         if (!impl.getFormat().isJson()) {
-          if (impl.isShowTree() && (impl.getShowBranch() != null)) {
+          List<String> showBranch = impl.getShowBranch();
+          if (impl.isShowTree() && (showBranch != null) && !showBranch.isEmpty()) {
             throw new UnloggedFailure(1, "fatal: --tree and --show-branch options are not compatible.");
           }
           if (impl.isShowTree() && impl.isShowDescription()) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
index 48e175d..5769a22 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
@@ -218,10 +218,8 @@
 
   private void applyReview(final ChangeControl ctl, final PatchSet patchSet,
       final PostReview.Input review) throws Exception {
-    if (!review.labels.isEmpty()) {
-      reviewProvider.get().apply(new RevisionResource(
-          new ChangeResource(ctl), patchSet), review);
-    }
+    reviewProvider.get().apply(new RevisionResource(
+        new ChangeResource(ctl), patchSet), review);
   }
 
   private void approveOne(final PatchSet patchSet) throws Exception {
@@ -246,7 +244,7 @@
     // If review labels are being applied, the comment will be included
     // on the review note. We don't need to add it again on the abandon
     // or restore comment.
-    if (!review.labels.isEmpty()) {
+    if (!review.labels.isEmpty() && (abandonChange || restoreChange)) {
       changeComment = null;
     }
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ScpCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ScpCommand.java
index 09c25ff..987380f 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ScpCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ScpCommand.java
@@ -92,6 +92,7 @@
 
   private void runImp() {
     try {
+      readAck();
       if (error != null) {
         throw error;
       }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/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/pom.xml b/pom.xml
index 1ff8e8f..c634b96 100644
--- a/pom.xml
+++ b/pom.xml
@@ -46,10 +46,9 @@
   </issueManagement>
 
   <properties>
-    <jgitVersion>2.3.1.201302201838-r.78-g8fcde4b</jgitVersion>
+    <jgitVersion>2.3.1.201302201838-r.175-g1b4320f</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-launcher</module>
     <module>gerrit-main</module>
     <module>gerrit-openid</module>
@@ -518,18 +518,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-consumer</artifactId>
         <version>0.9.6</version>
@@ -893,6 +881,13 @@
     </dependencies>
   </dependencyManagement>
 
+  <pluginRepositories>
+    <pluginRepository>
+      <id>gerrit-maven</id>
+      <url>https://gerrit-maven.commondatastorage.googleapis.com</url>
+    </pluginRepository>
+  </pluginRepositories>
+
   <repositories>
     <repository>
       <id>gerrit-maven</id>
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;"/>