Merge branch 'stable-2.9' into stable-2.10

* stable-2.9:
  Consume JGit artifacts from Maven Central

Change-Id: I373228681fc1c02f48bcc1e2b556d53b2024bb3f
diff --git a/.buckconfig b/.buckconfig
index d53edcf..43bfa5e 100644
--- a/.buckconfig
+++ b/.buckconfig
@@ -1,4 +1,5 @@
 [alias]
+  all = //:all
   api = //:api
   api_deploy = //tools/maven:api_deploy
   api_install = //tools/maven:api_install
@@ -6,8 +7,10 @@
   war_install = //tools/maven:war_install
   chrome = //:chrome
   docs = //Documentation:html
+  firefox = //:firefox
   gerrit = //:gerrit
   release = //:release
+  safari = //:safari
   withdocs = //:withdocs
 
 [buildfile]
@@ -21,4 +24,4 @@
 
 [cache]
   mode = dir
-  dir = buck-out/cache
+  dir = ~/.gerritcodereview/buck-cache/cache
diff --git a/.buckversion b/.buckversion
index ff1c137..a0c6bc2 100644
--- a/.buckversion
+++ b/.buckversion
@@ -1 +1 @@
-2b80cf780ae31bee6609ebc1bbab9ce6fd004dbe
+0fe4569e871fd6588f7cbfb4b1d4a14baa791a9f
diff --git a/.gitignore b/.gitignore
index e979409..b356144 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,6 @@
+/.apt_generated
 /.classpath
+/.factorypath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
 /.settings/org.eclipse.m2e.core.prefs
@@ -16,4 +18,5 @@
 /local.properties
 *.pyc
 /gwt-unitCache
+*.swp
 *.asc
diff --git a/BUCK b/BUCK
index 5165efe..2cd3fa8 100644
--- a/BUCK
+++ b/BUCK
@@ -3,8 +3,9 @@
 gerrit_war(name = 'gerrit')
 gerrit_war(name = 'chrome',   ui = 'ui_chrome')
 gerrit_war(name = 'firefox',  ui = 'ui_firefox')
+gerrit_war(name = 'safari',   ui = 'ui_safari')
 gerrit_war(name = 'withdocs', docs = True)
-gerrit_war(name = 'release',  docs = True, context = ['//plugins:core.zip'],  visibility = ['//tools/maven:'])
+gerrit_war(name = 'release',  docs = True, context = ['//plugins:core'],  visibility = ['//tools/maven:'])
 
 API_DEPS = [
   '//gerrit-extension-api:extension-api',
@@ -27,3 +28,13 @@
   deps = API_DEPS,
   out = 'api.zip',
 )
+
+genrule(
+  name = 'all',
+  cmd = 'echo done >$OUT',
+  deps = [
+    ':api',
+    ':release',
+  ],
+  out = '__fake.all__',
+)
diff --git a/Documentation/BUCK b/Documentation/BUCK
index 9c2aea8..f070d7e 100644
--- a/Documentation/BUCK
+++ b/Documentation/BUCK
@@ -10,33 +10,23 @@
   name = 'html',
   cmd = 'cd $TMP;' +
     'mkdir -p %s/images;' % DOC_DIR +
-    'unzip -q $SRCDIR/only_html.zip -d %s/;' % DOC_DIR +
+    'unzip -q $(location %s) -d %s/;'
+    % (':generate_html', DOC_DIR) +
     'for s in $SRCS;do ln -s $s %s;done;' % DOC_DIR +
     'mv %s/*.{jpg,png} %s/images;' % (DOC_DIR, DOC_DIR) +
-    'rm %s/only_html.zip;' % DOC_DIR +
-    'rm %s/licenses.txt;' % DOC_DIR +
-    'cp $SRCDIR/licenses.txt LICENSES.txt;' +
+    'cp $(location %s) LICENSES.txt;' % ':licenses.txt' +
     'zip -qr $OUT *',
   srcs = glob([
       'images/*.jpg',
       'images/*.png',
-    ]) + [
-      'doc.css',
-      genfile('licenses.txt'),
-      genfile('only_html.zip'),
-    ],
-  deps = [
-    ':generate_html',
-    ':licenses.txt',
-  ],
+    ]) + ['doc.css'],
   out = 'html.zip',
   visibility = ['PUBLIC'],
 )
 
 genasciidoc(
   name = 'generate_html',
-  srcs = SRCS + [genfile('licenses.txt')],
-  deps = [':licenses.txt'],
+  srcs = SRCS + [':licenses.txt'],
   attributes = documentation_attributes(git_describe()),
   backend = 'html5',
   out = 'only_html.zip',
@@ -66,18 +56,14 @@
       '--prefix "%s/" ' % DOC_DIR +
       '--in-ext ".txt" ' +
       '--out-ext ".html" ' +
-      '$SRCS',
-  srcs = SRCS + [genfile('licenses.txt')],
-  deps = [
-    ':licenses.txt',
-    '//lib/asciidoctor:doc_indexer',
-  ],
+      '$SRCS ' +
+      '$(location :licenses.txt)',
+  srcs = SRCS,
   out = 'index.jar',
 )
 
 prebuilt_jar(
   name = 'index_lib',
-  binary_jar = genfile('index.jar'),
-  deps = [':index'],
+  binary_jar = ':index',
   visibility = ['PUBLIC'],
 )
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index dd57249..909f972 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -741,9 +741,11 @@
 This category permits users to remove other users from the list of
 reviewers on a change.
 
-The change owner, project owner and site administrator can always
-remove reviewers (even without having the `Remove Reviewer` access
-right assigned).
+Change owners can always remove reviewers who have given a zero or positive
+score (even without having the `Remove Reviewer` access right assigned).
+
+Project owners and site administrators can always remove any reviewer (even
+without having the `Remove Reviewer` access right assigned).
 
 Users without this access right can only remove themselves from the
 reviewer list on a change.
@@ -862,7 +864,7 @@
 If it's desired to have the possibility to upload temporarily hidden
 changes there's a specific permission for that.  This enables someone
 to add specific reviewers for early feedback before making the change
-publically visible.  If you want to allow others than the owners to
+publicly visible.  If you want to allow others than the owners to
 publish a draft you also need to grant them `Publish Drafts`.
 
 Optional access rights to grant:
diff --git a/Documentation/asciidoc.defs b/Documentation/asciidoc.defs
index 0175431..7adf265 100644
--- a/Documentation/asciidoc.defs
+++ b/Documentation/asciidoc.defs
@@ -16,14 +16,12 @@
     name,
     out,
     srcs = [],
-    deps = [],
     attributes = [],
     backend = None,
     visibility = []):
   EXPN = '.expn'
 
   asciidoc = [
-      'cd $SRCDIR;',
       '$(exe //lib/asciidoctor:asciidoc)',
       '-z', '$OUT',
       '--tmp', '$TMP',
@@ -36,33 +34,35 @@
     asciidoc.extend(['-a', attribute])
   asciidoc.append('$SRCS')
   newsrcs = ["doc.css"]
-  newdeps = deps + ['//lib/asciidoctor:asciidoc']
-
   for src in srcs:
-    tx = []
     fn = src
-    if fn.startswith('BUCKGEN:') :
-      fn = src[8:]
-      tx = [':' + fn]
+    # We have two cases: regular source files and generated files.
+    # Generated files are passed as targets ':foo', and ':' is removed.
+    # 1. regular files: cmd = '-s foo', srcs = ['foo']
+    # 2. generated files: cmd = '-s $(location :foo)', srcs = []
+    srcs = [src]
+    passed_src = fn
+    if fn.startswith(':') :
+      fn = src[1:]
+      srcs = []
+      passed_src = '$(location :%s)' % fn
     ex = fn + EXPN
 
     genrule(
       name = ex,
       cmd = '$(exe :replace_macros) --suffix=' + EXPN +
-            ' -s $SRCDIR/%s' % fn +
+            ' -s ' + passed_src +
             ' -o $OUT',
-      srcs = [src],
-      deps = tx + [':replace_macros'],
+      srcs = srcs,
       out = ex,
     )
-    newdeps.append(':' + ex)
-    newsrcs.append(genfile(ex))
+
+    asciidoc.append('$(location :%s)' % ex)
 
   genrule(
     name = name,
     cmd = ' '.join(asciidoc),
     srcs = newsrcs,
-    deps = newdeps,
     out = out,
     visibility = visibility,
   )
diff --git a/Documentation/cmd-review.txt b/Documentation/cmd-review.txt
index 40393be..4c9962d 100644
--- a/Documentation/cmd-review.txt
+++ b/Documentation/cmd-review.txt
@@ -2,7 +2,7 @@
 ==============
 
 == NAME
-gerrit review - Verify, approve and/or submit one or more patch sets
+gerrit review - Apply reviews to one or more patch sets
 
 == SYNOPSIS
 --
diff --git a/Documentation/cmd-show-caches.txt b/Documentation/cmd-show-caches.txt
index facd133..dd79d8b 100644
--- a/Documentation/cmd-show-caches.txt
+++ b/Documentation/cmd-show-caches.txt
@@ -22,14 +22,23 @@
 	operating system, and other details about the environment
 	that Gerrit Code Review is running in.
 
+--show-threads::
+	Show detailed counts for Gerrit specific threads.
+
 --width::
 -w::
 	Width of the output table.
 
 == ACCESS
-Caller must be a member of the privileged 'Administrators' group,
-or have been granted
-link:access-control.html#capability_viewCaches[the 'View Caches' global capability].
+The caller must be a member of a group that is granted the
+link:access-control.html#capability_viewCaches[View Caches] capability
+or the link:access-control.html#capability_administrateServer[
+Administrate Server] capability.
+
+The summary information about SSH, threads, tasks, memory and JVM are
+only printed out if the caller is a member of a group that is granted
+the link:access-control.html#capability_administrateServer[Administrate
+Server] capability.
 
 == SCRIPTING
 Intended for interactive use only.
@@ -72,7 +81,9 @@
   Tasks:   10  total =    6 running +      0 ready +    4 sleeping
   Mem:  14.94g total =   3.04g used +  11.89g free +  10.00m buffers
         28.44g max
-           107 open files,        4 cpus available,      371 threads
+           107 open files
+
+  Threads: 4 CPUs available, 371 threads
 ====
 
 == SEE ALSO
diff --git a/Documentation/config-contact.txt b/Documentation/config-contact.txt
index 58df8ea..e0795be 100644
--- a/Documentation/config-contact.txt
+++ b/Documentation/config-contact.txt
@@ -142,7 +142,6 @@
 Full-Name: John Doe
 Preferred-Email: jdoe@example.com
 Identity: jd15@some-isp.com
-Identity: jdoe@example.com <https://www.google.com/accounts/o8/id?id=AIt18axxafvda821aQZaHDF1k8akbalk218sak>
 Identity: jdoe@example.com <http://jdoe.blogger.com/>
 Address:
 	123 Any Street
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 95d5283..632965f 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -145,6 +145,16 @@
 The configured <<ldap.username,ldap.username>> identity is not used to obtain
 account information.
 +
+* OAUTH
++
+OAuth is a protocol that lets external apps request authorization to private
+details in a user's account without getting their password. This is
+preferred over Basic Authentication because tokens can be limited to specific
+types of data, and can be revoked by users at any time.
++
+Site owners have to register their application before getting started. Note
+that provider specific plugins must be used with this authentication scheme.
++
 * `DEVELOPMENT_BECOME_ANY_ACCOUNT`
 +
 *DO NOT USE*.  Only for use in a development environment.
@@ -391,23 +401,6 @@
 specific config option in the `project.config` must be set:
 link:config-project-config.html[receive.requireContributorAgreement].
 
-auth.allowGoogleAccountUpgrade::
-+
-Allows Google Account users to automatically update their Gerrit
-account when/if their Google Account OpenID identity token changes.
-Identity tokens can change if the server changes hostnames, or
-for other reasons known only to Google.  The upgrade path works
-by matching users by email address if the identity is not present,
-and then changing the identity.
-+
-This setting also permits old Gerrit 1.x users to seamlessly upgrade
-from Google Accounts on Google App Engine to OpenID authentication.
-+
-Having this enabled incurs an extra database query when Google
-Account users register with the Gerrit server.
-+
-By default, unset/false.
-
 [[auth.trustContainerAuth]]auth.trustContainerAuth::
 +
 If true then it is the responsibility of the container hosting
@@ -713,14 +706,24 @@
 
 ==== [[cache_options]]Cache Options
 
-[[cache.diff_intraline.maxIdleWorkers]]cache.diff_intraline.maxIdleWorkers::
+[[cache.diff.timeout]]cache.diff.timeout::
 +
-Number of idle worker threads to maintain for the intraline difference
-computations.  There is no upper bound on how many concurrent requests
-can occur at once, if additional threads are started to handle a peak
-load, only this many will remain idle afterwards.
+Maximum number of milliseconds to wait for diff data before giving up and
+falling back on a simpler diff algorithm that will not be able to break down
+modified regions into smaller ones. This is a work around for an infinite loop
+bug in the default difference algorithm implementation.
 +
-Default is 1.5x number of available CPUs.
+Values should use common unit suffixes to express their setting:
++
+* ms, milliseconds
+* s, sec, second, seconds
+* m, min, minute, minutes
+* h, hr, hour, hours
+
++
+If a unit suffix is not specified, `milliseconds` is assumed.
++
+Default is 5 seconds.
 
 [[cache.diff_intraline.timeout]]cache.diff_intraline.timeout::
 +
@@ -796,7 +799,7 @@
 With a configured 30 second delay a server with 4900 active users will
 typically need to dedicate 1 CPU to the update check.  4900 users
 divided by an average delay of 30 seconds is 163 requests arriving per
-second.  If requests are served at ~6 ms response time, 1 CPU is
+second.  If requests are served at \~6 ms response time, 1 CPU is
 necessary to keep up with the update request traffic.  On a smaller
 user base of 500 active users, the default 30 second delay is only 17
 requests per second and requires ~10% CPU.
@@ -812,6 +815,21 @@
 +
 Default is true.
 
+[[change.submitLabel]]change.submitLabel::
++
+Label name for the submit button.
++
+Default is "Submit".
+
+[[change.submitTooltip]]change.submitTooltip::
++
+Tooltip for the submit button.  Variables available for replacement
+include `${patchSet}` for the current patch set number (1, 2, 3),
+`${branch}` for the branch name ("master") and `${commit}` for the
+abbreviated commit SHA-1 (`c9c0edb`).
++
+Default is "Submit patch set ${patchSet} into ${branch}".
+
 [[changeMerge]]
 === Section changeMerge
 
@@ -1095,17 +1113,21 @@
 Size of the buffer to store logging events for asynchronous logging.
 Putting a larger value can protect threads from stalling when the
 AsyncAppender threads are not fast enough to consume the logging events
-from the buffer. It also protects from loosing log entries in this case.
+from the buffer. It also protects from losing log entries in this case.
 +
 Default is 64 entries.
 
 [[core.useRecursiveMerge]]core.useRecursiveMerge::
 +
-Use JGit's new, experimental recursive merger for three-way merges.
-This only affects projects configured to automatically resolve
-conflicts.
+Use JGit's recursive merger for three-way merges. This only affects
+projects configured to automatically resolve conflicts.
 +
-Default is false, but in a future release may default to true.
+As explained in this
+link:http://codicesoftware.blogspot.com/2011/09/merge-recursive-strategy.html[
+blog], the recursive merge produces better results if the two commits
+that are merged have more than one common predecessor.
++
+Default is true.
 
 [[database]]
 === Section database
@@ -1333,6 +1355,84 @@
 If `download.scheme` is not specified, SSH, HTTP and Anonymous HTTP
 downloads are allowed.
 
+[[download.archive]]download.archive::
++
+Specifies which archive formats, if any, should be offered on the change
+screen:
++
+----
+[download]
+  archive = tar
+  archive = tbz2
+  archive = tgz
+  archive = txz
+----
+
+If `download.archive` is not specified defaults to all archive
+commands. Set to `off` or empty string to disable.
+
+[[gc]]
+=== Section gc
+
+This section allows to configure the git garbage collection and schedules it
+to run periodically. It will be triggered and executed sequentially for all
+projects.
+
+[[gc.startTime]]gc.startTime::
++
+Start time to define the first execution of the git garbage collection.
+If the configured `'gc.interval'` is shorter than `'gc.startTime - now'`
+the start time will be preponed by the maximum integral multiple of
+`'gc.interval'` so that the start time is still in the future.
++
+----
+<day of week> <hours>:<minutes>
+or
+<hours>:<minutes>
+
+<day of week> : Mon, Tue, Wed, Thu, Fri, Sat, Sun
+<hours>       : 00-23
+<minutes>     : 0-59
+----
+
+
+[[gc.interval]]gc.interval::
++
+Interval for periodic repetition of triggering the git garbage collection.
+The interval must be larger than zero. The following suffixes are supported
+to define the time unit for the interval:
++
+* `s, sec, second, seconds`
+* `m, min, minute, minutes`
+* `h, hr, hour, hours`
+* `d, day, days`
+* `w, week, weeks` (`1 week` is treated as `7 days`)
+* `mon, month, months` (`1 month` is treated as `30 days`)
+* `y, year, years` (`1 year` is treated as `365 days`)
+
+Examples::
++
+----
+gc.startTime = Fri 10:30
+gc.interval  = 2 day
+----
++
+Assuming the server is started on Mon 7:00 -> `'startTime - now = 4 days 3:30 hours'`.
+This is larger than the interval hence prepone the start time
+by the maximum integral multiple of the interval so that start
+time is still in the future, i.e. prepone by 4 days. This yields
+a start time of Mon 10:30, next executions are Wed 10:30, Fri 10:30
+etc.
++
+----
+gc.startTime = 6:00
+gc.interval = 1 day
+----
++
+Assuming the server is started on Mon 7:00 this yields the first run on next Tuesday
+at 6:00 and a repetition interval of 1 day.
+
+
 [[gerrit]]
 === Section gerrit
 
@@ -1354,6 +1454,13 @@
 +
 Defaults to `All-Projects` if not set.
 
+[[gerrit.allUsers]]gerrit.allUsers::
++
+Name of the project in which meta data of all users is stored.
+The name is relative to `gerrit.basePath`.
++
+Defaults to `All-Users` if not set.
+
 [[gerrit.canonicalWebUrl]]gerrit.canonicalWebUrl::
 +
 The default URL for Gerrit to be accessed through.
@@ -1890,6 +1997,12 @@
 If the file doesn't exist or can't be read the default robots.txt file
 bundled with the .war will be used instead.
 
+[[httpd.registerMBeans]]httpd.registerMBeans::
++
+Enable (or disable) registration of Jetty MBeans for Java JMX.
++
+By default, false.
+
 [[index]]
 === Section index
 
@@ -1899,9 +2012,6 @@
 using the link:pgm-reindex.html[reindex program] before restarting the
 Gerrit server.
 
-Open and closed changes are indexed in separate indexes named
-'changes_open' and 'changes_closed' respectively.
-
 [[index.type]]index.type::
 +
 Type of secondary indexing employed by Gerrit.  The supported
@@ -1925,11 +2035,12 @@
 +
 Defaults to 1 if not set, or set to a negative value.
 
-[[index.url]]index.url::
-+
-Only used when the type is `SOLR`.
-+
-URL of the index server.
+==== Lucene configuration
+
+Open and closed changes are indexed in separate indexes named
+'open' and 'closed' respectively.
+
+The following settings are only used when the index type is `LUCENE`.
 
 [[index.defaultMaxClauseCount]]index.defaultMaxClauseCount::
 +
@@ -1941,8 +2052,6 @@
 
 [[index.name.ramBufferSize]]index.name.ramBufferSize::
 +
-Only used when the type is `LUCENE`.
-+
 Determines the amount of RAM that may be used for buffering added documents
 and deletions before they are flushed to the index.  See the
 link:http://lucene.apache.org/core/4_6_0/core/org/apache/lucene/index/LiveIndexWriterConfig.html#setRAMBufferSizeMB(double)[
@@ -1952,8 +2061,6 @@
 
 [[index.name.maxBufferedDocs]]index.name.maxBufferedDocs::
 +
-Only used when the type is `LUCENE`.
-+
 Determines the minimal number of documents required before the buffered
 in-memory documents are flushed to the index. Large values generally
 give faster indexing.  See the
@@ -1965,8 +2072,6 @@
 
 [[index.name.commitWithin]]index.name.commitWithin::
 +
-Only used when the type is `LUCENE`.
-+
 Determines the period at which changes are automatically committed to
 stable store on disk. This is a costly operation and may block
 additional index writes, so lower with caution.
@@ -1981,10 +2086,10 @@
 If negative, `commitWithin` is disabled. Changes are flushed to disk when
 the in-memory buffer fills, but only committed and guaranteed to be synced
 to disk when the process finishes.
-
++
 Defaults to 300000 ms (5 minutes).
 
-Sample index configuration:
+Sample Lucene index configuration:
 ----
 [index]
   type = LUCENE
@@ -1999,6 +2104,17 @@
   maxBufferedDocs = 500
 ----
 
+==== Solr configuration
+
+Open and closed changes are indexed in separate indexes named
+'changes_open' and 'changes_closed' respectively.
+
+The following settings are only used when the index type is `SOLR`.
+
+[[index.url]]index.url::
++
+URL of the index server.
+
 [[ldap]]
 === Section ldap
 
@@ -2176,6 +2292,16 @@
 Default is unset for RFC 2307 servers (disabled)
 and `memberOf` for Active Directory.
 
+[[ldap.fetchMemberOfEagerly]]ldap.fetchMemberOfEagerly::
++
+_(Optional)_ Whether to fetch the `memberOf` account attribute on
+login. Setups which use LDAP for user authentication but don't make
+use of the LDAP groups may benefit from setting this option to `false`
+as this will result in a much faster LDAP login.
++
+Default is unset for RFC 2307 servers (disabled) and `true` for
+Active Directory.
+
 [[ldap.groupBase]]ldap.groupBase::
 +
 Root of the tree containing all group objects.  This is typically
@@ -2286,6 +2412,47 @@
 must have the DWORD value `allowtgtsessionkey` set to 1 and the account must not
 have local administrator privileges.
 
+[[ldap.useConnectionPooling]]ldap.useConnectionPooling::
++
+_(Optional)_ Enable the LDAP connection pooling or not.
++
+If it is true, the LDAP service provider maintains a pool of (possibly)
+previously used connections and assigns them to a Context instance as
+needed. When a Context instance is done with a connection (closed or
+garbage collected), the connection is returned to the pool for future use.
++
+For details, see link:http://docs.oracle.com/javase/tutorial/jndi/ldap/pool.html[
+LDAP connection management (Pool)] and link:http://docs.oracle.com/javase/tutorial/jndi/ldap/config.html[
+LDAP connection management (Configuration)]
++
+By default, false.
+
+[[ldap.connectTimeout]]ldap.connectTimeout::
++
+_(Optional)_ Timeout period for establishment of an LDAP connection.
++
+The value is in the usual time-unit format like "1 s", "100 ms",
+etc...
++
+By default there is no timeout and Gerrit will wait indefinitely.
+
+[[ldap-connection-pooling]]
+==== LDAP Connection Pooling
+Once LDAP connection pooling is enabled by setting the link:#ldap.useConnectionPooling[
+ldap.useConnectionPooling] configuration property to `true`, the connection pool
+can be configured using JVM system properties as explained in the
+link:http://docs.oracle.com/javase/7/docs/technotes/guides/jndi/jndi-ldap.html#POOL[
+Java SE Documentation].
+
+For standalone Gerrit (running with the embedded Jetty), JVM system properties
+are specified in the link:#container[container section]:
+
+----
+  javaOptions = -Dcom.sun.jndi.ldap.connect.pool.maxsize=20
+  javaOptions = -Dcom.sun.jndi.ldap.connect.pool.prefsize=10
+  javaOptions = -Dcom.sun.jndi.ldap.connect.pool.timeout=300000
+----
+
 [[mimetype]]
 === Section mimetype
 
@@ -2696,6 +2863,14 @@
 [[sshd]]
 === Section sshd
 
+[[sshd.backend]]sshd.backend::
++
+Starting from version 0.9.0 Apache SSHD project added support for NIO2
+IoSession. To use the new NIO2 session the `backend` option must be set
+to `NIO2`.
++
+By default, `MINA`.
+
 [[sshd.listenAddress]]sshd.listenAddress::
 +
 Specifies the local addresses the internal SSHD should listen
@@ -2897,6 +3072,24 @@
 +
 By default, true.
 
+[[sshd.rekeyBytesLimit]]sshd.rekeyBytesLimit::
++
+The SSH daemon will issue a rekeying after a certain amount of data.
+This configuration option allows you to tweak that setting.
++
+By default, 1073741824 (bytes, 1GB).
++
+The rekeyBytesLimit cannot be set to lower than 32.
+
+[[sshd.rekeyTimeLimit]]sshd.rekeyTimeLimit::
++
+The SSH daemon will issue a rekeying after a certain amount of time.
+This configuration option allows you to tweak that setting.
++
+By default, 1h.
++
+Set to 0 to disable this check.
+
 [[suggest]]
 === Section suggest
 
diff --git a/Documentation/config-gitweb.txt b/Documentation/config-gitweb.txt
index fb7c961..d1cee57 100644
--- a/Documentation/config-gitweb.txt
+++ b/Documentation/config-gitweb.txt
@@ -268,7 +268,7 @@
 === SEE ALSO
 
 * link:config-gerrit.html#gitweb[Section gitweb]
-* link:http://hjemli.net/git/cgit/[cgit]
+* link:http://git.zx2c4.com/cgit/about/[cgit]
 
 GERRIT
 ------
diff --git a/Documentation/config-hooks.txt b/Documentation/config-hooks.txt
index 0f4d094..8d58d36 100644
--- a/Documentation/config-hooks.txt
+++ b/Documentation/config-hooks.txt
@@ -41,15 +41,24 @@
 changes and drafts).
 
 ====
-  patchset-created --change <change id> --is-draft <boolean> --change-url <change url> --project <project name> --branch <branch> --topic <topic> --uploader <uploader> --commit <sha1> --patchset <patchset id>
+  patchset-created --change <change id> --is-draft <boolean> --kind <change kind> --change-url <change url> --change-owner <change owner> --project <project name> --branch <branch> --topic <topic> --uploader <uploader> --commit <sha1> --patchset <patchset id>
 ====
 
+kind:: change kind represents the kind of change uploaded, also represented in link:json.html#patchSet[patchSet]
+
+  REWORK;; Nontrivial content changes.
+
+  TRIVIAL_REBASE;; Conflict-free merge between the new parent and the prior patch set.
+
+  NO_CODE_CHANGE;; No code changed; same tree and same parents.
+
+
 === draft-published
 
 This is called whenever a draft change is published.
 
 ====
-  draft-published --change <change id> --change-url <change url> --project <project name> --branch <branch> --topic <topic> --uploader <uploader> --commit <sha1> --patchset <patchset id>
+  draft-published --change <change id> --change-url <change url> --change-owner <change owner> --project <project name> --branch <branch> --topic <topic> --uploader <uploader> --commit <sha1> --patchset <patchset id>
 ====
 
 === comment-added
@@ -57,7 +66,7 @@
 This is called whenever a comment is added to a change.
 
 ====
-  comment-added --change <change id> --is-draft <boolean> --change-url <change url> --project <project name> --branch <branch> --topic <topic> --author <comment author> --commit <commit> --comment <comment> [--<approval category id> <score> --<approval category id> <score> ...]
+  comment-added --change <change id> --is-draft <boolean> --change-url <change url> --change-owner <change owner> --project <project name> --branch <branch> --topic <topic> --author <comment author> --commit <commit> --comment <comment> [--<approval category id> <score> --<approval category id> <score> ...]
 ====
 
 === change-merged
@@ -65,7 +74,7 @@
 Called whenever a change has been merged.
 
 ====
-  change-merged --change <change id> --change-url <change url> --project <project name> --branch <branch> --topic <topic> --submitter <submitter> --commit <sha1>
+  change-merged --change <change id> --change-url <change url> --change-owner <change owner> --project <project name> --branch <branch> --topic <topic> --submitter <submitter> --commit <sha1>
 ====
 
 === merge-failed
@@ -73,7 +82,7 @@
 Called whenever a change has failed to merge.
 
 ====
-  merge-failed --change <change id> --change-url <change url> --project <project name> --branch <branch> --topic <topic> --submitter <submitter> --commit <sha1> --reason <reason>
+  merge-failed --change <change id> --change-url <change url> --change-owner <change owner> --project <project name> --branch <branch> --topic <topic> --submitter <submitter> --commit <sha1> --reason <reason>
 ====
 
 === change-abandoned
@@ -81,7 +90,7 @@
 Called whenever a change has been abandoned.
 
 ====
-  change-abandoned --change <change id> --change-url <change url> --project <project name> --branch <branch> --topic <topic> --abandoner <abandoner> --commit <sha1> --reason <reason>
+  change-abandoned --change <change id> --change-url <change url> --change-owner <change owner> --project <project name> --branch <branch> --topic <topic> --abandoner <abandoner> --commit <sha1> --reason <reason>
 ====
 
 === change-restored
@@ -89,7 +98,7 @@
 Called whenever a change has been restored.
 
 ====
-  change-restored --change <change id> --change-url <change url> --project <project name> --branch <branch> --topic <topic> --restorer <restorer> --commit <sha1> --reason <reason>
+  change-restored --change <change id> --change-url <change url> --change-owner <change owner> --project <project name> --branch <branch> --topic <topic> --restorer <restorer> --commit <sha1> --reason <reason>
 ====
 
 === ref-updated
@@ -105,7 +114,7 @@
 Called whenever a reviewer is added to a change.
 
 ====
-  reviewer-added --change <change id> --change-url <change url> --project <project name> --branch <branch> --reviewer <reviewer>
+  reviewer-added --change <change id> --change-url <change url> --change-owner <change owner> --project <project name> --branch <branch> --reviewer <reviewer>
 ====
 
 === topic-changed
@@ -113,7 +122,7 @@
 Called whenever a change's topic is changed from the Web UI or via the REST API.
 
 ====
-  topic-changed --change <change id> --project <project name> --branch <branch> --changer <changer> --old-topic <old topic> --new-topic <new topic>
+  topic-changed --change <change id> --change-owner <change owner> --project <project name> --branch <branch> --changer <changer> --old-topic <old topic> --new-topic <new topic>
 ====
 
 === cla-signed
diff --git a/Documentation/config-labels.txt b/Documentation/config-labels.txt
index fc25f22..aaeb834 100644
--- a/Documentation/config-labels.txt
+++ b/Documentation/config-labels.txt
@@ -169,12 +169,18 @@
 optional leading `+`.
 
 
-[[label_abbreviation]]
-=== `label.Label-Name.abbreviation`
+[[label_defaultValue]]
+=== `label.Label-Name.defaultValue`
 
-An abbreviated name for a label shown as a compact column header, for
-example on project dashboards. Defaults to all the uppercase characters
-in the label name, e.g. `Label-Name` is abbreviated by default as `LN`.
+The default value (or score) for the label.  The defaultValue must be
+within the range of valid label values.  It is an optional label setting,
+if not defined the defaultValue for the label will be 0.  When a
+defaultValue is defined, that value will get set in the Reply dialog
+by default.
+
+A defaultValue can be set to a score that is outside of the permissible
+range for a user.  In that case the score that will get set in the Reply
+box will be either the lowest or highest score in the permissible range.
 
 
 [[label_function]]
@@ -297,6 +303,32 @@
 copyright` will block submit, while `+1 Copyright clear` is required to
 enable submit.
 
+=== Default Value Example
+
+This example attempts to describe how a label default value works with the
+user permissions.  Assume the configuration below.
+
+====
+  [access "refs/heads/*"]
+      label-Snarky-Review = -3..+3 group Administrators
+      label-Snarky-Review = -2..+2 group Project Owners
+      label-Snarky-Review = -1..+1 group Registered Users
+  [label "Snarky-Review"]
+      value = -3 Ohh, hell no!
+      value = -2 Hmm, I'm not a fan
+      value = -1 I'm not sure I like this
+      value =  0 No score
+      value = +1 I like, but need another to like it as well
+      value = +2 Hmm, this is pretty nice
+      value = +3 Ohh, hell yes!
+      defaultValue = -3
+====
+
+Upon clicking the Reply button:
+* Administrators have all scores (-3..+3) available, -3 is set as the default.
+* Project Owners have limited scores (-2..+2) available, -2 is set as the default.
+* Registered Users have limited scores (-1..+1) available, -1 is set as the default.
+
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/config-project-config.txt b/Documentation/config-project-config.txt
index e66343c..43ede06 100644
--- a/Documentation/config-project-config.txt
+++ b/Documentation/config-project-config.txt
@@ -171,6 +171,21 @@
 documentation for a full list of available access rights.
 
 
+[[mimetype-section]]
+=== MIME Types section
+
+The +mimetype+ section may be configured to force the web code
+reviewer to return certain MIME types by file path. MIME types
+may be used to activate syntax highlighting.
+
+----
+[mimetype "text/x-c"]
+  path = *.pkt
+[mimetype "text/x-java"]
+  path = api/current.txt
+----
+
+
 [[capability-section]]
 === Capability section
 
@@ -183,6 +198,41 @@
 documentation for a full list of available capabilities.
 
 
+[[branchOrder-section]]
+=== branchOrder section
+
+Defines a branch ordering which is used for backporting of changes.
+Backporting will be offered for a change (in the Gerrit UI) for all
+more stable branches where the change can merge cleanly.
+
+[[branchOrder.branch]]branchOrder.branch::
++
+A branch name, typically multiple values will be defined. The order of branch
+names in this section defines the branch order. The topmost is considered to be
+the least stable branch (typically the master branch) and the last one the
+most stable (typically the last maintained release branch).
+
+Example:
+
+----
+[branchOrder]
+  branch = master
+  branch = stable-2.9
+  branch = stable-2.8
+  branch = stable-2.7
+----
+
+The `branchOrder` section is inheritable. This is useful when multiple or all
+projects follow the same branch rules. A `branchOrder` section in a child
+project completely overrides any `branchOrder` section from a parent i.e. there
+is no merging of `branchOrder` sections. A present but empty `branchOrder`
+section removes all inherited branch order.
+
+Branches not listed in this section will not be included in the mergeability
+check. If the `branchOrder` section is not defined then the mergeability of a
+change into other branches will not be done.
+
+
 [[file-groups]]
 == The file +groups+
 
diff --git a/Documentation/config-sso.txt b/Documentation/config-sso.txt
index 8c82091..e4cf20e 100644
--- a/Documentation/config-sso.txt
+++ b/Documentation/config-sso.txt
@@ -43,9 +43,9 @@
 * `http://` -- trust all OpenID providers using the HTTP protocol
 * `https://` -- trust all OpenID providers using the HTTPS protocol
 
-To trust only Google Accounts:
+To trust only Yahoo!:
 ====
-  git config --file $site_path/etc/gerrit.config auth.trustedOpenID 'https://www.google.com/accounts/o8/id?id='
+  git config --file $site_path/etc/gerrit.config auth.trustedOpenID https://me.yahoo.com
 ====
 
 === Database Schema
diff --git a/Documentation/config-validation.txt b/Documentation/config-validation.txt
index b7843a7..5d23c79 100644
--- a/Documentation/config-validation.txt
+++ b/Documentation/config-validation.txt
@@ -32,6 +32,19 @@
 If the commit fails the validation, the plugin can throw an exception
 which will cause the merge to fail.
 
+[[pre-upload-validation]]
+== Pre-upload validation
+
+
+Plugins implementing the `UploadValidationListener` interface can
+perform additional validation checks before any upload operations
+(clone, fetch, pull). The validation is executed right before Gerrit
+begins to send a pack back to the git client.
+
+If upload fails the validation, the plugin can throw an exception
+which will cause the upload to fail and the exception's message text
+will be reported to the git client.
+
 [[new-project-validation]]
 == New project validation
 
diff --git a/Documentation/database-setup.txt b/Documentation/database-setup.txt
index bbaa748..4f854fb 100644
--- a/Documentation/database-setup.txt
+++ b/Documentation/database-setup.txt
@@ -103,6 +103,49 @@
         password = secret_pasword
 ----
 
+[[createdb_maxdb]]
+=== SAP MaxDB
+
+SAP MaxDB is a supported database for running Gerrit Code Review. However it is
+recommended only for environments where you intend to run Gerrit on an existing
+MaxDB installation to reduce administrative overhead.
+
+In the MaxDB studio or using the SQLCLI command line interface create a user
+'gerrit2' with the user class 'RESOURCE' and a password <secret password>. This
+will also create an associated schema on the database.
+
+To run Gerrit on MaxDB, you need to obtain the MaxDB JDBC driver. It can be
+found in your MaxDB installation at the following location:
+
+- on Windows 64bit at "C:\Program Files\sdb\MaxDB\runtime\jar\sapdbc.jar"
+- on Linux at "/opt/sdb/MaxDB/runtime/jar/sapdbc.jar"
+
+It needs to be stored in the 'lib' folder of the review site.
+
+In the following sample database section it is assumed that the database name is
+'reviewdb' and the database is installed on localhost:
+
+In $site_path/etc/gerrit.config:
+
+----
+[database]
+        type = maxdb
+        database = reviewdb
+        hostname = localhost
+        username = gerrit2
+
+----
+
+In $site_path/etc/secure.config:
+
+----
+[database]
+        password = <secret password>
+----
+
+Visit SAP MaxDB's link:http://maxdb.sap.com/documentation/[documentation] for further
+information regarding using SAP MaxDB.
+
 
 GERRIT
 ------
diff --git a/Documentation/dev-buck.txt b/Documentation/dev-buck.txt
index 98cd513..ce40a01 100644
--- a/Documentation/dev-buck.txt
+++ b/Documentation/dev-buck.txt
@@ -5,7 +5,7 @@
 
 There is currently no binary distribution of Buck, so it has to be manually
 built and installed.  Apache Ant is required.  Currently only Linux and Mac
-OS are supported.  Gerrit's buck wrappers require Python version 2.6 or higher.
+OS are supported.  Buck requires Python version 2.7 to be installed.
 
 Clone the git and build it:
 
@@ -42,13 +42,6 @@
   which buck
 ----
 
-If you plan to use the link:#buck-daemon[Buck daemon] add a symbolic
-link in `~/bin` to the buckd executable:
-
-----
-  ln -s `pwd`/bin/buckd ~/bin/
-----
-
 To enable autocompletion of buck commands, install the autocompletion
 script from `./scripts/bash_completion` in the buck project.  Refer to
 the script's header comments for installation instructions.
@@ -249,13 +242,22 @@
   buck-out/gen/release.war
 ----
 
+[[all]]
+=== Combined build target
+
+To build release and api targets, a combined build target is provided:
+
+----
+  buck build all
+----
+
 [[tests]]
 == Running Unit Tests
 
 To run all tests including acceptance tests:
 
 ----
-  buck test --all
+  buck test
 ----
 
 To exclude slow tests:
@@ -264,6 +266,21 @@
   buck test --all --exclude slow
 ----
 
+To include a specific group of acceptance tests:
+
+----
+  buck test --all --include api
+----
+
+The following groups of tests are currently supported:
+
+* api
+* git
+* pgm
+* rest
+* server
+* ssh
+
 To run a specific test, e.g. the acceptance test
 `com.google.gerrit.acceptance.git.HttpPushForReviewIT`:
 
@@ -436,17 +453,52 @@
 directories and they will not interfere with each other. Buck's documentation
 covers daemon in http://facebook.github.io/buck/command/buckd.html[buckd].
 
-The trivial use case is to run `buckd` from the project's root directory and
-run `buck` as usual:
+To use `buckd` the additional
+link:https://facebook.github.io/watchman[watchman] program must be installed.
+
+[[watchman]]
+=== Installing watchman
+
+Watchman is used internally by Buck to monitor directory trees and is needed
+for buck daemon to work properly. Because buckd is activated by default in the
+latest version of Buck, it searches for the watchman executable in the
+path and issues a warning when it is not found and kills buckd.
+
+To prepare watchman installation on Linux:
 
 ----
-  buckd
-  buck build gerrit
-  Using buckd.
-  [-] PARSING BUILD FILES...FINISHED 0.6s
-  [-] BUILDING...FINISHED 0.2s
+  git clone https://github.com/facebook/watchman.git
+  cd watchman
+  ./autogen.sh
 ----
 
+To install it in user home directory (without root privileges):
+
+----
+  ./configure --prefix $HOME/watchman
+  make install
+----
+
+To install it system wide:
+
+----
+  ./configure
+  make
+  sudo make install
+----
+
+Put $HOME/watchman/bin/watchman in path or link to $HOME/bin/watchman.
+
+To install watchman on OS X:
+
+----
+  brew install --HEAD watchman
+----
+
+See the original documentation for more information:
+link:https://facebook.github.io/watchman/docs/install.html[Watchman
+installation].
+
 === Override Buck's settings
 
 Additional JVM args for Buck can be set in `.buckjavaargs` in the
@@ -461,15 +513,14 @@
 
 == Rerun unit tests
 
-If for some reasons tests, that were already run must be repeated, unit test
-cache must be removed fist. That's because the test execution results are
-cached by Buck:
+Test execution results are cached by Buck. If a test that was already run
+needs to be repeated, the unit test cache for that test must be removed first:
 
 ----
   $ rm -rf buck-out/bin/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/.AddRemoveGroupMembersIT/
 ----
 
-After clearing the cache test can be rerun again:
+After clearing the cache, the test can be run again:
 
 ----
   $ buck test //gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group:AddRemoveGroupMembersIT
@@ -478,25 +529,33 @@
   TESTS PASSED
 ----
 
-An alternative approach is to use a Buck feature:
---test-selectors (-filters, -f) option:
+An alternative approach is to use Buck's `--filters` (`-f`) option:
 
 ----
-  buck test --all -f 'com.google.gerrit.acceptance.rest.change.SubmitByMergeAlwaysIT'
+  buck test -f 'com.google.gerrit.acceptance.rest.change.SubmitByMergeAlwaysIT'
   TESTING SELECTED TESTS
   PASS  14,5s  6 Passed   0 Failed   com.google.gerrit.acceptance.rest.change.SubmitByMergeAlwaysIT
   TESTS PASSED
 ----
 
-When this option is used, cache is disabled per design and doesn't need to be deleted.
-Note: when -f option is used, the whole unit test cache is dropped. As a consequence,
+When this option is used, the cache is disabled per design and doesn't need to
+be explicitly deleted.
+
+Note that when this option is used, the whole unit test cache is dropped, so
 repeating the
 
 ----
-buck test --all
+buck test
 ----
 
-would re-execute all tests again.
+causes all tests to be executed again.
+
+To run tests without using cached results at all, use the `--no-results-cache`
+option:
+
+----
+buck test --no-results-cache
+----
 
 GERRIT
 ------
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt
index 5e307f9..95a5554 100644
--- a/Documentation/dev-contributing.txt
+++ b/Documentation/dev-contributing.txt
@@ -171,11 +171,13 @@
   * Define any static interfaces next in your class.
   * Define non static interfaces after static interfaces in your
     class.
-  * Next you should define static types and members.
+  * Next you should define static types, members, and methods, in
+    decreasing order of visibility (public to private).
   * Finally instance members, then constructors, and then instance
     methods.
-  * Some common exceptions are private helper static methods which
-    might appear near the instance methods which they help.
+  * Some common exceptions are private helper static methods, which
+    might appear near the instance methods which they help (but may
+    also appear at the top).
   * Getters and setters for the same instance field should usually
     be near each other baring a good reason not to.
   * If you are using assisted injection, the factory for your class
diff --git a/Documentation/dev-inspector.txt b/Documentation/dev-inspector.txt
index 2d56283..7c13a7d 100644
--- a/Documentation/dev-inspector.txt
+++ b/Documentation/dev-inspector.txt
@@ -240,7 +240,7 @@
 
 ----
 [2012-04-17 14:20:30,558] INFO  com.google.gerrit.pgm.shell.JythonShell : Jython shell instance created.
-[2012-04-17 14:20:38,005] ERROR com.google.gerrit.pgm.shell.JythonShell : Exception occured while loading file Startup.py :
+[2012-04-17 14:20:38,005] ERROR com.google.gerrit.pgm.shell.JythonShell : Exception occurred while loading file Startup.py :
 java.lang.reflect.InvocationTargetException
         at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
         at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 76c3da9..1030f63 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -36,7 +36,7 @@
 ----
 mvn archetype:generate -DarchetypeGroupId=com.google.gerrit \
     -DarchetypeArtifactId=gerrit-plugin-archetype \
-    -DarchetypeVersion=2.9 \
+    -DarchetypeVersion=2.10.7 \
     -DgroupId=com.googlesource.gerrit.plugins.testplugin \
     -DartifactId=testplugin
 ----
@@ -400,6 +400,10 @@
 +
 Update of HEAD on a project
 
+* `com.google.gerrit.extensions.events.UsageDataPublishedListener`:
++
+Publication of usage data
+
 [[stream-events]]
 == Sending Events to the Events Stream
 
@@ -432,6 +436,14 @@
 for those plugins which would like to monitor changes in Git
 repositories.
 
+[[pre-upload-hook]]
+== Pre Upload-Pack Hooks
+
+Plugins may register PreUploadHook instances in order to get
+notified when JGit is about to upload a pack. This may be useful
+for those plugins which would like to monitor usage in Git
+repositories.
+
 [[ssh]]
 == SSH Commands
 
@@ -1707,6 +1719,42 @@
 The download schemes and download commands which are used most often
 are provided by the Gerrit core plugin `download-commands`.
 
+[[links-to-external-tools]]
+== Links To External Tools
+
+Gerrit has extension points that enables development of a
+light-weight plugin that links commits to external
+tools (GitBlit, CGit, company specific resources etc).
+
+PatchSetWebLinks will appear to the right of the commit-SHA1 in the UI.
+
+[source, java]
+----
+import com.google.gerrit.extensions.annotations.Listen;
+import com.google.gerrit.extensions.webui.PatchSetWebLink;;
+
+@Listen
+public class MyWeblinkPlugin implements PatchSetWebLink {
+
+  private String name = "MyLink";
+  private String placeHolderUrlProjectCommit = "http://my.tool.com/project=%s/commit=%s";
+
+  @Override
+  public String getLinkName() {
+    return name ;
+  }
+
+  @Override
+  public String getPatchSetUrl(String project, String commit) {
+    return String.format(placeHolderUrlProjectCommit, project, commit);
+  }
+
+}
+----
+
+ProjectWebLinks will appear in the project list in the
+`Repository Browser` column.
+
 [[documentation]]
 == Documentation
 
diff --git a/Documentation/dev-readme.txt b/Documentation/dev-readme.txt
index 2b0cda8..da1ca70 100644
--- a/Documentation/dev-readme.txt
+++ b/Documentation/dev-readme.txt
@@ -50,15 +50,24 @@
 refer to: link:dev-buck.html#eclipse[Eclipse integration with Buck].
 
 
+== Configuring IntelliJ IDEA
+
+To use IntelliJ IDEA for development, the easiest way is to follow
+Eclipse integration and then open it as Eclipse project in IDEA.
+You need the Eclipse plugin activated in IntelliJ IDEA.
+
+
 == Mac OS X
 
-On Mac OS X ensure "Java For Mac OS X 10.5 Upate 4" (or later) has
-been installed, and that `JAVA_HOME` is set to
-"/System/Library/Frameworks/JavaVM.framework/Versions/1.6/Home".
-Check the installed version by running `java -version` and looking
-for 'build 1.6.0_13-b03-211'.  Versions of Java 6 prior to this
-version crash during the build due to a bug in the JIT compiler.
+On Mac OS X ensure "Java For Mac OS X 10.5 Update 4" (or later) has
+been installed, and that `JAVA_HOME` is set to the
+link:install.html#Requirements[required Java version].
 
+Java installations can typically be found in
+"/System/Library/Frameworks/JavaVM.framework/Versions".
+
+You can check the installed Java version by running `java -version` in
+the terminal.
 
 [[init]]
 == Site Initialization
diff --git a/Documentation/dev-release.txt b/Documentation/dev-release.txt
index e966899..6cf9edf 100644
--- a/Documentation/dev-release.txt
+++ b/Documentation/dev-release.txt
@@ -226,8 +226,8 @@
 ** Go to the link:https://oss.sonatype.org/[Sonatype Nexus Server] and
 sign in with your Sonatype credentials.
 
-** Click in the left navigation bar under `Build Promotion` on
-`Staging Repositories` and find the `comgooglegerrit-XXXX` staging
+** Click on 'Build Promotion' in the left navigation bar under
+'Staging Repositories' and find the `comgooglegerrit-XXXX` staging
 repository.
 
 ** Verify its content
@@ -282,6 +282,17 @@
 +
 http://central.maven.org/maven2/com/google/gerrit/
 
+* [optional]: View download statistics
+
+** Sign in to the
+link:https://oss.sonatype.org/[Sonatype Nexus Server].
+
+** Click on 'Views/Repositories' in the left navigation bar under
+'Central Statistics'.
+
+** Select `com.google.gerrit` as `Project`.
+
+
 [[push-stable]]
 ==== Push the Stable Branch
 
diff --git a/Documentation/dev-rest-api.txt b/Documentation/dev-rest-api.txt
index d07da62..ec4b666 100644
--- a/Documentation/dev-rest-api.txt
+++ b/Documentation/dev-rest-api.txt
@@ -44,6 +44,11 @@
   curl -X PUT --data-binary @testdata.txt --header "Content-Type: text/plain" http://localhost:8080/path/to/api/
 ----
 
+Example to set a Gerrit project's link:rest-api-projects.html#set-project-description[description]:
+
+----
+ curl -X PUT --digest --user john:2LlAB3K9B0PF --data-binary @project-desc.txt --header "Content-Type: application/json;charset=UTF-8" http://localhost:8080/a/projects/myproject/description
+----
 
 === Authentication
 
@@ -65,7 +70,6 @@
 
 In both cases, the password should be the user's link:user-upload.html#http[HTTP password].
 
-
 === Verifying Header Content
 
 To verify the headers returned from a REST API call, use `curl` in verbose mode:
diff --git a/Documentation/gen_licenses.py b/Documentation/gen_licenses.py
index fb03526..61d2e24 100755
--- a/Documentation/gen_licenses.py
+++ b/Documentation/gen_licenses.py
@@ -18,15 +18,23 @@
 from __future__ import print_function
 
 from collections import defaultdict, deque
+from os import chdir, path
 import re
 from shutil import copyfileobj
 from subprocess import Popen, PIPE
-from sys import stdout
+from sys import stdout, stderr
 
 MAIN = ['//gerrit-pgm:pgm', '//gerrit-gwtui:ui_module']
+KNOWN_PROVIDED_DEPS = [
+  '//lib/bouncycastle:bcpg',
+  '//lib/bouncycastle:bcpkix',
+  '//lib/bouncycastle:bcprov',
+]
 
 def parse_graph():
   graph = defaultdict(list)
+  while not path.isfile('.buckconfig'):
+    chdir('..')
   p = Popen(
     ['buck', 'audit', 'classpath', '--dot'] + MAIN,
     stdout = PIPE)
@@ -35,7 +43,14 @@
     if not m:
       continue
     target, dep = m.group(1), m.group(2)
-    if not target.endswith('__compile'):
+    # Dependencies included in provided_deps set are contained in audit
+    # classpath and must be sorted out. That's safe thing to do because
+    # they are not included in the final artifact.
+    if "DO_NOT_DISTRIBUTE" in dep:
+      if not target in KNOWN_PROVIDED_DEPS:
+        print('DO_NOT_DISTRIBUTE license for target: %s' % target, file=stderr)
+        exit(1)
+    else:
       graph[target].append(dep)
   r = p.wait()
   if r != 0:
diff --git a/Documentation/images/link.png b/Documentation/images/link.png
new file mode 100644
index 0000000..621443e
--- /dev/null
+++ b/Documentation/images/link.png
Binary files differ
diff --git a/Documentation/images/user-review-ui-side-by-side-diff-screen-preferences-popup.png b/Documentation/images/user-review-ui-side-by-side-diff-screen-preferences-popup.png
index 06e1f1f..35e29a3 100644
--- a/Documentation/images/user-review-ui-side-by-side-diff-screen-preferences-popup.png
+++ b/Documentation/images/user-review-ui-side-by-side-diff-screen-preferences-popup.png
Binary files differ
diff --git a/Documentation/install-j2ee.txt b/Documentation/install-j2ee.txt
index 50eccc2..f7252e0 100644
--- a/Documentation/install-j2ee.txt
+++ b/Documentation/install-j2ee.txt
@@ -109,6 +109,24 @@
 `system_config`) is as simple as touching the context config file:
 `'$JETTY_HOME'/contexts/gerrit.xml`
 
+[[tomcat]]
+== Tomcat 7.x
+
+If a reverse proxy is used in front of Tomcat then see the
+link:config-reverseproxy.html[configuration instructions for encoding
+slashes]. Otherwise Tomcat must be configured to encode slashes, by adding
+`-Dorg.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH=true` to the
+`CATALINA_OPTS` environment variable.
+
+Excerpt from the
+link:https://tomcat.apache.org/tomcat-7.0-doc/config/systemprops.html[
+documentation]:
+
+----
+Property org.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH:
+If this is true '%2F' and '%5C' will be permitted as path delimiters.
+If not specified, the default value of false will be used.
+----
 
 GERRIT
 ------
diff --git a/Documentation/intro-project-owner.txt b/Documentation/intro-project-owner.txt
index 78b819e..c55a43c 100644
--- a/Documentation/intro-project-owner.txt
+++ b/Documentation/intro-project-owner.txt
@@ -395,6 +395,30 @@
 link:access-control.html#capability_streamEvents[Stream Events] global
 capability assigned.
 
+[[commit-validation]]
+== Commit Validation
+
+Gerrit provides an
+link:https://gerrit-review.googlesource.com/Documentation/config-validation.html#new-commit-validation[
+extension point to do validation of new commits]. A Gerrit plugin
+implementing this extension point can perform validation checks when
+new commits are pushed to Gerrit. The plugin can either provide a
+message to the client or reject the commit and cause the push to fail.
+
+There are some plugins available that provide commit validation:
+
+- link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/uploadvalidator[
+  uploadvalidator]:
++
+The `uploadvalidator` plugin allows project owners to configure blocked
+file extensions, required footers and a maximum allowed path length.
+
+- link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/commit-message-length-validator[
+  commit-message-length-validator]
++
+The `commit-message-length-validator` core plugin validates that commit
+messages conform to line length limits.
+
 [[branch-administration]]
 == Branch Administration
 
diff --git a/Documentation/js-api.txt b/Documentation/js-api.txt
index 396edf6..883198a 100644
--- a/Documentation/js-api.txt
+++ b/Documentation/js-api.txt
@@ -168,8 +168,9 @@
 self.onAction(type, view_name, callback);
 ----
 
-* type: `'change'`, `'revision'` or `'project'`, indicating which type
-  of resource the `UiAction` was bound to in the server.
+* type: `'change'`, `'revision'`, `'project'`, or `'branch'`
+  indicating which type of resource the `UiAction` was bound to
+  in the server.
 
 * view_name: string appearing in URLs to name the view. This is the
   second argument of the `get()`, `post()`, `put()`, and `delete()`
@@ -837,8 +838,8 @@
 Gerrit.onAction(type, view_name, callback);
 ----
 
-* type: `'change'` or `'revision'`, indicating what sort of resource
-  the `UiAction` was bound to in the server.
+* type: `'change'`, `'revision'`, `'project'` or `'branch'` indicating
+  what sort of resource the `UiAction` was bound to in the server.
 
 * view_name: string appearing in URLs to name the view. This is the
   second argument of the `get()`, `post()`, `put()`, and `delete()`
diff --git a/Documentation/json.txt b/Documentation/json.txt
index 4034050..b45f404 100644
--- a/Documentation/json.txt
+++ b/Documentation/json.txt
@@ -115,6 +115,14 @@
 
 isDraft:: Whether or not the patch set is a draft patch set.
 
+kind:: Kind of change uploaded.
+
+  REWORK;; Nontrivial content changes.
+
+  TRIVIAL_REBASE;; Conflict-free merge between the new parent and the prior patch set.
+
+  NO_CODE_CHANGE;; No code changed; same tree and same parents.
+
 approvals:: The <<approval,approval attribute>> granted.
 
 comments:: All comments for this patchset in <<patchsetcomment,patchsetComment attributes>>.
diff --git a/Documentation/prolog-cookbook.txt b/Documentation/prolog-cookbook.txt
index 5bff6bf..2fedb9e 100644
--- a/Documentation/prolog-cookbook.txt
+++ b/Documentation/prolog-cookbook.txt
@@ -139,15 +139,14 @@
 
 * `ok(user(ID))` or just `ok(_)` if user info is not important. This status is
    used to tell that this label/category has been met.
-* `need(_)` is used to tell that this label/category is needed for change to
-   become submittable
-* `reject(user(ID))` or just `reject(_)`. This status is used to tell that label/category
-   is blocking change submission
-* `impossible(_)` is used when the logic knows that the change cannot be submitted as-is.
-   Administrative intervention is probably required. This is meant for cases
-   where the logic requires members of "FooEng" to score "Code-Review +2" on a
-   change, but nobody is in group "FooEng". It is to hint at permissions
-   misconfigurations.
+* `need(_)` is used to tell that this label/category is needed for the change to
+   become submittable.
+* `reject(user(ID))` or just `reject(_)`. This status is used to tell that this
+   label/category is blocking submission of the change.
+* `impossible(_)` is used when the logic knows that the change cannot be submitted
+   as-is. This is meant for cases where the logic requires members of a specific
+   group to apply a specific label on a change, but no users are in that group.
+   This is usually caused by misconfiguration of permissions.
 * `may(_)` allows expression of approval categories that are optional, i.e.
    could either be set or unset without ever influencing whether the change
    could be submitted.
diff --git a/Documentation/replace_macros.py b/Documentation/replace_macros.py
index 7623382..5cdfe91 100755
--- a/Documentation/replace_macros.py
+++ b/Documentation/replace_macros.py
@@ -80,6 +80,115 @@
 
 """
 
+LINK_SCRIPT = """
+
+++++
+<script type="text/javascript">
+    decorate(document.getElementsByTagName('h1'));
+    decorate(document.getElementsByTagName('h2'));
+    decorate(document.getElementsByTagName('h3'));
+    decorate(document.getElementsByTagName('h4'));
+
+    var divs = document.getElementsByTagName('div');
+    var arr = new Array();
+    var excluded = getExcludedIds();
+    for(var i = 0; i < divs.length; i++) {
+      var d = divs[i];
+      var id = d.getAttribute('id');
+      if (id != null && !(id in excluded)) {
+        arr[arr.length] = d;
+      }
+    }
+    decorate(arr);
+
+    var anchors = document.getElementsByTagName('a');
+    arr = new Array();
+    for(var i = 0; i < anchors.length; i++) {
+      var a = anchors[i];
+      // if the anchor has no id there is no target to
+      // which we can link
+      if (a.getAttribute('id') != null) {
+        // if the anchor is empty there is no content which
+        // can receive the mouseover event, an empty anchor
+        // applies to the element that follows, move the
+        // element that follows into the anchor so that there
+        // is content which can receive the mouseover event
+        if (a.firstChild == null) {
+          var next = a.nextSibling;
+          if (next != null) {
+            next.parentNode.removeChild(next);
+            a.appendChild(next);
+          }
+        }
+        arr[arr.length] = a;
+      }
+    }
+    decorate(arr);
+
+    function decorate(e) {
+      for(var i = 0; i < e.length; i++) {
+        e[i].onmouseover = function (evt) {
+          var element = this;
+          // do nothing if the link icon is currently showing
+          var a = element.firstChild;
+          if (a != null && a instanceof Element
+              && a.getAttribute('id') == 'LINK') {
+            return;
+          }
+
+          // if there is no id there is no target to link to
+          var id = element.getAttribute('id');
+          if (id == null) {
+            return;
+          }
+
+          // create and show a link icon that links to this element
+          a = document.createElement('a');
+          a.setAttribute('id', 'LINK');
+          a.setAttribute('href', '#' + id);
+          a.setAttribute('style', 'position: absolute;'
+              + ' left: ' + (element.offsetLeft - 16 - 2 * 4) + 'px;'
+              + ' padding-left: 4px; padding-right: 4px; padding-top:4px;');
+          var img = document.createElement('img');
+          img.setAttribute('src', 'images/link.png');
+          img.setAttribute('style', 'background-color: #FFFFFF;');
+          a.appendChild(img);
+          element.insertBefore(a, element.firstChild);
+
+          // remove the link icon when the mouse is moved away,
+          // but keep it shown if the mouse is over the element, the link or the icon
+          hide = function(evt) {
+            if (document.elementFromPoint(evt.clientX, evt.clientY) != element
+                && document.elementFromPoint(evt.clientX, evt.clientY) != a
+                && document.elementFromPoint(evt.clientX, evt.clientY) != img
+                && element.contains(a)) {
+              element.removeChild(a);
+            }
+          }
+          element.onmouseout = hide;
+          a.onmouseout = hide;
+          img.onmouseout = hide;
+        }
+      }
+    }
+
+    function getExcludedIds() {
+      var excluded = {};
+      excluded['header'] = true;
+      excluded['toc'] = true;
+      excluded['toctitle'] = true;
+      excluded['content'] = true;
+      excluded['preamble'] = true;
+      excluded['footer'] = true;
+      excluded['footer-text'] = true;
+      return excluded;
+    }
+</script>
+
+++++
+
+"""
+
 opts = OptionParser()
 opts.add_option('-o', '--out', help='output file')
 opts.add_option('-s', '--src', help='source file')
@@ -127,6 +236,7 @@
       out_file.write(last_line)
       last_line = line
   out_file.write(last_line)
+  out_file.write(LINK_SCRIPT)
   out_file.close()
 except IOError as err:
   sys.stderr.write(
diff --git a/Documentation/rest-api-access.txt b/Documentation/rest-api-access.txt
index 3ff3595..42214fe 100644
--- a/Documentation/rest-api-access.txt
+++ b/Documentation/rest-api-access.txt
@@ -268,7 +268,6 @@
     "MyProject": {
       "revision": "61157ed63e14d261b6dca40650472a9b0bd88474",
       "inherits_from": {
-        "kind": "gerritcodereview#project",
         "id": "All-Projects",
         "name": "All-Projects",
         "description": "Access inherited by all other projects."
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index 2276e40..201b020 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -753,7 +753,6 @@
   )]}'
   [
     {
-      "kind": "gerritcodereview#group",
       "id": "global%3AAnonymous-Users",
       "url": "#/admin/groups/uuid-global%3AAnonymous-Users",
       "options": {
@@ -763,7 +762,6 @@
       "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389"
     },
     {
-      "kind": "gerritcodereview#group",
       "id": "834ec36dd5e0ed21a2ff5d7e2255da082d63bbd7",
       "url": "#/admin/groups/uuid-834ec36dd5e0ed21a2ff5d7e2255da082d63bbd7",
       "options": {
@@ -773,7 +771,6 @@
       "owner_id": "834ec36dd5e0ed21a2ff5d7e2255da082d63bbd7"
     },
     {
-      "kind": "gerritcodereview#group",
       "id": "global%3ARegistered-Users",
       "url": "#/admin/groups/uuid-global%3ARegistered-Users",
       "options": {
@@ -836,6 +833,172 @@
   https://profiles/pictures/john.doe
 ----
 
+[[get-user-preferences]]
+=== Get User Preferences
+--
+'GET /accounts/link:#account-id[\{account-id\}]/preferences'
+--
+
+Retrieves the user's preferences.
+
+.Request
+----
+  GET /a/accounts/self/preferences HTTP/1.0
+----
+
+As result the account preferences of the user are returned as a
+link:#preferences-info[PreferencesInfo] entity.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "changes_per_page": 25,
+    "show_site_header": true,
+    "use_flash_clipboard": true,
+    "date_format": "STD",
+    "time_format": "HHMM_12",
+    "size_bar_in_change_table": true,
+    "review_category_strategy": "ABBREV",
+    "comment_visibility_strategy": "EXPAND_RECENT",
+    "diff_view": "SIDE_BY_SIDE",
+    "my": [
+      {
+        "url": "#/dashboard/self",
+        "name": "Changes"
+      },
+      {
+        "url": "#/q/is:draft",
+        "name": "Drafts"
+      },
+      {
+        "url": "#/q/has:draft",
+        "name": "Draft Comments"
+      },
+      {
+        "url": "#/q/is:watched+is:open",
+        "name": "Watched Changes"
+      },
+      {
+        "url": "#/q/is:starred",
+        "name": "Starred Changes"
+      },
+      {
+        "url": "#/groups/self",
+        "name": "Groups"
+      }
+    ]
+  }
+----
+
+[[set-user-preferences]]
+=== Set User Preferences
+--
+'PUT /accounts/link:#account-id[\{account-id\}]/preferences'
+--
+
+Sets the user's preferences.
+
+The new preferences must be provided in the request body as a
+link:#preferences-input[PreferencesInput] entity.
+
+.Request
+----
+  PUT /a/accounts/self/preferences HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "changes_per_page": 50,
+    "show_site_header": true,
+    "use_flash_clipboard": true,
+    "date_format": "STD",
+    "time_format": "HHMM_12",
+    "size_bar_in_change_table": true,
+    "review_category_strategy": "NAME",
+    "comment_visibility_strategy": "EXPAND_RECENT",
+    "diff_view": "SIDE_BY_SIDE",
+    "my": [
+      {
+        "url": "#/dashboard/self",
+        "name": "Changes"
+      },
+      {
+        "url": "#/q/is:draft",
+        "name": "Drafts"
+      },
+      {
+        "url": "#/q/has:draft",
+        "name": "Draft Comments"
+      },
+      {
+        "url": "#/q/is:watched+is:open",
+        "name": "Watched Changes"
+      },
+      {
+        "url": "#/q/is:starred",
+        "name": "Starred Changes"
+      },
+      {
+        "url": "#/groups/self",
+        "name": "Groups"
+      }
+    ]
+  }
+----
+
+As result the new preferences of the user are returned as a
+link:#preferences-info[PreferencesInfo] entity.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "changes_per_page": 50,
+    "show_site_header": true,
+    "use_flash_clipboard": true,
+    "date_format": "STD",
+    "time_format": "HHMM_12",
+    "size_bar_in_change_table": true,
+    "review_category_strategy": "NAME",
+    "comment_visibility_strategy": "EXPAND_RECENT",
+    "diff_view": "SIDE_BY_SIDE",
+    "my": [
+      {
+        "url": "#/dashboard/self",
+        "name": "Changes"
+      },
+      {
+        "url": "#/q/is:draft",
+        "name": "Drafts"
+      },
+      {
+        "url": "#/q/has:draft",
+        "name": "Draft Comments"
+      },
+      {
+        "url": "#/q/is:watched+is:open",
+        "name": "Watched Changes"
+      },
+      {
+        "url": "#/q/is:starred",
+        "name": "Starred Changes"
+      },
+      {
+        "url": "#/groups/self",
+        "name": "Groups"
+      }
+    ]
+  }
+----
+
 [[get-diff-preferences]]
 === Get Diff Preferences
 --
@@ -948,7 +1111,6 @@
   )]}'
   [
     {
-      "kind": "gerritcodereview#change",
       "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
       "project": "myProject",
       "branch": "master",
@@ -1152,6 +1314,103 @@
 link:access-control.html#capability_viewQueue[View Queue] capability.
 |=================================
 
+[[preferences-info]]
+=== PreferencesInfo
+The `PreferencesInfo` entity contains information about a user's preferences.
+
+[options="header",width="50%",cols="1,^1,5"]
+|=====================================
+|Field Name              ||Description
+|`changes_per_page`               ||
+The number of changes to show on each page.
+Allowed values are `10`, `25`, `50`, `100`.
+|`show_site_header`   |not set if `false`|
+Whether the site header should be shown.
+|`use_flash_clipboard`     |not set if `false`|
+Whether to use the flash clipboard widget.
+|`download_scheme`      ||
+The type of download URL the user prefers to use.
+|`download_command`     ||
+The type of download command the user prefers to use.
+|`copy_self_on_email`       |not set if `false`|
+Whether to CC me on comments I write.
+|`date_format`         ||
+The format to display the date in.
+Allowed values are `STD`, `US`, `ISO`, `EURO`, `UK`.
+|`time_format`     ||
+The format to display the time in.
+Allowed values are `HHMM_12`, `HHMM_24`.
+|`reverse_patch_set_order`     |not set if `false`|
+Whether to display the patch sets in reverse order.
+|`relative_date_in_change_table`  |not set if `false`|
+Whether to show relative dates in the changes table.
+|`size_bar_in_change_table`      |not set if `false`|
+Whether to show the change sizes as colored bars in the change table.
+|`legacycid_in_change_table`      |not set if `false`|
+Whether to show change number in the change table.
+|`review_category_strategy`   ||
+The strategy used to displayed info in the review category column.
+Allowed values are `NONE`, `NAME`, `EMAIL`, `USERNAME`, `ABBREV`.
+|`comment_visibility_strategy`         ||
+The strategy used to display the comments.
+Allowed values are `COLLAPSE_ALL`, `EXPAND_MOST_RECENT`, `EXPAND_RECENT`, `EXPAND_ALL`.
+|`diff_view`     ||
+The type of diff view to show.
+Allowed values are `SIDE_BY_SIDE`, `UNIFIED_DIFF`.
+|`change_screen`            ||
+The change screen to use.
+Allowed values are `OLD_UI`, `CHANGE_SCREEN2`.
+|=====================================
+
+[[preferences-input]]
+=== PreferencesInput
+The `PreferencesInput` entity contains information for setting the
+user preferences. Fields which are not set will not be updated.
+
+[options="header",width="50%",cols="1,^1,5"]
+|=====================================
+|Field Name              ||Description
+|`changes_per_page`               |optional|
+The number of changes to show on each page.
+Allowed values are `10`, `25`, `50`, `100`.
+|`show_site_header`   |optional|
+Whether the site header should be shown.
+|`use_flash_clipboard`     |optional|
+Whether to use the flash clipboard widget.
+|`download_scheme`      |optional|
+The type of download URL the user prefers to use.
+|`download_command`     |optional|
+The type of download command the user prefers to use.
+|`copy_self_on_email`       |optional|
+Whether to CC me on comments I write.
+|`date_format`         |optional|
+The format to display the date in.
+Allowed values are `STD`, `US`, `ISO`, `EURO`, `UK`.
+|`time_format`     |optional|
+The format to display the time in.
+Allowed values are `HHMM_12`, `HHMM_24`.
+|`reverse_patch_set_order`     |optional|
+Whether to display the patch sets in reverse order.
+|`relative_date_in_change_table`  |optional|
+Whether to show relative dates in the changes table.
+|`size_bar_in_change_table`      |optional|
+Whether to show the change sizes as colored bars in the change table.
+|`legacycid_in_change_table`      |optional|
+Whether to show change number in the change table.
+|`review_category_strategy`   |optional|
+The strategy used to displayed info in the review category column.
+Allowed values are `NONE`, `NAME`, `EMAIL`, `USERNAME`, `ABBREV`.
+|`comment_visibility_strategy`         |optional|
+The strategy used to display the comments.
+Allowed values are `COLLAPSE_ALL`, `EXPAND_MOST_RECENT`, `EXPAND_RECENT`, `EXPAND_ALL`.
+|`diff_view`     |optional|
+The type of diff view to show.
+Allowed values are `SIDE_BY_SIDE`, `UNIFIED_DIFF`.
+|`change_screen`            |optional|
+The change screen to use.
+Allowed values are `OLD_UI`, `CHANGE_SCREEN2`.
+|=====================================
+
 [[diff-preferences-info]]
 === DiffPreferencesInfo
 The `DiffPreferencesInfo` entity contains information about the diff
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 26bf070..e5fd1b6 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -7,15 +7,72 @@
 [[change-endpoints]]
 == Change Endpoints
 
+[[create-change]]
+=== Create Change
+--
+'POST /changes'
+--
+
+The change info link:#change-info[ChangeInfo] entity must be provided in the
+request body. Only the following attributes are honored: `project`,
+`branch`, `subject`, `status` and `topic`. The first three attributes are
+mandatory. Valid values for status are: `DRAFT` and `NEW`.
+
+.Request
+----
+  POST /changes HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "project" : "myProject",
+    "subject" : "Let's support 100% Gerrit workflow direct in browser",
+    "branch" : "master",
+    "topic" : "create-change-in-browser",
+    "status" : "DRAFT"
+  }
+----
+
+As response a link:#change-info[ChangeInfo] entity is returned that describes
+the resulting change.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9941",
+    "project": "myProject",
+    "branch": "master",
+    "topic": "create-change-in-browser",
+    "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9941",
+    "subject": "Let's support 100% Gerrit workflow direct in browser",
+    "status": "DRAFT",
+    "created": "2014-05-05 07:15:44.639000000",
+    "updated": "2014-05-05 07:15:44.639000000",
+    "mergeable": true,
+    "insertions": 0,
+    "deletions": 0,
+    "_sortkey": "002cbc25000004e5",
+    "_number": 4711,
+    "owner": {
+      "name": "John Doe"
+    }
+  }
+----
+
 [[list-changes]]
 === Query Changes
 --
 'GET /changes/'
 --
 
-Queries changes visible to the caller. The query string must be
-provided by the `q` parameter. The `n` parameter can be used to limit
-the returned results.
+Queries changes visible to the caller. The
+link:user-search.html#_search_operators[query string] must be provided
+by the `q` parameter. The `n` parameter can be used to limit the
+returned results.
 
 As result a list of link:#change-info[ChangeInfo] entries is returned.
 The change output is sorted by the last update time, most recently
@@ -37,7 +94,6 @@
   )]}'
   [
     {
-      "kind": "gerritcodereview#change",
       "id": "demo~master~Idaf5e098d70898b7119f6f4af5a6c13343d64b57",
       "project": "demo",
       "branch": "master",
@@ -56,7 +112,6 @@
       },
     },
     {
-      "kind": "gerritcodereview#change",
       "id": "demo~master~I09c8041b5867d5b33170316e2abc34b79bbb8501",
       "project": "demo",
       "branch": "master",
@@ -111,7 +166,6 @@
   [
     [
       {
-        "kind": "gerritcodereview#change",
         "id": "demo~master~Idaf5e098d70898b7119f6f4af5a6c13343d64b57",
         "project": "demo",
         "branch": "master",
@@ -241,6 +295,11 @@
   authenticated and has commented on the current revision.
 --
 
+[[patch-set-links]]
+--
+* `PATCHSET_LINKS`: include the `web_links` field.
+--
+
 .Request
 ----
   GET /changes/?q=97&o=CURRENT_REVISION&o=CURRENT_COMMIT&o=CURRENT_FILES&o=DOWNLOAD_COMMANDS HTTP/1.0
@@ -255,8 +314,7 @@
   )]}'
   [
     {
-      "kind": "gerritcodereview#change",
-      "id": "demo~master~I7ea46d2e2ee5c64c0d807677859cfb7d90b8966a",
+      "id": "gerrit~master~I7ea46d2e2ee5c64c0d807677859cfb7d90b8966a",
       "project": "gerrit",
       "branch": "master",
       "change_id": "I7ea46d2e2ee5c64c0d807677859cfb7d90b8966a",
@@ -392,7 +450,6 @@
 
   )]}'
   {
-    "kind": "gerritcodereview#change",
     "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
     "project": "myProject",
     "branch": "master",
@@ -433,7 +490,10 @@
 ----
 
 As response a link:#change-info[ChangeInfo] entity is returned that
-describes the change.
+describes the change.  This response will contain all votes for each
+label and include one combined vote.  The combined label vote is
+calculated in the following order (from highest to lowest):
+REJECTED > APPROVED > DISLIKED > RECOMMENDED.
 
 .Response
 ----
@@ -443,7 +503,6 @@
 
   )]}'
   {
-    "kind": "gerritcodereview#change",
     "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
     "project": "myProject",
     "branch": "master",
@@ -488,12 +547,6 @@
         }
       },
       "Code-Review": {
-        "recommended": {
-          "_account_id": 1000097,
-          "name": "Jane Roe",
-          "email": "jane.roe@example.com",
-          "username": "jroe"
-        },
         "disliked": {
           "_account_id": 1000096,
           "name": "John Doe",
@@ -694,7 +747,6 @@
 
   )]}'
   {
-    "kind": "gerritcodereview#change",
     "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
     "project": "myProject",
     "branch": "master",
@@ -754,7 +806,6 @@
 
   )]}'
   {
-    "kind": "gerritcodereview#change",
     "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
     "project": "myProject",
     "branch": "master",
@@ -812,7 +863,6 @@
 
   )]}'
   {
-    "kind": "gerritcodereview#change",
     "id": "myProject~master~I3ea943139cb62e86071996f2480e58bf3eeb9dd2",
     "project": "myProject",
     "branch": "master",
@@ -905,7 +955,6 @@
 
   )]}'
   {
-    "kind": "gerritcodereview#change",
     "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
     "project": "myProject",
     "branch": "master",
@@ -971,7 +1020,6 @@
 
   )]}'
   {
-    "kind": "gerritcodereview#change",
     "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
     "project": "myProject",
     "branch": "master",
@@ -1061,7 +1109,6 @@
 
   )]}'
   {
-    "kind": "gerritcodereview#includedininfo",
     "branches": [
       "master"
     ],
@@ -1119,7 +1166,6 @@
   )]}'
   [
     {
-      "kind": "gerritcodereview#reviewer",
       "approvals": {
         "Verified": "+1",
         "Code-Review": "+2"
@@ -1129,7 +1175,6 @@
       "email": "john.doe@example.com"
     },
     {
-      "kind": "gerritcodereview#reviewer",
       "approvals": {
         "Verified": " 0",
         "Code-Review": "-1"
@@ -1166,7 +1211,6 @@
   )]}'
   [
     {
-      "kind": "gerritcodereview#suggestedreviewer",
       "account": {
         "_account_id": 1000097,
         "name": "Jane Roe",
@@ -1174,7 +1218,6 @@
       }
     },
     {
-      "kind": "gerritcodereview#suggestedreviewer",
       "group": {
         "id": "4fd581c0657268f2bdcc26699fbf9ddb76e3a279",
         "name": "Joiner"
@@ -1207,7 +1250,6 @@
 
   )]}'
   {
-    "kind": "gerritcodereview#reviewer",
     "approvals": {
       "Verified": "+1",
       "Code-Review": "+2"
@@ -1252,7 +1294,6 @@
   {
     "reviewers": [
       {
-        "kind": "gerritcodereview#reviewer",
         "approvals": {
           "Verified": " 0",
           "Code-Review": " 0"
@@ -1354,7 +1395,6 @@
 
   )]}'
   {
-    "kind": "gerritcodereview#commit",
     "parents": [
       {
         "commit": "1eee2c9d8f352483781e772f35dc586a69ff5646",
@@ -1409,7 +1449,6 @@
 
   )]}'
   {
-    "kind": "gerritcodereview#change",
     "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
     "project": "myProject",
     "branch": "master",
@@ -1514,6 +1553,79 @@
   }
 ----
 
+[[get-related-changes]]
+=== Get Related Changes
+--
+'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/related'
+--
+
+Retrieves related changes of a revision.  Related changes are changes that either
+depend on, or are dependencies of the revision.
+
+.Request
+----
+  GET /changes/gerrit~master~I5e4fc08ce34d33c090c9e0bf320de1b17309f774/revisions/b1cb4caa6be46d12b94c25aa68aebabcbb3f53fe/related HTTP/1.0
+----
+
+As result a link:#related-changes-info[RelatedChangesInfo] entity is returned
+describing the related changes.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "changes": [
+      {
+        "change_id": "Ic62ae3103fca2214904dbf2faf4c861b5f0ae9b5",
+        "commit": {
+          "commit": "78847477532e386f5a2185a4e8c90b2509e354e3",
+          "parents": [
+            {
+              "commit": "bb499510bbcdbc9164d96b0dbabb4aa45f59a87e"
+            }
+          ],
+          "author": {
+            "name": "David Ostrovsky",
+            "email": "david@ostrovsky.org",
+            "date": "2014-07-12 15:04:24.000000000",
+            "tz": 120
+          },
+          "subject": "Remove Solr"
+        },
+        "_change_number": 58478,
+        "_revision_number": 2,
+        "_current_revision_number": 2
+      },
+      {
+        "change_id": "I5e4fc08ce34d33c090c9e0bf320de1b17309f774",
+        "commit": {
+          "commit": "b1cb4caa6be46d12b94c25aa68aebabcbb3f53fe",
+          "parents": [
+            {
+              "commit": "d898f12a9b7a92eb37e7a80636195a1b06417aad"
+            }
+          ],
+          "author": {
+            "name": "David Pursehouse",
+            "email": "david.pursehouse@sonymobile.com",
+            "date": "2014-06-24 02:01:28.000000000",
+            "tz": 540
+          },
+          "subject": "Add support for secondary index with Elasticsearch"
+        },
+        "_change_number": 58081,
+        "_revision_number": 10,
+        "_current_revision_number": 10
+      }
+    ]
+  }
+----
+
+
 [[set-review]]
 === Set Review
 --
@@ -1601,7 +1713,6 @@
 
   )]}'
   {
-    "kind": "gerritcodereview#change",
     "id": "myProject~master~I3ea943139cb62e86071996f2480e58bf3eeb9dd2",
     "project": "myProject",
     "branch": "master",
@@ -1804,6 +1915,8 @@
   GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/current/mergeable HTTP/1.0
 ----
 
+As response a link:#mergeable-info[MergeableInfo] entity is returned.
+
 .Response
 ----
   HTTP/1.1 200 OK
@@ -1817,6 +1930,37 @@
   }
 ----
 
+If the `other-branches` parameter is specified, the mergeability will also be
+checked for all other branches.
+
+If the `force` parameter is specified, the mergeability against the destination
+will be rechecked, in case of prior transient failures or bugs.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/current/mergeable?other-branches HTTP/1.0
+----
+
+The response will then contain a list of all other branches where this changes
+could merge cleanly.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    submit_type: "MERGE_IF_NECESSARY",
+    mergeable: true,
+    mergeable_into: [
+        "refs/heads/stable-2.7",
+        "refs/heads/stable-2.8",
+    ]
+  }
+----
+
 [[get-submit-type]]
 === Get Submit Type
 --
@@ -1941,14 +2085,12 @@
   {
     "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java": [
       {
-        "kind": "gerritcodereview#comment",
         "id": "TvcXrmjM",
         "line": 23,
         "message": "[nit] trailing whitespace",
         "updated": "2013-02-26 15:40:43.986000000"
       },
       {
-        "kind": "gerritcodereview#comment",
         "id": "TveXwFiA",
         "line": 49,
         "in_reply_to": "TfYX-Iuo",
@@ -1993,7 +2135,6 @@
 
   )]}'
   {
-    "kind": "gerritcodereview#comment",
     "id": "TvcXrmjM",
     "path": "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java",
     "line": 23,
@@ -2027,7 +2168,6 @@
 
   )]}'
   {
-    "kind": "gerritcodereview#comment",
     "id": "TvcXrmjM",
     "path": "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java",
     "line": 23,
@@ -2070,7 +2210,6 @@
 
   )]}'
   {
-    "kind": "gerritcodereview#comment",
     "id": "TvcXrmjM",
     "path": "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java",
     "line": 23,
@@ -2107,7 +2246,9 @@
 
 As result a map is returned that maps the file path to a list of
 link:#comment-info[CommentInfo] entries. The entries in the map are
-sorted by file path.
+sorted by file path and only include file (or inline) comments. Use
+the link:#get-change-detail[Get Change Detail] endpoint to retrieve
+the general change message (or comment).
 
 .Request
 ----
@@ -2124,7 +2265,6 @@
   {
     "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java": [
       {
-        "kind": "gerritcodereview#comment",
         "id": "TvcXrmjM",
         "line": 23,
         "message": "[nit] trailing whitespace",
@@ -2136,7 +2276,6 @@
         }
       },
       {
-        "kind": "gerritcodereview#comment",
         "id": "TveXwFiA",
         "line": 49,
         "in_reply_to": "TfYX-Iuo",
@@ -2176,7 +2315,6 @@
 
   )]}'
   {
-    "kind": "gerritcodereview#comment",
     "id": "TvcXrmjM",
     "path": "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java",
     "line": 23,
@@ -2516,7 +2654,6 @@
 
   )]}'
   {
-    "kind": "gerritcodereview#change",
     "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9941",
     "project": "myProject",
     "branch": "release-branch",
@@ -2568,7 +2705,6 @@
 
   )]}'
   {
-    "kind": "gerritcodereview#change",
     "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9941",
     "project": "myProject",
     "branch": "release-branch",
@@ -2732,7 +2868,6 @@
 [options="header",width="50%",cols="1,^1,5"]
 |==================================
 |Field Name           ||Description
-|`kind`               ||`gerritcodereview#change`
 |`id`                 ||
 The ID of the change in the format "'<project>\~<branch>~<Change-Id>'",
 where 'project', 'branch' and 'Change-Id' are URL encoded. For 'branch' the
@@ -2761,7 +2896,7 @@
 Only set if link:#reviewed[reviewed] is requested.
 |`mergeable`          |optional|
 Whether the change is mergeable. +
-Not set for merged changes.
+Not set for merged changes, or if the change has not yet been tested.
 |`insertions`         ||
 Number of inserted lines.
 |`deletions`          ||
@@ -2807,6 +2942,37 @@
 Only set on either the last or the first change that is returned.
 |==================================
 
+[[related-changes-info]]
+=== RelatedChangesInfo
+The `RelatedChangesInfo` entity contains information about related
+changes.
+
+[options="header",width="50%",cols="1,6"]
+|===========================
+|Field Name                |Description
+|`changes`                 |A list of
+link:#related-change-and-commit-info[RelatedChangeAndCommitInfo] entities
+describing the related changes. Sorted by git commit order, newest to
+oldest. Empty if there are no related changes.
+|===========================
+
+[[related-change-and-commit-info]]
+=== RelatedChangeAndCommitInfo
+
+The `RelatedChangeAndCommitInfo` entity contains information about
+a related change and commit.
+
+[options="header",width="50%",cols="1,^1,5"]
+|===========================
+|Field Name                ||Description
+|`change_id`               |optional|The Change-Id of the change.
+|`commit`                  ||The commit as a
+link:#commit-info[CommitInfo] entity.
+|`_change_number`          |optional|The change number.
+|`_revision_number`        |optional|The revision number.
+|`_current_revision_number`|optional|The current revision number.
+|===========================
+
 [[change-message-info]]
 === ChangeMessageInfo
 The `ChangeMessageInfo` entity contains information about a message
@@ -2817,7 +2983,7 @@
 |Field Name           ||Description
 |`id`                 ||The ID of the message.
 |`author`             |optional|
-Author of the message as an 
+Author of the message as an
 link:rest-api-accounts.html#account-info[AccountInfo] entity. +
 Unset if written by the Gerrit system.
 |`date`            ||
@@ -2835,7 +3001,7 @@
 |===========================
 |Field Name    |Description
 |`message`     |Commit message for the cherry-picked change
-|`destination` |Destination Branch
+|`destination` |Destination branch
 |===========================
 
 [[comment-info]]
@@ -2845,7 +3011,6 @@
 [options="header",width="50%",cols="1,^1,5"]
 |===========================
 |Field Name    ||Description
-|`kind`        ||`gerritcodereview#comment`
 |`id`          ||The URL encoded UUID of the comment.
 |`path`        |optional|
 The path of the file for which the inline comment was done. +
@@ -2881,8 +3046,6 @@
 [options="header",width="50%",cols="1,^1,5"]
 |===========================
 |Field Name    ||Description
-|`kind`        |optional|
-Must be `gerritcodereview#comment` if provided.
 |`id`          |optional|
 The URL encoded UUID of the comment if an existing draft comment should
 be updated.
@@ -3119,6 +3282,8 @@
 |`value`       |optional|The voting value of the user who
 recommended/disliked this label on the change if it is not
 "`+1`"/"`-1`".
+|`default_value`|optional|The default voting value for the label.
+This value may be outside the range specified in permitted_labels.
 |===========================
 
 ==== Fields set by `DETAILED_LABELS`
@@ -3132,6 +3297,23 @@
 to the value descriptions.
 |===========================
 
+[[mergeable-info]]
+=== MergeableInfo
+The `MergeableInfo` entity contains information about the mergeability of a
+change.
+
+[options="header",width="50%",cols="1,^1,5"]
+|============================
+|Field Name      ||Description
+|`submit_type`   ||
+Submit type used for this change, can be `MERGE_IF_NECESSARY`,
+`FAST_FORWARD_ONLY`, `REBASE_IF_NECESSARY`, `MERGE_ALWAYS` or
+`CHERRY_PICK`.
+|`mergeable`     ||
+`true` if this change is cleanly mergeable, `false` otherwise
+|`mergeable_into`|optional|
+A list of other branch names where this change could merge cleanly
+|============================
 
 [[restore-input]]
 === RestoreInput
@@ -3222,7 +3404,6 @@
 [options="header",width="50%",cols="1,6"]
 |==========================
 |Field Name    |Description
-|`kind`        |`gerritcodereview#reviewer`
 |`approvals`   |
 The approvals of the reviewer as a map that maps the label names to the
 approval values ("`-2`", "`-1`", "`0`", "`+1`", "`+2`").
@@ -3274,6 +3455,9 @@
 Actions the caller might be able to perform on this revision. The
 information is a map of view name to link:#action-info[ActionInfo]
 entities.
+|'web_links'   |optional|
+Links to the patch set in external sites as a list of
+link:#web-link-info[WebLinkInfo] entities.
 |===========================
 
 [[rule-input]]
@@ -3396,13 +3580,22 @@
 [options="header",width="50%",cols="1,6"]
 |==========================
 |Field Name |Description
-|`kind`     |`gerritcodereview#includedininfo`
 |`branches` | The list of branches this change was merged into.
 Each branch is listed without the 'refs/head/' prefix.
 |`tags`     | The list of tags this change was tagged with.
 Each tag is listed without the 'refs/tags/' prefix.
 |==========================
 
+[[web-link-info]]
+=== WebLinkInfo
+The `WebLinkInfo` entity describes a link to an external site.
+
+[options="header",width="50%",cols="1,6"]
+|======================
+|Field Name|Description
+|`name`    |The link name.
+|`url`     |The link URL.
+|======================
 
 GERRIT
 ------
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index 6a3b34e..2968ebb 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -30,6 +30,515 @@
   "2.7"
 ----
 
+[[list-caches]]
+=== List Caches
+--
+'GET /config/server/caches/'
+--
+
+Lists the caches of the server. Caches defined by plugins are included.
+
+The caller must be a member of a group that is granted the
+link:access-control.html#capability_viewCaches[View Caches] capability
+or the link:access-control.html#capability_administrateServer[
+Administrate Server] capability.
+
+As result a map of link:#cache-info[CacheInfo] entities is returned.
+
+The entries in the map are sorted by cache name.
+
+.Request
+----
+  GET /config/server/caches/ HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "accounts": {
+      "type": "MEM",
+      "entries": {
+        "mem": 4
+      },
+      "average_get": "2.5ms",
+      "hit_ratio": {
+        "mem": 94
+      }
+    },
+    "accounts_byemail": {
+      "type": "MEM",
+      "entries": {
+        "mem": 4
+      },
+      "average_get": "771.8us",
+      "hit_ratio": {
+        "mem": 95
+      }
+    },
+    "accounts_byname": {
+      "type": "MEM",
+      "entries": {
+        "mem": 4
+      },
+      "hit_ratio": {
+        "mem": 100
+      }
+    },
+    "adv_bases": {
+      "type": "MEM",
+      "entries": {},
+      "hit_ratio": {}
+    },
+    "change_kind": {
+      "type": "DISK",
+      "entries": {
+        "space": "0.00k"
+      },
+      "hit_ratio": {}
+    },
+    "changes": {
+      "type": "MEM",
+      "entries": {},
+      "hit_ratio": {}
+    },
+    "conflicts": {
+      "type": "DISK",
+      "entries": {
+        "mem": 2,
+        "disk": 3,
+        "space": "2.75k"
+      },
+      "hit_ratio": {
+        "mem": 0,
+        "disk": 100
+      }
+    },
+    "diff": {
+      "type": "DISK",
+      "entries": {
+        "mem": 177,
+        "disk": 253,
+        "space": "170.97k"
+      },
+      "average_get": "1.1ms",
+      "hit_ratio": {
+        "mem": 67,
+        "disk": 100
+      }
+    },
+    "diff_intraline": {
+      "type": "DISK",
+      "entries": {
+        "mem": 1,
+        "disk": 1,
+        "space": "0.37k"
+      },
+      "average_get": "6.8ms",
+      "hit_ratio": {
+        "mem": 0
+      }
+    },
+    "git_tags": {
+      "type": "DISK",
+      "entries": {
+        "space": "0.00k"
+      },
+      "hit_ratio": {}
+    },
+    groups": {
+      "type": "MEM",
+      "entries": {
+        "mem": 27
+      },
+      "average_get": "183.2us",
+      "hit_ratio": {
+        "mem": 12
+      }
+    },
+    "groups_byinclude": {
+      "type": "MEM",
+      "entries": {},
+      "hit_ratio": {}
+    },
+    "groups_byname": {
+      "type": "MEM",
+      "entries": {},
+      "hit_ratio": {}
+    },
+    "groups_byuuid": {
+      "type": "MEM",
+      "entries": {
+        "mem": 25
+      },
+      "average_get": "173.4us",
+      "hit_ratio": {
+        "mem": 13
+      }
+    },
+    "groups_external": {
+      "type": "MEM",
+      "entries": {},
+      "hit_ratio": {}
+    },
+    groups_members": {
+      "type": "MEM",
+      "entries": {
+        "mem": 4
+      },
+      "average_get": "697.8us",
+      "hit_ratio": {
+        "mem": 82
+      }
+    },
+    "permission_sort": {
+      "type": "MEM",
+      "entries": {
+        "mem": 16
+      },
+      "hit_ratio": {
+        "mem": 96
+      }
+    },
+    "plugin_resources": {
+      "type": "MEM",
+      "entries": {
+        "mem": 2
+      },
+      "hit_ratio": {
+        "mem": 83
+      }
+    },
+    "project_list": {
+      "type": "MEM",
+      "entries": {
+        "mem": 1
+      },
+      "average_get": "18.6ms",
+      "hit_ratio": {
+        "mem": 0
+      }
+    },
+    "projects": {
+      "type": "MEM",
+      "entries": {
+        "mem": 35
+      },
+      "average_get": "8.6ms",
+      "hit_ratio": {
+        "mem": 99
+      }
+    },
+    "quota-repo_size": {
+      "type": "DISK",
+      "entries": {
+        "space": "0.00k"
+      },
+      "hit_ratio": {}
+    },
+    "sshkeys": {
+      "type": "MEM",
+      "entries": {
+        "mem": 1
+      },
+      "average_get": "3.2ms",
+      "hit_ratio": {
+        "mem": 50
+      }
+    },
+    "web_sessions": {
+      "type": "DISK",
+      "entries": {
+        "mem": 1,
+        "disk": 2,
+        "space": "0.78k"
+      },
+      "hit_ratio": {
+        "mem": 82
+      }
+    }
+  }
+----
+
+It is possible to get different output formats by specifying the
+`format` option:
+
+* `LIST`:
++
+Returns the cache names as JSON list.
++
+The cache names are alphabetically sorted.
++
+.Request
+----
+  GET /config/server/caches/?format=LIST HTTP/1.0
+----
++
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  [
+    "accounts",
+    "accounts_byemail",
+    "accounts_byname",
+    "adv_bases",
+    "change_kind",
+    "changes",
+    "conflicts",
+    "diff",
+    "diff_intraline",
+    "git_tags",
+    "groups",
+    "groups_byinclude",
+    "groups_byname",
+    "groups_byuuid",
+    "groups_external",
+    "groups_members",
+    "permission_sort",
+    "plugin_resources",
+    "project_list",
+    "projects",
+    "quota-repo_size",
+    "sshkeys",
+    "web_sessions"
+  ]
+----
+
+* `TEXT_LIST`:
++
+Returns the cache names as a UTF-8 list that is base64 encoded. The
+cache names are delimited by '\n'.
++
+The cache names are lexicographically sorted.
++
+.Request
+----
+  GET /config/server/caches/?format=TEXT_LIST HTTP/1.0
+----
++
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Type: text/plain;charset=UTF-8
+
+  YWNjb3VudHMKYW...ViX3Nlc3Npb25z
+----
++
+E.g. this could be used to flush all caches:
++
+----
+  for c in $(curl --digest --user jdoe:TNAuLkXsIV7w http://gerrit/a/config/server/caches/?format=TEXT_LIST | base64 -D)
+  do
+    curl --digest --user jdoe:TNAuLkXsIV7w -X POST http://gerrit/a/config/server/caches/$c/flush
+  done
+----
+
+[[cache-operations]]
+=== Cache Operations
+--
+'POST /config/server/caches/'
+--
+
+Executes a cache operation that is specified in the request body in a
+link:#cache-operation-input[CacheOperationInput] entity.
+
+[[flush-all-caches]]
+==== Flush All Caches
+
+.Request
+----
+  POST /config/server/caches/ HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "operation": "FLUSH_ALL"
+  }
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+----
+
+[[flush-several-caches]]
+==== Flush Several Caches At Once
+
+.Request
+----
+  POST /config/server/caches/ HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "operation": "FLUSH"
+    "caches": [
+      "projects",
+      "project_list"
+    ]
+  }
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+----
+
+[[get-cache]]
+=== Get Cache
+--
+'GET /config/server/caches/link:#cache-name[\{cache-name\}]'
+--
+
+Retrieves information about a cache.
+
+The caller must be a member of a group that is granted the
+link:access-control.html#capability_viewCaches[View Caches] capability
+or the link:access-control.html#capability_administrateServer[
+Administrate Server] capability.
+
+As result a link:#cache-info[CacheInfo] entity is returned.
+
+.Request
+----
+  GET /config/server/caches/projects HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "name": "projects",
+    "type": "MEM",
+    "entries": {
+      "mem": 35
+    },
+    "average_get": " 8.6ms",
+    "hit_ratio": {
+      "mem": 99
+    }
+  }
+----
+
+[[flush-cache]]
+=== Flush Cache
+--
+'POST /config/server/caches/link:#cache-name[\{cache-name\}]/flush'
+--
+
+Flushes a cache.
+
+The caller must be a member of a group that is granted the
+link:access-control.html#capability_flushCaches[Flush Caches] capability
+or the link:access-control.html#capability_administrateServer[
+Administrate Server] capability.
+
+The "web_sessions" cache can only be flushed if the caller is member of
+a group that is granted the
+link:access-control.html#capability_administrateServer[Administrate
+Server] capability.
+
+.Request
+----
+  POST /config/server/caches/projects/flush HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+----
+
+[[get-summary]]
+=== Get Summary
+--
+'GET /config/server/summary'
+--
+
+Retrieves a summary of the current server state.
+
+The caller must be a member of a group that is granted the
+link:access-control.html#capability_administrateServer[Administrate
+Server] capability.
+
+The following options are supported:
+
+* `jvm`:
++
+Includes a JVM summary.
+
+* `gc`:
++
+Requests a Java garbage collection before computing the information
+about the Java memory heap.
+
+.Request
+----
+  GET /config/server/summary?jvm HTTP/1.0
+----
+
+As result a link:#summary-info[SummaryInfo] entity is returned.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "task_summary": {
+      "total": 2,
+      "sleeping": 2
+    },
+    "mem_summary": {
+      "total": "341.06m",
+      "used": "57.16m",
+      "free": "283.90m",
+      "buffers": "0.00k",
+      "max": "1.67g",
+    }
+    "thread_summary": {
+      "cpus": 8,
+      "threads": 44,
+      "counts": {
+        "HTTP": {
+          "RUNNABLE": 3,
+          "TIMED_WAITING": 2
+        },
+        "SSH-Interactive-Worker": {
+          "WAITING": 1
+        },
+        "Other": {
+          "WAITING": 10,
+          "RUNNABLE": 2,
+          "TIMED_WAITING": 25
+        },
+        "SshCommandStart": {
+          "WAITING": 1
+        }
+      }
+    },
+    "jvm_summary": {
+      "vm_vendor": "Oracle Corporation",
+      "vm_name": "Java HotSpot(TM) 64-Bit Server VM",
+      "vm_version": "23.25-b01",
+      "os_name": "Mac OS X",
+      "os_version": "10.8.5",
+      "os_arch": "x86_64",
+      "user": "gerrit",
+      "host": "GERRIT",
+      "current_working_directory": "/Users/gerrit/site",
+      "site": "/Users/gerrit/site"
+    }
+  }
+----
+
 [[list-capabilities]]
 === List Capabilities
 --
@@ -57,88 +566,204 @@
   )]}'
   {
     "accessDatabase": {
-      "kind": "gerritcodereview#capability",
       "id": "accessDatabase",
       "name": "Access Database"
     },
     "administrateServer": {
-      "kind": "gerritcodereview#capability",
       "id": "administrateServer",
       "name": "Administrate Server"
     },
     "createAccount": {
-      "kind": "gerritcodereview#capability",
       "id": "createAccount",
       "name": "Create Account"
     },
     "createGroup": {
-      "kind": "gerritcodereview#capability",
       "id": "createGroup",
       "name": "Create Group"
     },
     "createProject": {
-      "kind": "gerritcodereview#capability",
       "id": "createProject",
       "name": "Create Project"
     },
     "emailReviewers": {
-      "kind": "gerritcodereview#capability",
       "id": "emailReviewers",
       "name": "Email Reviewers"
     },
     "flushCaches": {
-      "kind": "gerritcodereview#capability",
       "id": "flushCaches",
       "name": "Flush Caches"
     },
     "killTask": {
-      "kind": "gerritcodereview#capability",
       "id": "killTask",
       "name": "Kill Task"
     },
     "priority": {
-      "kind": "gerritcodereview#capability",
       "id": "priority",
       "name": "Priority"
     },
     "queryLimit": {
-      "kind": "gerritcodereview#capability",
       "id": "queryLimit",
       "name": "Query Limit"
     },
     "runGC": {
-      "kind": "gerritcodereview#capability",
       "id": "runGC",
       "name": "Run Garbage Collection"
     },
     "streamEvents": {
-      "kind": "gerritcodereview#capability",
       "id": "streamEvents",
       "name": "Stream Events"
     },
     "viewCaches": {
-      "kind": "gerritcodereview#capability",
       "id": "viewCaches",
       "name": "View Caches"
     },
     "viewConnections": {
-      "kind": "gerritcodereview#capability",
       "id": "viewConnections",
       "name": "View Connections"
     },
     "viewPlugins": {
-      "kind": "gerritcodereview#capability",
       "id": "viewPlugins",
       "name": "View Plugins"
     },
     "viewQueue": {
-      "kind": "gerritcodereview#capability",
       "id": "viewQueue",
       "name": "View Queue"
     }
   }
 ----
 
+[[list-tasks]]
+=== List Tasks
+--
+'GET /config/server/tasks/'
+--
+
+Lists the tasks from the background work queues that the Gerrit daemon
+is currently performing, or will perform in the near future.
+
+Gerrit contains an internal scheduler, similar to cron, that it uses to
+queue and dispatch both short and long term tasks.
+
+Tasks that are completed or canceled exit the queue very quickly once
+they enter this state, but it can be possible to observe tasks in these
+states.
+
+End-users may see a task only if they can also see the project the task
+is associated with. Tasks operating on other projects, or that do not
+have a specific project, are hidden.
+
+Members of a group that is granted the
+link:access-control.html#capability_viewQueue[View Queue] capability or
+the link:access-control.html#capability_administrateServer[Administrate
+Server] capability can see all tasks.
+
+As result a list of link:#task-info[TaskInfo] entities is returned.
+
+The entries in the list are sorted by task state, remaining delay and
+command.
+
+.Request
+----
+  GET /config/server/tasks/ HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  [
+    {
+      "id": "1e688bea",
+      "state": "SLEEPING",
+      "start_time": "2014-06-11 12:58:51.991000000",
+      "delay": 3453,
+      "command": "Reload Submit Queue"
+    },
+    {
+      "id": "3e6d4ffa",
+      "state": "SLEEPING",
+      "start_time": "2014-06-11 12:58:51.508000000",
+      "delay": 3287966,
+      "command": "Log File Compressor"
+    }
+  ]
+----
+
+[[get-task]]
+=== Get Task
+--
+'GET /config/server/tasks/link:#task-id[\{task-id\}]'
+--
+
+Retrieves a task from the background work queue that the Gerrit daemon
+is currently performing, or will perform in the near future.
+
+End-users may see a task only if they can also see the project the task
+is associated with. Tasks operating on other projects, or that do not
+have a specific project, are hidden.
+
+Members of a group that is granted the
+link:access-control.html#capability_viewQueue[View Queue] capability or
+the link:access-control.html#capability_administrateServer[Administrate
+Server] capability can see all tasks.
+
+As result a link:#task-info[TaskInfo] entity is returned.
+
+.Request
+----
+  GET /config/server/tasks/1e688bea HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "id": "1e688bea",
+    "state": "SLEEPING",
+    "start_time": "2014-06-11 12:58:51.991000000",
+    "delay": 3453,
+    "command": "Reload Submit Queue"
+  }
+----
+
+[[delete-task]]
+=== Delete Task
+--
+'DELETE /config/server/tasks/link:#task-id[\{task-id\}]'
+--
+
+Kills a task from the background work queue that the Gerrit daemon
+is currently performing, or will perform in the near future.
+
+The caller must be a member of a group that is granted the
+link:access-control.html#capability_kill[Kill Task] capability
+or the link:access-control.html#capability_administrateServer[
+Administrate Server] capability.
+
+End-users may see a task only if they can also see the project the task
+is associated with. Tasks operating on other projects, or that do not
+have a specific project, are hidden.
+
+Members of a group that is granted the
+link:access-control.html#capability_viewQueue[View Queue] capability or
+the link:access-control.html#capability_administrateServer[Administrate
+Server] capability can see all tasks.
+
+.Request
+----
+  DELETE /config/server/tasks/1e688bea HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
 [[get-top-menus]]
 === Get Top Menus
 --
@@ -176,9 +801,52 @@
 ----
 
 
+[[ids]]
+== IDs
+
+[[cache-name]]
+=== \{cache-name\}
+The name of the cache.
+
+If the cache is defined by a plugin the cache name must include the
+plugin name: "<plugin-name>-<cache-name>".
+
+Gerrit core caches can optionally be prefixed with "gerrit":
+"gerrit-<cache-name>".
+
+[[task-id]]
+=== \{task-id\}
+The ID of the task (hex string).
+
+
 [[json-entities]]
 == JSON Entities
 
+[[cache-info]]
+=== CacheInfo
+The `CacheInfo` entity contains information about a cache.
+
+[options="header",width="50%",cols="1,^1,5"]
+|==================================
+|Field Name           ||Description
+|`name`               |
+not set if returned in a map where the cache name is used as map key|
+The cache name. If the cache is defined by a plugin the cache name
+includes the plugin name: "<plugin-name>-<cache-name>".
+|`type`               ||
+The type of the cache (`MEM`: in memory cache, `DISK`: disk cache).
+|`entries`            ||
+Information about the entries in the cache as a
+link:#entries-info[EntriesInfo] entity.
+|`average_get`        |optional|
+The average duration of getting one entry from the cache. The value is
+returned with a standard time unit abbreviation (`ns`: nanoseconds,
+`us`: microseconds, `ms`: milliseconds, `s`: seconds).
+|`hit_ratio`          ||
+Information about the hit ratio as a link:#hit-ration-info[
+HitRatioInfo] entity.
+|==================================
+
 [[capability-info]]
 === CapabilityInfo
 The `CapabilityInfo` entity contains information about a capability.
@@ -186,11 +854,195 @@
 [options="header",width="50%",cols="1,6"]
 |=================================
 |Field Name           |Description
-|`kind`               |`gerritcodereview#capability`
 |`id`                 |capability ID
 |`name`               |capability name
 |=================================
 
+[[cache-operation-input]]
+=== CacheOperationInput
+The `CacheOperationInput` entity contains information about an
+operation that should be executed on caches.
+
+[options="header",width="50%",cols="1,^1,5"]
+|==================================
+|Field Name           ||Description
+|`operation`          ||
+The cache operation that should be executed:
+
+`FLUSH_ALL`: Flushes all caches, except the `web_sessions` cache.
+
+`FLUSH`: Flushes the specified caches.
+|`caches`             |optional|
+A list of cache names. This list defines the caches on which the
+specified operation should be executed. Whether this list must be
+specified depends on the operation being executed.
+|==================================
+
+[[entries-info]]
+=== EntriesInfo
+The `EntriesInfo` entity contains information about the entries in a
+cache.
+
+[options="header",width="50%",cols="1,^1,5"]
+|==================================
+|Field Name ||Description
+|`mem`      |optional|Number of cache entries that are held in memory.
+|`disk`     |optional|Number of cache entries on the disk. For non-disk
+caches this value is not set; for disk caches it is only set if there
+are entries in the cache.
+|`space`    |optional|
+The space that is consumed by the cache on disk. The value is returned
+with a unit abbreviation (`k`: kilobytes, `m`: megabytes,
+`g`: gigabytes). Only set for disk caches.
+|==================================
+
+[[hit-ration-info]]
+=== HitRatioInfo
+The `HitRatioInfo` entity contains information about the hit ratio of a
+cache.
+
+[options="header",width="50%",cols="1,^1,5"]
+|==================================
+|Field Name ||Description
+|`mem`      ||
+Hit ratio for cache entries that are held in memory (0 \<= value \<= 100).
+|`disk`     |optional|
+Hit ratio for cache entries that are held on disk (0 \<= value \<= 100).
+Only set for disk caches.
+|==================================
+
+[[jvm-summary-info]]
+=== JvmSummaryInfo
+The `JvmSummaryInfo` entity contains information about the JVM.
+
+[options="header",width="50%",cols="1,^1,5"]
+|========================================
+|Field Name                 ||Description
+|`vm_vendor`                ||The vendor of the virtual machine.
+|`vm_name`                  ||The name of the virtual machine.
+|`vm_version`               ||The version of the virtual machine.
+|`os_name`                  ||The name of the operating system.
+|`os_version`               ||The version of the operating system.
+|`os_arch`                  ||The architecture of the operating system.
+|`user`                     ||The user that is running Gerrit.
+|`host`                     |optional|
+The host on which Gerrit is running.
+|`current_working_directory`||The current working directory.
+|`site`                     ||The path to the review site.
+|========================================
+
+[[mem-summary-info]]
+=== MemSummaryInfo
+The `MemSummaryInfo` entity contains information about the current
+memory usage.
+
+[options="header",width="50%",cols="1,^1,5"]
+|============================
+|Field Name     ||Description
+|`total`        ||
+The total size of the memory. The value is returned with a unit
+abbreviation (`k`: kilobytes, `m`: megabytes, `g`: gigabytes).
+|`used`         ||
+The size of used memory. The value is returned with a unit abbreviation
+(`k`: kilobytes, `m`: megabytes, `g`: gigabytes).
+|`free`         ||
+The size of free memory. The value is returned with a unit abbreviation
+(`k`: kilobytes, `m`: megabytes, `g`: gigabytes).
+|`buffers`      ||
+The size of memory used for JGit buffers. The value is returned with a
+unit abbreviation (`k`: kilobytes, `m`: megabytes, `g`: gigabytes).
+|`max`          ||
+The maximal memory size. The value is returned with a unit abbreviation
+(`k`: kilobytes, `m`: megabytes, `g`: gigabytes).
+|`open_files`   |optional|
+The number of open files.
+|============================
+
+[[summary-info]]
+=== SummaryInfo
+The `SummaryInfo` entity contains information about the current state
+of the server.
+
+[options="header",width="50%",cols="1,^1,5"]
+|============================
+|Field Name     ||Description
+|`task_summary` ||
+Summary about current tasks as a link:#task-summary-info[
+TaskSummaryInfo] entity.
+|`mem_summary`  ||
+Summary about current memory usage as a link:#mem-summary-info[
+MemSummaryInfo] entity.
+|`thread_summary`  ||
+Summary about current threads as a link:#thread-summary-info[
+ThreadSummaryInfo] entity.
+|`jvm_summary`  |optional|
+Summary about the JVM link:#jvm-summary-info[JvmSummaryInfo] entity.
+Only set if the `jvm` option was set.
+|============================
+
+[[task-info]]
+=== TaskInfo
+The `TaskInfo` entity contains information about a task in a background
+work queue.
+
+[options="header",width="50%",cols="1,^1,5"]
+|====================================
+|Field Name   ||Description
+|`id`         ||The ID of the task.
+|`state`      ||
+The state of the task, can be `DONE`, `CANCELLED`, `RUNNING`, `READY`,
+`SLEEPING` and `OTHER`.
+|`start_time` ||The start time of the task.
+|`delay`      ||The remaining delay of the task.
+|`command`    ||The command of the task.
+|`remote_name`|optional|
+The remote name. May only be set for tasks that are associated with a
+project.
+|`project`    |optional|The project the task is associated with.
+|====================================
+
+[[task-summary-info]]
+=== TaskSummaryInfo
+The `TaskSummaryInfo` entity contains information about the current
+tasks.
+
+[options="header",width="50%",cols="1,^1,5"]
+|============================
+|Field Name     ||Description
+|`total`        |optional|
+Total number of current tasks.
+|`running`      |optional|
+Number of currently running tasks.
+|`ready`        |optional|
+Number of currently ready tasks.
+|`sleeping`     |optional|
+Number of currently sleeping tasks.
+|============================
+
+[[thread-summary-info]]
+=== ThreadSummaryInfo
+The `ThreadSummaryInfo` entity contains information about the current
+threads.
+
+[options="header",width="50%",cols="1,6"]
+|===========================
+|Field Name     |Description
+|`cpus`         |
+The number of available processors.
+|`threads`      |
+The total number of current threads.
+|`counts`       |
+Detailed thread counts as a map that maps a thread kind to a map that
+maps a thread state to the thread count. The thread kinds group the
+counts by threads that have the same name prefix (`HTTP`,
+`IntraLineDiff`, `ReceiveCommits`, `SSH git-receive-pack`,
+`SSH git-upload-pack`, `SSH-Interactive-Worker`, `SSH-Stream-Worker`,
+`SshCommandStart`). The counts for other threads are available under
+the thread kind `Other`. Counts for the following thread states can be
+included: `NEW`, `RUNNABLE`, `BLOCKED`, `WAITING`, `TIMED_WAITING` and
+`TERMINATED`.
+|===========================
+
 [[top-menu-entry-info]]
 === TopMenuEntryInfo
 The `TopMenuEntryInfo` entity contains information about a top menu
diff --git a/Documentation/rest-api-groups.txt b/Documentation/rest-api-groups.txt
index 5ca3039..afdcffd 100644
--- a/Documentation/rest-api-groups.txt
+++ b/Documentation/rest-api-groups.txt
@@ -35,7 +35,6 @@
   )]}'
   {
     "Administrators": {
-      "kind": "gerritcodereview#group",
       "id": "6a1e70e1a88782771a91808c8af9bbb7a9871389",
       "url": "#/admin/groups/uuid-6a1e70e1a88782771a91808c8af9bbb7a9871389",
       "options": {
@@ -46,7 +45,6 @@
       "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389"
     },
     "Anonymous Users": {
-      "kind": "gerritcodereview#group",
       "id": "global%3AAnonymous-Users",
       "url": "#/admin/groups/uuid-global%3AAnonymous-Users",
       "options": {
@@ -57,7 +55,6 @@
       "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389"
     },
     "MyProject_Committers": {
-      "kind": "gerritcodereview#group",
       "id": "834ec36dd5e0ed21a2ff5d7e2255da082d63bbd7",
       "url": "#/admin/groups/uuid-834ec36dd5e0ed21a2ff5d7e2255da082d63bbd7",
       "options": {
@@ -68,7 +65,6 @@
       "owner_id": "834ec36dd5e0ed21a2ff5d7e2255da082d63bbd7"
     },
     "Non-Interactive Users": {
-      "kind": "gerritcodereview#group",
       "id": "5057f3cbd3519d6ab69364429a89ffdffba50f73",
       "url": "#/admin/groups/uuid-5057f3cbd3519d6ab69364429a89ffdffba50f73",
       "options": {
@@ -79,7 +75,6 @@
       "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389"
     },
     "Project Owners": {
-      "kind": "gerritcodereview#group",
       "id": "global%3AProject-Owners",
       "url": "#/admin/groups/uuid-global%3AProject-Owners",
       "options": {
@@ -90,7 +85,6 @@
       "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389"
     },
     "Registered Users": {
-      "kind": "gerritcodereview#group",
       "id": "global%3ARegistered-Users",
       "url": "#/admin/groups/uuid-global%3ARegistered-Users",
       "options": {
@@ -147,7 +141,6 @@
   )]}'
   {
     "MyProject-Committers": {
-      "kind": "gerritcodereview#group",
       "id": "9999c971bb4ab872aab759d8c49833ee6b9ff320",
       "url": "#/admin/groups/uuid-9999c971bb4ab872aab759d8c49833ee6b9ff320",
       "options": {
@@ -203,7 +196,6 @@
 
   )]}'
   {
-    "kind": "gerritcodereview#group",
     "id": "6a1e70e1a88782771a91808c8af9bbb7a9871389",
     "name": "Administrators",
     "url": "#/admin/groups/uuid-6a1e70e1a88782771a91808c8af9bbb7a9871389",
@@ -251,7 +243,6 @@
 
   )]}'
   {
-    "kind": "gerritcodereview#group",
     "id": "9999c971bb4ab872aab759d8c49833ee6b9ff320",
     "name": "MyProject-Committers",
     "url": "#/admin/groups/uuid-9999c971bb4ab872aab759d8c49833ee6b9ff320",
@@ -293,7 +284,6 @@
 
   )]}'
   {
-    "kind": "gerritcodereview#group",
     "id": "6a1e70e1a88782771a91808c8af9bbb7a9871389",
     "name": "Administrators",
     "url": "#/admin/groups/uuid-6a1e70e1a88782771a91808c8af9bbb7a9871389",
@@ -544,7 +534,6 @@
 
   )]}'
   {
-    "kind": "gerritcodereview#group",
     "id": "6a1e70e1a88782771a91808c8af9bbb7a9871389",
     "name": "Administrators",
     "url": "#/admin/groups/uuid-6a1e70e1a88782771a91808c8af9bbb7a9871389",
@@ -591,7 +580,6 @@
 
   )]}'
   {
-    "kind": "gerritcodereview#group",
     "id": "6a1e70e1a88782771a91808c8af9bbb7a9871389",
     "name": "Administrators",
     "url": "#/admin/groups/uuid-6a1e70e1a88782771a91808c8af9bbb7a9871389",
@@ -892,7 +880,6 @@
   )]}'
   [
     {
-      "kind": "gerritcodereview#group",
       "id": "7ca042f4d5847936fcb90ca91057673157fd06fc",
       "name": "MyProject-Verifiers",
       "url": "#/admin/groups/uuid-7ca042f4d5847936fcb90ca91057673157fd06fc",
@@ -929,7 +916,6 @@
 
   )]}'
   {
-    "kind": "gerritcodereview#group",
     "id": "7ca042f4d5847936fcb90ca91057673157fd06fc",
     "name": "MyProject-Verifiers",
     "url": "#/admin/groups/uuid-7ca042f4d5847936fcb90ca91057673157fd06fc",
@@ -966,7 +952,6 @@
 
   )]}'
   {
-    "kind": "gerritcodereview#group",
     "id": "6a1e70e1a88782771a91808c8af9bbb7a9871389",
     "name": "MyGroup",
     "url": "#/admin/groups/uuid-6a1e70e1a88782771a91808c8af9bbb7a9871389",
@@ -1027,7 +1012,6 @@
   )]}'
   [
     {
-      "kind": "gerritcodereview#group",
       "id": "6a1e70e1a88782771a91808c8af9bbb7a9871389",
       "name": "MyGroup",
       "url": "#/admin/groups/uuid-6a1e70e1a88782771a91808c8af9bbb7a9871389",
@@ -1038,7 +1022,6 @@
       "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389"
     },
     {
-      "kind": "gerritcodereview#group",
       "id": "5057f3cbd3519d6ab69364429a89ffdffba50f73",
       "name": "MyOtherGroup",
       "url": "#/admin/groups/uuid-5057f3cbd3519d6ab69364429a89ffdffba50f73",
@@ -1133,7 +1116,6 @@
 [options="header",width="50%",cols="1,^1,5"]
 |===========================
 |Field Name    ||Description
-|`kind`        ||`gerritcodereview#group`
 |`id`          ||The URL encoded UUID of the group.
 |`name`        |
 not set if returned in a map where the group name is used as map key|
diff --git a/Documentation/rest-api-plugins.txt b/Documentation/rest-api-plugins.txt
index 0011213..b8f7d36 100644
--- a/Documentation/rest-api-plugins.txt
+++ b/Documentation/rest-api-plugins.txt
@@ -45,13 +45,11 @@
   )]}'
   {
     "delete-project": {
-      "kind": "gerritcodereview#plugin",
       "id": "delete-project",
       "index_url": "plugins/delete-project/",
       "version": "2.9-SNAPSHOT"
     },
     "reviewers-by-blame": {
-      "kind": "gerritcodereview#plugin",
       "id": "reviewers-by-blame",
       "index_url": "plugins/reviewers-by-blame/",
       "version": "2.9-SNAPSHOT",
@@ -103,7 +101,6 @@
 
   )]}'
   {
-    "kind": "gerritcodereview#plugin",
     "id": "delete-project",
     "version": "2.8"
   }
@@ -135,7 +132,6 @@
 
   )]}'
   {
-    "kind": "gerritcodereview#plugin",
     "id": "delete-project",
     "version": "2.8"
   }
@@ -165,7 +161,6 @@
 
   )]}'
   {
-    "kind": "gerritcodereview#plugin",
     "id": "delete-project",
     "version": "2.8"
   }
@@ -201,7 +196,6 @@
 
   )]}'
   {
-    "kind": "gerritcodereview#plugin",
     "id": "delete-project",
     "version": "2.8",
     "disabled": true
@@ -232,7 +226,6 @@
 
   )]}'
   {
-    "kind": "gerritcodereview#plugin",
     "id": "delete-project",
     "version": "2.8",
     "disabled": true
@@ -257,7 +250,6 @@
 [options="header",width="50%",cols="1,^2,4"]
 |=======================
 |Field Name ||Description
-|`kind`     ||`gerritcodereview#plugin`
 |`id`       ||The ID of the plugin.
 |`version`  ||The version of the plugin.
 |`index_url`|optional|URL of the plugin's default page.
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 39eabdf..2cc1e9a 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -35,43 +35,127 @@
   )]}'
   {
     "external/bison": {
-      "kind": "gerritcodereview#project",
       "id": "external%2Fbison",
       "description": "GNU parser generator"
     },
     "external/gcc": {
-      "kind": "gerritcodereview#project",
-      "id": "external%2Fgcc",
+      "id": "external%2Fgcc"
     },
     "external/openssl": {
-      "kind": "gerritcodereview#project",
       "id": "external%2Fopenssl",
       "description": "encryption\ncrypto routines"
     },
     "test": {
-      "kind": "gerritcodereview#project",
       "id": "test",
       "description": "\u003chtml\u003e is escaped"
     }
   }
 ----
 
-.Get all projects with their description
-****
-get::/projects/?d
-****
+[[project-options]]
+==== Project Options
+
+Branch(b)::
+Limit the results to the projects having the specified branch and
+include the sha1 of the branch in the results.
++
+Get projects that have a 'master' branch:
++
+.Request
+----
+GET /projects/?b=master HTTP/1.0
+----
++
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "some-project": {
+      "id": "some-project",
+      "branches": {
+        "master": "c5ed9dfcbf002ca0e432d788dab6ca2387829ca7"
+      }
+    },
+    "some-other-project": {
+      "id": "some-other-project",
+      "branches": {
+        "master": "ef1c270142f9581ecf768f4193fc8f8a81102ec2"
+      }
+    },
+  }
+----
+
+Description(d)::
+Include project description in the results.
++
+Get all the projects with their description:
++
+.Request
+----
+GET /projects/?d HTTP/1.0
+----
++
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "some-project": {
+      "id": "some-project",
+      "description": "Description of some project."
+    },
+    "some-other-project": {
+      "id": "some-other-project",
+       "description": "Description of some other project."
+      }
+    },
+  }
+----
+
+Limit(n)::
+Limit the number of projects to be included in the results.
++
+Query the first project in the project list:
++
+.Request
+----
+  GET /projects/?n=1 HTTP/1.0
+----
++
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "some-project": {
+      "id": "some-project"
+    }
+  }
+----
+
 
 [[suggest-projects]]
-The `/projects/` URL also accepts a prefix string in the `p` parameter.
-This limits the results to those projects that start with the specified
+Prefix(p)::
+Limit the results to those projects that start with the specified
 prefix.
++
 List all projects that start with `platform/`:
-
++
 .Request
 ----
   GET /projects/?p=platform%2F HTTP/1.0
 ----
-
++
 .Response
 ----
   HTTP/1.1 200 OK
@@ -81,31 +165,157 @@
   )]}'
   {
     "platform/drivers": {
-      "kind": "gerritcodereview#project",
-      "id": "platform%2Fdrivers",
+      "id": "platform%2Fdrivers"
     },
     "platform/tools": {
-      "kind": "gerritcodereview#project",
-      "id": "platform%2Ftools",
+      "id": "platform%2Ftools"
     }
   }
 ----
++
 E.g. this feature can be used by suggestion client UI's to limit results.
 
-The `/projects/` URL also accepts a limit integer in the `n` parameter.
-This limits the results to show `n` projects.
-
-Query the first 25 projects in project list.
+Regex(r)::
+Limit the results to those projects that match the specified regex.
++
+Boundary matchers '^' and '$' are implicit. For example: the regex 'test.*' will
+match any projects that start with 'test' and regex '.*test' will match any
+project that end with 'test'.
++
+List all projects that match regex `test.*project`:
++
+.Request
 ----
-  GET /projects/?n=25 HTTP/1.0
+  GET /projects/?r=test.*project HTTP/1.0
+----
++
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "test/some-project": {
+      "id": "test%2Fsome-project"
+    },
+    "test/some-other-project": {
+      "id": "test%2Fsome-other-project"
+    }
+  }
+
 ----
 
-The `/projects/` URL also accepts a start integer in the `S` parameter.
-The results will skip `S` projects from project list.
-
-Query 25 projects starting from index 50.
+Skip(S)::
+Skip the given number of projects from the beginning of the list.
++
+Query the second project in the project list:
++
+.Request
 ----
-  GET /projects/?n=25&S=50 HTTP/1.0
+  GET /projects/?n=1&S=1 HTTP/1.0
+----
++
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "some-other-project": {
+      "id": "some-other-project"
+    }
+  }
+----
+
+Substring(m)::
+Limit the results to those projects that match the specified substring.
++
+List all projects that match substring `test/`:
++
+.Request
+----
+  GET /projects/?m=test%2F HTTP/1.0
+----
++
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "test/some-project": {
+      "id": "test%2Fsome-project"
+    },
+    "some-path/test/some-other-project": {
+      "id": "some-path%2Ftest%2Fsome-other-project"
+    }
+  }
+----
+
+Tree(t)::
+Get projects inheritance in a tree-like format. This option does
+not work together with the branch option.
++
+Get all the projects with tree option:
++
+.Request
+----
+GET /projects/?t HTTP/1.0
+----
++
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "All-Projects" {
+      "id": "All-Projects"
+    },
+    "child-project": {
+      "id": "child-project",
+      "parent":"parent-project"
+    },
+    "parent-project": {
+      "id": "parent-project",
+      "parent":"All-Projects"
+    }
+  }
+----
+
+Type(type)::
+Get projects with specified type: ALL, CODE, PERMISSIONS.
++
+Get all the projects of type 'PERMISSIONS':
++
+.Request
+----
+GET /projects/?type=PERMISSIONS HTTP/1.0
+----
++
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "All-Projects" {
+      "id": "All-Projects"
+    },
+    "some-parent-project": {
+      "id": "some-parent-project"
+    }
+  }
 ----
 
 [[get-project]]
@@ -132,11 +342,11 @@
 
   )]}'
   {
-    "kind": "gerritcodereview#project",
     "id": "plugins%2Freplication",
     "name": "plugins/replication",
     "parent": "Public-Plugins",
-    "description": "Copies to other servers using the Git protocol"
+    "description": "Copies to other servers using the Git protocol",
+    "state": "ACTIVE"
   }
 ----
 
@@ -176,7 +386,6 @@
 
   )]}'
   {
-    "kind": "gerritcodereview#project",
     "id": "MyProject",
     "name": "MyProject",
     "parent": "All-Projects",
@@ -446,7 +655,6 @@
 
   )]}'
   {
-    "kind": "gerritcodereview#project_config",
     "description": "demo project",
     "use_contributor_agreements": {
       "value": true,
@@ -535,7 +743,6 @@
 
   )]}'
   {
-    "kind": "gerritcodereview#project_config",
     "use_contributor_agreements": {
       "value": false,
       "configured_value": "FALSE",
@@ -614,6 +821,57 @@
   done.
 ----
 
+[[ban-commit]]
+=== Ban Commit
+--
+'PUT /projects/link:#project-name[\{project-name\}]/ban'
+--
+
+Marks commits as banned for the project. If a commit is banned Gerrit
+rejects every push that includes this commit with
+link:error-contains-banned-commit.html[contains banned commit ...].
+
+[NOTE]
+This REST endpoint only marks the commits as banned, but it does not
+remove the commits from the history of any central branch. This needs
+to be done manually.
+
+The commits to be banned must be specified in the request body as a
+link:#ban-input[BanInput] entity.
+
+The caller must be project owner.
+
+.Request
+----
+  PUT /projects/plugins%2Freplication/ban HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "commits": [
+      "a8a477efffbbf3b44169bb9a1d3a334cbbd9aa96",
+      "cf5b56541f84b8b57e16810b18daca9c3adc377b"
+    ],
+    "reason": "Violates IP"
+  }
+----
+
+As response a link:#ban-result-info[BanResultInfo] entity is returned.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "newly_banned": [
+      "a8a477efffbbf3b44169bb9a1d3a334cbbd9aa96",
+      "cf5b56541f84b8b57e16810b18daca9c3adc377b"
+    ]
+  }
+----
+
 [[branch-endpoints]]
 == Branch Endpoints
 
@@ -770,6 +1028,85 @@
   Ly8gQ29weXJpZ2h0IChDKSAyMDEwIFRoZSBBbmRyb2lkIE9wZW4gU291cmNlIFByb2plY...
 ----
 
+[[get-reflog]]
+=== Get Reflog
+--
+'GET /projects/link:#project-name[\{project-name\}]/branches/link:#branch-id[\{branch-id\}]/reflog'
+--
+
+Gets the reflog of a certain branch.
+
+The caller must be project owner.
+
+.Request
+----
+  GET /projects/gerrit/branches/master/reflog HTTP/1.0
+----
+
+As response a list of link:#reflog-entry-info[ReflogEntryInfo] entities
+is returned that describe the reflog entries. The reflog entries are
+returned in reverse order.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  [
+    {
+      "old_id": "976ced8f4fc0909d7e1584d18455299545881d60",
+      "new_id": "2eaa94bac536654eb592c941e33b91f925698d16",
+      "who": {
+        "name": "Jane Roe",
+        "email": "jane.roe@example.com",
+        "date": "2014-06-30 11:53:43.000000000",
+        "tz": 120
+      },
+      "comment": "merged: fast forward"
+    },
+    {
+      "old_id": "c271c6a7161b74f85560c5899c8c73ee89ca5e29",
+      "new_id": "976ced8f4fc0909d7e1584d18455299545881d60",
+      "who": {
+        "name": "John Doe",
+        "email": "john.doe@example.com",
+        "date": "2013-10-02 10:45:26.000000000",
+        "tz": 120
+      },
+      "comment": "merged: fast forward"
+    },
+    {
+      "old_id": "0000000000000000000000000000000000000000",
+      "new_id": "c271c6a7161b74f85560c5899c8c73ee89ca5e29",
+      "who": {
+        "name": "John Doe",
+        "email": "john.doe@example.com",
+        "date": "2013-09-30 19:08:44.000000000",
+        "tz": 120
+      },
+      "comment": ""
+    }
+  ]
+----
+
+The get reflog endpoint also accepts a limit integer in the `n`
+parameter. This limits the results to show the last `n` reflog entries.
+
+Query the last 25 reflog entries.
+----
+  GET /projects/gerrit/branches/master/reflog?n=25 HTTP/1.0
+----
+
+The reflog can also be filtered by timestamp by specifying the `from`
+and `to` parameters. The timestamp for `from` and `to` must be given as
+UTC in the following format: `yyyyMMdd_HHmm`.
+
+----
+  GET /projects/gerrit/branches/master/reflog?from=20130101_0000&to=20140101_0000=25 HTTP/1.0
+----
+
 [[child-project-endpoints]]
 == Child Project Endpoints
 
@@ -798,21 +1135,18 @@
   )]}'
   [
     {
-      "kind": "gerritcodereview#project",
       "id": "plugins%2Freplication",
       "name": "plugins/replication",
       "parent": "Public-Plugins",
       "description": "Copies to other servers using the Git protocol"
     },
     {
-      "kind": "gerritcodereview#project",
       "id": "plugins%2Freviewnotes",
       "name": "plugins/reviewnotes",
       "parent": "Public-Plugins",
       "description": "Annotates merged commits using notes on refs/notes/review."
     },
     {
-      "kind": "gerritcodereview#project",
       "id": "plugins%2Fsingleusergroup",
       "name": "plugins/singleusergroup",
       "parent": "Public-Plugins",
@@ -841,35 +1175,30 @@
   )]}'
   [
     {
-      "kind": "gerritcodereview#project",
       "id": "gerrit",
       "name": "gerrit",
       "parent": "Public-Projects",
       "description": "Gerrit Code Review"
     },
     {
-      "kind": "gerritcodereview#project",
       "id": "plugins%2Freplication",
       "name": "plugins/replication",
       "parent": "Public-Plugins",
       "description": "Copies to other servers using the Git protocol"
     },
     {
-      "kind": "gerritcodereview#project",
       "id": "plugins%2Freviewnotes",
       "name": "plugins/reviewnotes",
       "parent": "Public-Plugins",
       "description": "Annotates merged commits using notes on refs/notes/review."
     },
     {
-      "kind": "gerritcodereview#project",
       "id": "plugins%2Fsingleusergroup",
       "name": "plugins/singleusergroup",
       "parent": "Public-Plugins",
       "description": "GroupBackend enabling users to be directly added to access rules"
     },
     {
-      "kind": "gerritcodereview#project",
       "id": "Public-Plugins",
       "name": "Public-Plugins",
       "parent": "Public-Projects",
@@ -903,7 +1232,6 @@
 
   )]}'
   {
-    "kind": "gerritcodereview#project",
     "id": "plugins%2Freplication",
     "name": "plugins/replication",
     "parent": "Public-Plugins",
@@ -911,6 +1239,83 @@
   }
 ----
 
+[[commit-endpoints]]
+== Commit Endpoints
+
+[[get-commit]]
+=== Get Commit
+--
+'GET /projects/link:#project-name[\{project-name\}]/commits/link:#commit-id[\{commit-id\}]'
+--
+
+Retrieves a commit of a project.
+
+The commit must be visible to the caller.
+
+.Request
+----
+  GET /projects/work%2Fmy-project/commits/a8a477efffbbf3b44169bb9a1d3a334cbbd9aa96 HTTP/1.0
+----
+
+As response a link:rest-api-changes.html#commit-info[CommitInfo] entity
+is returned that describes the commit.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "commit": "184ebe53805e102605d11f6b143486d15c23a09c",
+    "parents": [
+      {
+        "commit": "1eee2c9d8f352483781e772f35dc586a69ff5646",
+        "subject": "Migrate contributor agreements to All-Projects."
+      }
+    ],
+    "author": {
+      "name": "Shawn O. Pearce",
+      "email": "sop@google.com",
+      "date": "2012-04-24 18:08:08.000000000",
+      "tz": -420
+    },
+    "committer": {
+      "name": "Shawn O. Pearce",
+      "email": "sop@google.com",
+      "date": "2012-04-24 18:08:08.000000000",
+      "tz": -420
+    },
+    "subject": "Use an EventBus to manage star icons",
+    "message": "Use an EventBus to manage star icons\n\nImage widgets that need to ..."
+  }
+----
+
+[[get-content]]
+=== Get Content
+--
+'GET /projects/link:#project-name[\{project-name\}]/commits/link:#commit-id[\{commit-id\}]/files/link:rest-api-changes.html#file-id[\{file-id\}]/content'
+--
+
+Gets the content of a file from a certain commit.
+
+.Request
+----
+  GET /projects/work%2Fmy-project/commits/a8a477efffbbf3b44169bb9a1d3a334cbbd9aa96/files/gerrit-server%2Fsrc%2Fmain%2Fjava%2Fcom%2Fgoogle%2Fgerrit%2Fserver%2Fproject%2FRefControl.java/content HTTP/1.0
+----
+
+The content is returned as base64 encoded string.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: text/plain;charset=UTF-8
+
+  Ly8gQ29weXJpZ2h0IChDKSAyMDEwIFRoZSBBbmRyb2lkIE9wZW4gU291cmNlIFByb2plY...
+----
+
 [[dashboard-endpoints]]
 == Dashboard Endpoints
 
@@ -941,7 +1346,6 @@
   )]}'
   [
     {
-      "kind": "gerritcodereview#dashboard",
       "id": "main:closed",
       "ref": "main",
       "path": "closed",
@@ -993,7 +1397,6 @@
 
   )]}'
   {
-    "kind": "gerritcodereview#dashboard",
     "id": "main:closed",
     "ref": "main",
     "path": "closed",
@@ -1030,7 +1433,6 @@
 
   )]}'
   {
-    "kind": "gerritcodereview#dashboard",
     "id": "main:closed",
     "ref": "main",
     "path": "closed",
@@ -1086,7 +1488,6 @@
 
   )]}'
   {
-    "kind": "gerritcodereview#dashboard",
     "id": "main:closed",
     "ref": "main",
     "path": "closed",
@@ -1142,6 +1543,10 @@
 The name of a branch or `HEAD`. The prefix `refs/heads/` can be
 omitted.
 
+[[commit-id]]
+=== \{commit-id\}
+Commit ID.
+
 [[dashboard-id]]
 === \{dashboard-id\}
 The ID of a dashboard in the format '<ref>:<path>'.
@@ -1170,6 +1575,30 @@
 Whether the calling user can delete this branch.
 |=========================
 
+[[ban-input]]
+=== BanInput
+The `BanInput` entity contains information for banning commits in a
+project.
+
+[options="header",width="50%",cols="1,^2,4"]
+|=======================
+|Field Name||Description
+|`commits` ||List of commits to be banned.
+|`reason`  |optional|Reason for banning the commits.
+|=======================
+
+[[ban-result-info]]
+=== BanResultInfo
+The `BanResultInfo` entity describes the result of banning commits.
+
+[options="header",width="50%",cols="1,^2,4"]
+|=============================
+|Field Name      ||Description
+|`newly_banned`  |optional|List of newly banned commits.
+|`already_banned`|optional|List of commits that were already banned.
+|`ignored`       |optional|List of object IDs that were ignored.
+|=============================
+
 [[branch-input]]
 === BranchInput
 The `BranchInput` entity contains information for the creation of
@@ -1347,7 +1776,6 @@
 [options="header",width="50%",cols="1,^2,4"]
 |===============================
 |Field Name        ||Description
-|`kind`            ||`gerritcodereview#dashboard`
 |`id`              ||
 The ID of the dashboard. The ID has the format '<ref>:<path>',
 where ref and path are URL encoded.
@@ -1490,7 +1918,6 @@
 [options="header",width="50%",cols="1,^2,4"]
 |===========================
 |Field Name    ||Description
-|`kind`        ||`gerritcodereview#project`
 |`id`          ||The URL encoded project name.
 |`name`        |
 not set if returned in a map where the project name is used as map key|
@@ -1500,7 +1927,11 @@
 `?-<n>` if the parent project is not visible (`<n>` is a number which
 is increased for each non-visible project).
 |`description` |optional|The description of the project.
+|`state`       |optional|`ACTIVE`, `READ_ONLY` or `HIDDEN`.
 |`branches`    |optional|Map of branch names to HEAD revisions.
+|'web_links'   |optional|
+Links to the project in external sites as a list of
+link:rest-api-changes.html#web-link-info[WebLinkInfo] entries.
 |===========================
 
 [[project-input]]
@@ -1574,6 +2005,21 @@
 in the `project.config` file to the `refs/meta/config` branch.
 |=============================
 
+[[reflog-entry-info]]
+=== ReflogEntryInfo
+The `ReflogEntryInfo` entity describes an entry in a reflog.
+
+[options="header",width="50%",cols="1,6"]
+|============================
+|Field Name      |Description
+|`old_id`        |The old commit ID.
+|`new_id`        |The new commit ID.
+|`who`           |
+The user performing the change as a
+link:rest-api-changes.html#git-person-info[GitPersonInfo] entity.
+|`comment`       |Comment of the reflog entry.
+|============================
+
 [[repository-statistics-info]]
 === RepositoryStatisticsInfo
 The `RepositoryStatisticsInfo` entity contains information about
diff --git a/Documentation/rest-api.txt b/Documentation/rest-api.txt
index 6c681e5..b228dda 100644
--- a/Documentation/rest-api.txt
+++ b/Documentation/rest-api.txt
@@ -32,11 +32,14 @@
 results to correspond to what anonymous users can read (which may
 be nothing at all).
 
-Users (and programs) may authenticate using HTTP authentication by
-supplying the HTTP password from the user's account settings page.
-Gerrit by default uses HTTP digest authentication. To authenticate,
-prefix the endpoint URL with `/a/`. For example to authenticate to
-`/projects/` request URL `/a/projects/`.
+Users (and programs) may authenticate by prefixing the endpoint URL with
+`/a/`. For example to authenticate to `/projects/`, request the URL
+`/a/projects/`.
+
+By default Gerrit uses HTTP digest authentication with the HTTP password
+from the user's account settings page. HTTP basic authentication is used
+if link:config-gerrit.html#auth.gitBasicAuth[`auth.gitBasicAuth`] is set
+to true in the Gerrit configuration.
 
 [[preconditions]]
 === Preconditions
@@ -47,17 +50,22 @@
 
 [[output]]
 === Output Format
-Most APIs return pretty printed JSON by default. Compact JSON can be
-requested by setting the `Accept` HTTP request header to include
-`application/json`, for example:
+JSON responses are encoded using UTF-8 and use content type
+`application/json`.
+
+By default most APIs return pretty-printed JSON, which uses extra
+whitespace to make the output more readable for humans.
+
+Compact JSON can be requested by setting the `pp=0` query parameter,
+or by setting the `Accept` HTTP request header to include `application/json`:
 
 ----
   GET /projects/ HTTP/1.0
   Accept: application/json
 ----
 
-JSON responses are encoded using UTF-8 and use content type
-`application/json`.
+Producing (and parsing) the non-pretty compact format is more efficient,
+so tools should request it whenever possible.
 
 To prevent against Cross Site Script Inclusion (XSSI) attacks, the JSON
 response body starts with a magic prefix line that must be stripped before
@@ -68,12 +76,6 @@
   [ ... valid JSON ... ]
 ----
 
-The default JSON format is pretty, which uses extra whitespace to make
-the output more readable for a human. Producing (and parsing) the
-non-pretty compact format is more efficient so tools should request it
-by using the `Accept: application/json` header or `pp=0` query
-parameter whenever possible.
-
 Responses will be gzip compressed by the server if the HTTP
 `Accept-Encoding` request header is set to `gzip`. This may
 save on network transfer time for larger responses.
@@ -81,7 +83,7 @@
 [[timestamp]]
 === Timestamp
 Timestamps are given in UTC and have the format
-"'yyyy-mm-dd hh:mm:ss.fffffffff'" where "'ffffffffff'" indicates the
+"'yyyy-mm-dd hh:mm:ss.fffffffff'" where "'ffffffffff'" represents
 nanoseconds.
 
 [[encoding]]
@@ -91,32 +93,33 @@
 
 [[response-codes]]
 === Response Codes
-HTTP status codes are well defined and the Gerrit REST endpoints use
-them as described in the HTTP spec.
+The Gerrit REST endpoints use HTTP status codes as described
+in the link:http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html[
+HTTP specification].
 
-Here are examples for some HTTP status codes that show how they are
-used in the context of the Gerrit REST API.
+Here are examples that show how HTTP status codes are used in the
+context of the Gerrit REST API.
 
 ==== 400 Bad Request
-`400 Bad Request` is used if the request is not understood by the
+`400 Bad Request` is returned if the request is not understood by the
 server due to malformed syntax.
 
 E.g. `400 Bad Request` is returned if JSON input is expected but the
 'Content-Type' of the request is not 'application/json' or the request
 body doesn't contain valid JSON.
 
-`400 Bad Request` is also used if required input fields are not set or
+`400 Bad Request` is also returned if required input fields are not set or
 if options are set which cannot be used together.
 
 ==== 403 Forbidden
-`403 Forbidden` is used if the operation is not allowed because the
-calling user has no sufficient permissions.
+`403 Forbidden` is returned if the operation is not allowed because the
+calling user does not have sufficient permissions.
 
 E.g. some REST endpoints require that the calling user has certain
 link:access-control.html#global_capabilities[global capabilities]
 assigned.
 
-`403 Forbidden` is also used if `self` is used as account ID and the
+`403 Forbidden` is also returned if `self` is used as account ID and the
 REST call was done without authentication.
 
 ==== 404 Not Found
@@ -125,27 +128,27 @@
 cannot be found if the URL contains a non-existing ID or view.
 
 ==== 405 Method Not Allowed
-`405 Method Not Allowed` is used if the resource exists but doesn't
+`405 Method Not Allowed` is returned if the resource exists but doesn't
 support the operation.
 
 E.g. some of the `/groups/` endpoints are only supported for Gerrit
-internal groups, if they are invoked for an external group the response
+internal groups; if they are invoked for an external group the response
 is `405 Method Not Allowed`.
 
 ==== 409 Conflict
-`409 Conflict` is used if the request cannot be completed because the
+`409 Conflict` is returned if the request cannot be completed because the
 current state of the resource doesn't allow the operation.
 
 E.g. if you try to submit a change that is abandoned, this fails with
 `409 Conflict` because the state of the change doesn't allow the submit
 operation.
 
-`409 Conflict` is also used if you try to create a resource but the
+`409 Conflict` is also returned if you try to create a resource but the
 name is already occupied by an existing resource.
 
 ==== 412 Precondition Failed
-`412 Precondition Failed` is used if a precondition from the request
-header fields is not fulfilled as described in the link:#preconditions[
+`412 Precondition Failed` is returned if a precondition from the request
+header fields is not fulfilled, as described in the link:#preconditions[
 Preconditions] section.
 
 ==== 422 Unprocessable Entity
diff --git a/Documentation/user-review-ui.txt b/Documentation/user-review-ui.txt
index 5bc5b44..bb1aeef 100644
--- a/Documentation/user-review-ui.txt
+++ b/Documentation/user-review-ui.txt
@@ -1014,7 +1014,8 @@
 Controls whether syntax highlighting should be enabled.
 +
 The language for the syntax highlighting is automatically detected from
-the file extension.
+the file extension. The language can also be set manually by selecting
+it from the `Language` drop-down list.
 +
 image::images/user-review-ui-side-by-side-diff-screen-syntax-coloring.png[width=800, link="images/user-review-ui-side-by-side-diff-screen-syntax-coloring.png"]
 
@@ -1030,6 +1031,17 @@
 +
 Controls whether line numbers are shown.
 
+- `Empty Pane`:
++
+Controls whether empty panes are shown or not. The Left pane is empty when a
+file was added; the right pane is empty when a file was deleted.
+
+- `Left Side`:
++
+Controls whether the left side is shown. This preference is not
+persistent and is ignored by the `Save` button. Every time a
+patch diff is opened, this preference is reset to `Show`.
+
 - `Top Menu`:
 +
 Controls whether the top menu is shown.
@@ -1058,6 +1070,8 @@
 DOM tree, which makes the rendering slow for large files. The advantage
 of this setting is that `Ctrl+F` can be used to search in the complete
 file.
++
+Large files that exceed 4000 lines will not be fully rendered.
 
 [[keyboard-shortcuts]]
 == Keyboard Shortcuts
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 16dab4e..eaf0f6e 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -43,11 +43,7 @@
 Operators act as restrictions on the search.  As more operators
 are added to the same query string, they further restrict the
 returned results. Search can also be performed by typing only a
-text with no operator. It will try to match a project name by
-substring.
-
-E.g. Searching for just "gerrit is:starred" will try to match a
-project name by "gerrit" as substring.
+text with no operator, which will match against a variety of fields.
 
 [[age]]
 age:'AGE'::
@@ -127,6 +123,11 @@
 link:http://www.brics.dk/automaton/[dk.brics.automaton
 library] is used for evaluation of such patterns.
 
+[[projects]]
+projects:'PREFIX'::
++
+Changes occurring in projects starting with 'PREFIX'.
+
 [[parentproject]]
 parentproject:'PROJECT'::
 +
@@ -264,7 +265,7 @@
 True on any change where the current user is a reviewer.
 Same as `reviewer:self`.
 
-is:open::
+is:open, is:pending::
 +
 True if the change is either open or submitted, merge pending.
 
@@ -287,7 +288,7 @@
 destination branch.
 
 [[status]]
-status:open::
+status:open, status:pending::
 +
 True if the change state is either 'review in progress' or 'submitted,
 merge pending'.
@@ -313,6 +314,18 @@
 +
 Change has been abandoned.
 
+[[size]]
+added:'RELATION''LINES', deleted:'RELATION''LINES', delta/size:'RELATION''LINES'::
++
+True if the number of lines added/deleted/changed satisfies the given relation
+for the given number of lines.
++
+For example, added:>50 will be true for any change which adds at least 50
+lines.
++
+Valid relations are >=, >, <=, <, or no relation, which will match if the
+number of lines is exactly equal.
+
 
 == Argument Quoting
 
diff --git a/Documentation/user-upload.txt b/Documentation/user-upload.txt
index 17ca968..5c54259 100644
--- a/Documentation/user-upload.txt
+++ b/Documentation/user-upload.txt
@@ -20,7 +20,8 @@
 
 When link:config-gerrit.html#auth.gitBasicAuth[gitBasicAuth] is enabled,
 the user is authenticated using standard BasicAuth and credentials validated
-using the same authentication method configured for the Gerrit Web UI.
+using the randomly generated HTTP password on the `HTTP Password` tab
+in the user settings page or against LDAP when configured for the Gerrit Web UI.
 
 When gitBasicAuth is not configured, the user's HTTP credentials can be
 accessed within Gerrit by going to `Settings`, and then accessing the `HTTP
@@ -154,6 +155,20 @@
   git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/experimental%topic=driver/i42
 ====
 
+[[review_labels]]
+Review labels can be applied to the change by using the -l option
+in the reference:
+
+====
+  git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/experimental%l=Verified+1
+====
+
+The `l='label[score]'` option may be specified more than once to
+apply multiple review labels.
+
+The value is optional.  If not specified, it defaults to +1 (if
+the label range allows it).
+
 If you are frequently uploading changes to the same Gerrit server,
 consider adding an SSH host block in `~/.ssh/config` to remember
 your username, hostname and port number.  This permits the use of
diff --git a/ReleaseNotes/Makefile b/ReleaseNotes/Makefile
index 5137b59..3081600 100644
--- a/ReleaseNotes/Makefile
+++ b/ReleaseNotes/Makefile
@@ -14,39 +14,15 @@
 
 ASCIIDOC       ?= asciidoc
 ASCIIDOC_EXTRA ?=
-SVN            ?= svn
-PUB_ROOT       ?= https://gerrit-documentation.googlecode.com/svn/ReleaseNotes
 
 DOC_HTML      := $(patsubst %.txt,%.html,$(wildcard ReleaseNotes*.txt))
-COMMIT        := $(shell git describe HEAD | sed s/^v//)
-LOCAL_ROOT    := .published
-PUB_DIR       := $(PUB_ROOT)
 
 all: html
 
 html: index.html $(DOC_HTML)
 
-update: html
-	@-rm -rf $(LOCAL_ROOT)
-	@echo "Checking out current release notes"
-	@$(SVN) checkout $(PUB_DIR) $(LOCAL_ROOT)
-	@rm -f $(LOCAL_ROOT)/*.html
-	@cp *.html $(LOCAL_ROOT)
-	@cd $(LOCAL_ROOT) && \
-	  r=`$(SVN) status | perl -ne 'print if s/^!  *//' ` && \
-	  if [ -n "$$r" ]; then $(SVN) rm $$r; fi && \
-	  a=`$(SVN) status | perl -ne 'print if s/^\?  *//' ` && \
-	  if [ -n "$$a" ]; then \
-	    $(SVN) add $$a && \
-	    $(SVN) propset svn:mime-type text/html $$a ; \
-	    fi && \
-	  echo "Committing release notes at v$(COMMIT)" && \
-	  $(SVN) commit -m "Updated release notes to v$(COMMIT)"
-	@-rm -rf $(LOCAL_ROOT)
-
 clean:
 	rm -f *.html
-	rm -rf $(LOCAL_ROOT)
 
 index.html: index.txt
 	@echo FORMAT $@
@@ -61,9 +37,8 @@
 	@echo FORMAT $@
 	@rm -f $@+ $@
 	@v=$$(echo $< | sed 's/^ReleaseNotes-//;s/.txt$$//;') && \
-	 c=$$(git rev-list -1 HEAD -- $<) && \
-	 n=$$(git describe $$c) && \
-	 if [ "X$$n" != "Xv$$v" ]; then v="$$v (from $$n)"; fi && \
+	 n=$$(git describe HEAD) && \
+	 if ! git diff-index --quiet v$$v -- $< 2>/dev/null; then v="$$v (from $$n)"; fi && \
 	 $(ASCIIDOC) --unsafe \
 		-a toc \
 		-a "revision=$$v" \
diff --git a/ReleaseNotes/ReleaseNotes-2.10.1.txt b/ReleaseNotes/ReleaseNotes-2.10.1.txt
new file mode 100644
index 0000000..df70b64
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.10.1.txt
@@ -0,0 +1,42 @@
+Release notes for Gerrit 2.10.1
+===============================
+
+There are no schema changes from link:ReleaseNotes-2.10.html[2.10].
+
+Download:
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.10.1.war[
+https://gerrit-releases.storage.googleapis.com/gerrit-2.10.1.war]
+
+Bug Fixes
+---------
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2260[Issue 2260]:
+LDAP horrendous login time due to recursive lookup.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3210[Issue 3210]:
+Null Pointer Exception for query command with --comments switch.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3211[Issue 3211]:
+Intermittent Null Pointer Exception when showing process queue.
+
+LDAP
+----
+
+* Several performance improvements when using LDAP, both in the number of LDAP
+requests and in the amount of data transferred.
+
+* Sites using LDAP for authentication but otherwise rely on local Gerrit groups
+should set the new `ldap.fetchMemberOfEagerly` option to `false`.
+
+OAuth
+-----
+
+* Expose extension point for generic OAuth providers.
+
+OpenID
+------
+
+* Add support for Launchpad on the login form.
+
+* Remove pre-configured Google OpenID 2.0 provider from the login form, that is
+going to be shut down on 20, April 2015.
diff --git a/ReleaseNotes/ReleaseNotes-2.10.2.txt b/ReleaseNotes/ReleaseNotes-2.10.2.txt
new file mode 100644
index 0000000..2ca5505
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.10.2.txt
@@ -0,0 +1,31 @@
+Release notes for Gerrit 2.10.2
+===============================
+
+There are no schema changes from link:ReleaseNotes-2.10.1.html[2.10.1].
+
+Download:
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.10.2.war[
+https://gerrit-releases.storage.googleapis.com/gerrit-2.10.2.war]
+
+Bug Fixes
+---------
+
+* Work around MyersDiff infinite loop in PatchListLoader. If the MyersDiff diff
+doesn't finish within 5 seconds, interrupt it and fall back to a different diff
+algorithm. From the user perspective, the only difference when the infinite
+loop is detected is that the files in the commit will not be compared in-depth,
+which will result in bigger edit regions.
+
+Secondary Index
+---------------
+
+* Online reindexing: log the number of done/failed changes in the error_log.
+Administrators can use the logged information to decide whether to activate the
+new index version or not.
+
+Gitweb
+------
+
+* Do not return `Forbidden` when clicking on Gitweb breadcrumb. Now when the
+user clicks on the parent folder, redirect to Gerrit projects list screen with
+the parent folder path as the filter.
diff --git a/ReleaseNotes/ReleaseNotes-2.10.3.1.txt b/ReleaseNotes/ReleaseNotes-2.10.3.1.txt
new file mode 100644
index 0000000..2b90a6d
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.10.3.1.txt
@@ -0,0 +1,11 @@
+Release notes for Gerrit 2.10.3.1
+=================================
+
+There are no schema changes from link:ReleaseNotes-2.10.3.html[2.10.3].
+
+Download:
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.10.3.1.war[
+https://gerrit-releases.storage.googleapis.com/gerrit-2.10.3.1.war]
+
+The 2.10.3 release packaged wrong version of the core plugins due to a bug
+in our buck build scripts. This version fixes this issue.
diff --git a/ReleaseNotes/ReleaseNotes-2.10.3.txt b/ReleaseNotes/ReleaseNotes-2.10.3.txt
new file mode 100644
index 0000000..052840d
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.10.3.txt
@@ -0,0 +1,124 @@
+Release notes for Gerrit 2.10.3
+===============================
+
+Download:
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.10.3.war[
+https://gerrit-releases.storage.googleapis.com/gerrit-2.10.3.war]
+
+Important Notes
+---------------
+
+*WARNING:* There are no schema changes from
+link:ReleaseNotes-2.10.2.html[2.10.2], but Bouncycastle was upgraded to 1.51.
+It is therefore important to upgrade the site with the `init` program, rather
+than only copying the .war file over the existing one.
+
+*WARNING:* When upgrading from version 2.8.4 or older with a site that uses
+Bouncy Castle Crypto, new versions of the libraries will be downloaded. The old
+libraries should be manually removed from site's `lib` folder to prevent the
+startup failure described in
+link:https://code.google.com/p/gerrit/issues/detail?id=3084[Issue 3084].
+
+It is recommended to run the `init` program in interactive mode. Warnings will
+be suppressed in batch mode.
+
+----
+  java -jar gerrit.war init -d site_path
+----
+
+New Features
+------------
+
+* Support hybrid OpenID and OAuth2 authentication
++
+OpenID auth scheme is aware of optional OAuth2 plugin-based authentication.
+This feature is considered to be experimental and hasn't reached full feature set yet.
+Particularly, linking of user identities across protocol boundaries and even from
+one OAuth2 identity to another OAuth2 identity wasn't implemented yet.
+
+Configuration
+~~~~~~~~~~~~~
+
+* Allow to configure
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10.3/config-gerrit.html#sshd.rekeyBytesLimit[
+SSHD rekey parameters].
+
+SSH
+---
+
+* Update SSHD to 0.14.0.
++
+This fixes link:https://issues.apache.org/jira/browse/SSHD-348[SSHD-348] which
+was causing ssh threads allocated to stream-events clients to get stuck.
++
+Also update SSHD Mina to 2.0.8 and Bouncycastle to 1.51.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2797[Issue 2797]:
+Add support for ECDSA based public key authentication.
+
+Bug Fixes
+---------
+
+* Prevent wrong content type for CSS files.
++
+The mime-util library contains two content type mappings for .css files:
+`application/x-pointplus` and `text/css`.  Unfortunately, using the wrong one
+will result in most browsers discarding the file as a CSS file.  Ensure we only
+use the correct type for CSS files.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3289[Issue 3289]:
+Prevent NullPointerException in Gitweb servlet.
+
+Replication plugin
+~~~~~~~~~~~~~~~~~~
+
+* Set connection timeout to 120 seconds for SSH remote operations.
++
+The creation of a missing Git, before starting replication, is a blocking
+operation. By setting a timeout, we ensure the operation does not get stuck
+forever, essentially blocking all future remote git creation operations.
+
+OAuth extension point
+~~~~~~~~~~~~~~~~~~~~~
+
+* Respect servlet context path in URL for login token
++
+On sites with non empty context path, first redirect was broken and ended up
+with 404 Not found.
+
+* Invalidate OAuth session after web_sessions cache expiration
++
+After web session cache expiration there is no way to re-sign-in into Gerrit.
+
+Daemon
+~~~~~~
+
+* Print proper names for tasks in output of `show-queue` command.
++
+Some tasks were not displayed with the proper name.
+
+Web UI
+~~~~~~
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=3044[Issue 3044]:
+Remove stripping `#` in login redirect.
+
+SSH
+~~~
+
+* Prevent double authentication for the same public key.
+
+
+Performance
+-----------
+
+* Improved performance when creating a new branch on a repository with a large
+number of changes.
+
+
+Upgrades
+--------
+
+* Update Bouncycastle to 1.51.
+
+* Update SSHD to 0.14.0.
diff --git a/ReleaseNotes/ReleaseNotes-2.10.4.txt b/ReleaseNotes/ReleaseNotes-2.10.4.txt
new file mode 100644
index 0000000..e221549
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.10.4.txt
@@ -0,0 +1,49 @@
+Release notes for Gerrit 2.10.4
+===============================
+
+There are no schema changes from link:ReleaseNotes-2.10.3.1.html[2.10.3.1].
+
+Download:
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.10.4.war[
+https://gerrit-releases.storage.googleapis.com/gerrit-2.10.4.war]
+
+New Features
+------------
+
+* Support identity linking in hybrid OpenID and OAuth2 authentication.
++
+Linking of user identities across protocol boundaries and from one OAuth2
+identity to another OAuth2 identity is supported.
+
+* Support identity linking in OAuth2 extension point.
++
+Linking of user identities from one OAuth2 identity to another OAuth2
+identity is supported.
+
+Bug Fixes
+---------
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3300[Issue 3300]:
+Fix >10x performance degradation for Git push and replication operations.
++
+A link:https://bugs.eclipse.org/bugs/show_bug.cgi?id=465509[regression in jgit]
+caused a performance degradation.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3312[Issue 3312]:
+Flush padding on patches downloaded as base64.
++
+The padding was not flushed, which caused the downloaded patch to not be
+valid base64.
+
+OAuth extension point
+~~~~~~~~~~~~~~~~~~~~~
+
+* Check for session validity during logout.
++
+When user was trying to log out, after Gerrit restart, the session was
+invalidated and IllegalStateException was recorded in the error_log.
+
+Updates
+-------
+
+* Update jgit to 4.0.0.201505050340-m2.
diff --git a/ReleaseNotes/ReleaseNotes-2.10.5.txt b/ReleaseNotes/ReleaseNotes-2.10.5.txt
new file mode 100644
index 0000000..eb48c31
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.10.5.txt
@@ -0,0 +1,27 @@
+Release notes for Gerrit 2.10.5
+===============================
+
+There are no schema changes from link:ReleaseNotes-2.10.4.html[2.10.4].
+
+Download:
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.10.5.war[
+https://gerrit-releases.storage.googleapis.com/gerrit-2.10.5.war]
+
+Bug Fixes
+---------
+
+* Update JGit to include a memory leak fix as discussed
+link:https://groups.google.com/forum/#!topic/repo-discuss/RRQT_xCqz4o[here]
+
+* Attempt to fix the "Cannot read project" issue in Gerrit, as discussed
+link:https://groups.google.com/forum/\#!topic/repo-discuss/ZeGWPyyJlrM[here]
+and
+link:https://groups.google.com/forum/#!topic/repo-discuss/CYYoHfDxCfA[here]
+
+* Fixed a regression caused by the defaultValue feature which broke the ability
+to remove labels in subprojects
+
+Updates
+-------
+
+* Update JGit to v4.0.0.201506090130-r
diff --git a/ReleaseNotes/ReleaseNotes-2.10.6.txt b/ReleaseNotes/ReleaseNotes-2.10.6.txt
new file mode 100644
index 0000000..94a95bd
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.10.6.txt
@@ -0,0 +1,14 @@
+Release notes for Gerrit 2.10.6
+===============================
+
+There are no schema changes from link:ReleaseNotes-2.10.5.html[2.10.5].
+
+Download:
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.10.6.war[
+https://gerrit-releases.storage.googleapis.com/gerrit-2.10.6.war]
+
+Bug Fixes
+---------
+
+* Fix generation of licenses in documentation.
+
diff --git a/ReleaseNotes/ReleaseNotes-2.10.7.txt b/ReleaseNotes/ReleaseNotes-2.10.7.txt
new file mode 100644
index 0000000..28cf37b
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.10.7.txt
@@ -0,0 +1,19 @@
+Release notes for Gerrit 2.10.7
+===============================
+
+There are no schema changes from link:ReleaseNotes-2.10.6.html[2.10.6].
+
+Download:
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.10.7.war[
+https://gerrit-releases.storage.googleapis.com/gerrit-2.10.7.war]
+
+Bug Fixes
+---------
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3361[Issue 3361]:
+Synchronize Myers diff and Histogram diff invocations to prevent pack file
+corruption.
++
+See also the link:https://bugs.eclipse.org/bugs/show_bug.cgi?id=467467[
+bug report on JGit].
+
diff --git a/ReleaseNotes/ReleaseNotes-2.10.8.txt b/ReleaseNotes/ReleaseNotes-2.10.8.txt
new file mode 100644
index 0000000..e7de0e1
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.10.8.txt
@@ -0,0 +1,39 @@
+Release notes for Gerrit 2.10.8
+===============================
+
+There are no schema changes from link:ReleaseNotes-2.10.7.html[2.10.7].
+
+Download:
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.10.8.war[
+https://gerrit-releases.storage.googleapis.com/gerrit-2.10.8.war]
+
+Bug Fixes
+---------
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=10262[Issue 10262]: Fix validation of wants in git-upload-pack for protocol v0 stateless transports.
++
+See the following section for details.
+
+* Upgrade JGit to 4.5.5.201812240535-r.
++
+This upgrade includes several major versions since 4.0.0 used in Gerrit version 2.10.7.
+Important fixes are summarized below. Please refer to the corresponding JGit release notes for full details.
+
+** link:https://projects.eclipse.org/projects/technology.jgit/releases/4.5.5[JGit 4.5.5]: link:https://bugs.chromium.org/p/gerrit/issues/detail?id=10262[Issue 10262]: Fix validation of wants in git-upload-pack for protocol v0 stateless transports.
++
+AdvertiseRefsHook was not called for git-upload-pack in protocol v0 stateless transports, meaning that wants were not validated and a user could fetch anything that is pointed to by any ref (using fetch-by-sha1), as long as they could guess the object name.
+
+** link:https://projects.eclipse.org/projects/technology.jgit/releases/4.5.4[JGit 4.5.4]: Fix LockFile semantics when running on NFS.
++
+Honor trustFolderStats also when reading packed-refs.
+
+** link:https://projects.eclipse.org/projects/technology.jgit/releases/4.5.3[JGit 4.5.3]: Fix exception handling for opening bitmap index files.
+
+** link:https://projects.eclipse.org/projects/technology.jgit/releases/4.5.2[JGit 4.5.2]: Fix pack marked as corrupted even if it isn’t.
+
+** link:https://projects.eclipse.org/projects/technology.jgit/releases/4.5.1[JGit 4.5.1]: Don’t remove Pack when FileNotFoundException is transient.
+
+** link:https://projects.eclipse.org/projects/technology.jgit/releases/4.1.0[JGit 4.1.0]: Handle stale NFS file handles on packed-refs file.
++
+Use java.io.File instead of NIO to check existence of loose objects in ObjectDirectory to speed up inserting of loose objects.
+Reduce memory consumption when creating bitmaps during writing pack files.
\ No newline at end of file
diff --git a/ReleaseNotes/ReleaseNotes-2.10.txt b/ReleaseNotes/ReleaseNotes-2.10.txt
new file mode 100644
index 0000000..336f02f3
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.10.txt
@@ -0,0 +1,715 @@
+Release notes for Gerrit 2.10
+=============================
+
+
+Gerrit 2.10 is now available:
+
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.10.war[
+https://gerrit-releases.storage.googleapis.com/gerrit-2.10.war]
+
+Gerrit 2.10 includes the bug fixes done with
+link:ReleaseNotes-2.9.1.html[Gerrit 2.9.1],
+link:ReleaseNotes-2.9.2.html[Gerrit 2.9.2],
+link:ReleaseNotes-2.9.3.html[Gerrit 2.9.3] and
+link:ReleaseNotes-2.9.4.html[Gerrit 2.9.4].
+These bug fixes are *not* listed in these release notes.
+
+Important Notes
+---------------
+
+
+*WARNING:* This release contains schema changes.  To upgrade:
+----
+  java -jar gerrit.war init -d site_path
+----
+
+*WARNING:* When upgrading from an existing site that was initialized with Gerrit
+version 2.6 to version 2.9.1, the primary key column order will be updated for
+some tables. It is therefore important to upgrade the site with the `init` program,
+rather than only copying the .war file over the existing one.
+
+It is recommended to run the `init` program in interactive mode. Warnings will
+be suppressed in batch mode.
+
+*WARNING:* Upgrading to 2.10.x requires the server be first upgraded to 2.8
+(or 2.9) and then to 2.10.x. If you are upgrading from 2.8.x or
+later, you may ignore this warning and upgrade directly to 2.10.x.
+
+*WARNING:* The `auth.allowGoogleAccountUpgrade` setting is no longer supported.
+
+
+Release Highlights
+------------------
+
+
+* Support for externally loaded plugins.
++
+Plugins can be implemented in Scala or Groovy using the
+link:https://gerrit-review.googlesource.com/\#/admin/projects/plugins/scripting/groovy-provider[
+Groovy provider] and
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/scripting/scala-provider[
+Scala provider] plugins.
+
+* Customizable 'My' menu.
++
+Users can customize the contents of the 'My' menu in the top menu.  Administrators
+can configure the default contents of the menu.
+
+
+New Features
+------------
+
+
+Web UI
+~~~~~~
+
+
+Global
+^^^^^^
+
+* Add 'All-Users' project to store meta data for all users.
+
+* Administrators can customize the default contents of the 'My' menu.
+
+* Add 'My' > 'Groups' menu entry that shows the list of own groups.
+
+* Allow UiActions to perform redirects without JavaScript.
+
+
+Change Screen
+^^^^^^^^^^^^^
+
+
+* Display avatar for author, committer, and change owner.
+
+* Remove message box when editing topic of change.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2573[Issue 2573]:
+Add option to quickly add current user as reviewer of a change.
++
+An 'Add Me' button is displayed next to the 'Add' button when searching for
+reviewers to add to a change. This allows users to quickly add themselves as a
+reviewer on the change without having to type their name in the search
+box.
+
+* Link project name to dashboard.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2667[Issue 2667]:
+Allow to customize Submit button label and tooltip.
+
+
+Side-by-Side Diff Screen
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+* Allow the user to select the syntax highlighter.
+
+* Add `Shift-a` keybinding to show/hide left side.
+
+* Allow to toggle empty pane for added and deleted files.
+
+* Add syntax highlighting of the commit message.
+
+
+Change List / Dashboards
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+* Remove age operator when drilling down from a dashboard to a query.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2646[Issue 2646]:
+Add option to show Change-ID in the change table.
+
+* Make the own user dashboard available under '/dashboard/self'.
+
+* Add 'R' key binding to refresh custom dashboards.
++
+Account dashboards, search results and the change screen refresh their content
+when 'R' is pressed.  The same binding is added for custom dashboards.
+
+
+Project Screens
+^^^^^^^^^^^^^^^
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2751[Issue 2751]:
+Add support for filtering by regex in project list screen.
+
+* Disable content merge option if project's merge strategy is fast forward only.
+
+* Add branch actions to 'Projects > Branches' view.
+
+User Preferences
+^^^^^^^^^^^^^^^^
+
+
+* Users can customize the contents of the 'My' menu from the preferences
+screen.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=2628[Issue 2628]:
+Replace 'Display name in review category' preference with a list of options.
++
+Including new options 'Show Abbreviated Name' to display abbreviated reviewer
+names and 'Show Username' to show usernames in the change list.
+
+
+Secondary Index / Search
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+
+* Allow to search projects by prefix.
+
+* Add search fields for number of changed lines.
+
+* Add suggestions for 'is:pending' and 'status:pending'.
+
+* Add 'pending' as alias for 'open'.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=2545[Issue 2545]:
+Support `topic:""` to find changes with no topic.
+
+* Search more fields in the default search query.
++
+If a search is given with only a text, search over a variety of fields
+rather than just the project name.
+
+
+ssh
+~~~
+
+
+* Expose SSHD backend in
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/cmd-show-connections.html[
+`show connections`] SSH command.
+
+* Add support for JCE (Java Cryptography Extension) ciphers.
+
+REST API
+~~~~~~~~
+
+
+General
+^^^^^^^
+
+
+* Remove `kind` attribute from REST containers.
+
+* Support `AcceptsPost` on non top-level REST collections.
+
+* Accept `HEAD` in RestApiServlet.
+
+Accounts
+^^^^^^^^
+
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-accounts.html#get-user-preferences[
+Get user preferences].
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-accounts.html#set-user-preferences[
+Set user preferences].
+
+Changes
+^^^^^^^
+
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2338[Issue 2338]:
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-changes.html#create-change[
+Create change].
+
+* Add `other-branches` option on
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-changes.html#get-mergeable[
+Get mergeable] endpoint.
++
+If the `other-branches` option is specified, the mergeability will also be
+checked for all other branches.
+
+Config
+^^^^^^
+
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-config.html#list-tasks[
+List tasks].
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-config.html#get-task[
+Get task].
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-config.html#delete-task[
+Delete task].
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-config.html#list-caches[
+List caches].
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-config.html#flush-cache[
+Flush cache].
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-config.html#flush-several-caches[
+Flush several caches].
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-config.html#flush-all-caches[
+Flush all caches].
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-config.html#get-summary[
+Get server summary].
+
+Projects
+^^^^^^^^
+
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-projects.html#ban-commit[
+Ban commits].
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-projects.html#get-content[
+Get the content of a file from a certain commit].
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2604[Issue 2604]:
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-projects.html#get-commit[
+Get an arbitrary commit from a project].
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-projects.html#get-reflog[
+Get the reflog of a branch].
+
+* Add option 'S' to
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-projects.html#list-projects[
+list projects endpoint] to support query offset.
+
+
+Daemon
+~~~~~~
+
+
+* Add change subject to output of change URL on push.
+
+* Indicate trivial rebase and commit message update on push.
+
+* Add support for
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/user-upload.html#review_labels[
+adding review labels on changes] during git push.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2634[Issue 2634]:
+Add change kind to PatchSetCreatedEvent.
+
+
+Configuration
+~~~~~~~~~~~~~
+
+* Use
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/config-gerrit.html#core.useRecursiveMerge[
+recursive merge] by default.
+
+* Allow to configure the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/config-gerrit.html#download.archive[
+available download archive formats].
+
+* Add support for
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/database-setup.html#createdb_maxdb[
+SAP MaxDB].
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2041[Issue 2041]:
+Allow
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/config-labels.html#label_defaultValue[
+configuration of a default value for a label].
+
+* Allow projects to
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/config-project-config.html#mimetype-section[
+configure MIME types for files].
+
+* Allow to configure
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/config-gerrit.html#gc[
+periodic garbage collection of all projects].
+
+* Remove `auth.allowGoogleAccountUpgrade` setting.
++
+It's been more than 5 years since Gerrit ran on Google AppEngine.  It is assumed
+that everyone has upgraded their installations to a modern 2.x based server, and
+will not need to have this upgrade path enabled.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2618[Issue 2618]:
+Remove `label.Label-Name.abbreviation` setting.
++
+The setting was no longer used, so it has been removed.
+
+* New `httpd.registerMBeans` setting.
++
+The
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/config-gerrit.html#httpd.registerMBeans[
+`httpd.registerMBeans` setting] allows to enable (or disable) registration of
+Jetty MBeans for Java JMX.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2600[Issue 2600]:
+Add documentation of how to
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/install-j2ee.html#tomcat[
+configure Tomcat] to allow embedded slashes.
+
+
+Misc
+~~~~
+
+* Don't allow empty user name and passwords in InternalAuthBackend.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2596[Issue 2596]:
+Add change-owner parameter to gerrit hooks.
+
+
+Plugins
+~~~~~~~
+
+* Support for externally loaded plugins.
++
+Plugins can be implemented in Scala or Groovy using the
+link:https://gerrit-review.googlesource.com/\#/admin/projects/plugins/scripting/groovy-provider[
+Groovy provider] and
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/scripting/scala-provider[
+Scala provider] plugins.
+
+* Allow plugins to replace the WebSession implementation.
++
+Plugins can replace the existing implementation with the statement:
+`DynamicItem.bind(binder(), WebSession.class).to(...);`
+in a module designated as a `<Gerrit-HttpModule>` in the manifest.
++
+Just the Cache implementation used for web sessions can be changed
+by binding to a subclass of the now abstract `CacheBasedWebSession`
+which supplies the Cache in the superclass constructor.
++
+This is a step towards solving web session issues with multi-master.
++
+The link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/websession-flatfile[
+websession-flatfile plugin] replaces the built-in Gerrit WebSession implementation
+with one that uses a flat file based cache.
+
+* Allow http and ssh plugins to replace the Gerrit-provided DynamicItem.
+
+* New extension point to listen to usage data published events.
++
+Plugins implementing the `UsageDataPublishedListener` can listen to
+events published about usage data.
+
+* New extension point to link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/dev-plugins.html#pre-upload-hook[
+register JGit PreUploadHook].
++
+Plugins may register PreUploadHook instances in order to get
+notified when JGit is about to upload a pack. This may be useful
+for those plugins which would like to monitor usage in Git
+repositories.
+
+* New link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/config-validation.html#pre-upload-validation[
+pre-upload validation extension point].
++
+Plugins implementing the `UploadValidationListener` interface can
+perform additional validation checks before any upload operations
+(clone, fetch, pull). The validation is executed right before Gerrit
+begins to send a pack back to the git client.
+
+* New link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/dev-plugins.html#links-to-external-tools[
+external tool links extension points].
++
+Plugins can now contribute project links that will be displayed on the project
+list screen in the 'Repository Browser' column, and revision links that will be
+shown on the change screen.
+
+* Allow creation of persistent caches after server is started.
++
+This enables plugins to create own persistent caches when they are
+installed.
+
+* Make gerrit's HttpServletRequest and HttpServletResponse visible to http
+plugins.
+
+* New extensions in the Java Plugin API:
+
+** Query changes
+** Create/get/list projects
+** Get/set review status
+** Create change
+** Get account
+** Star/unstar changes
+** Check if revision needs rebase
+
+Bug Fixes
+---------
+
+General
+~~~~~~~
+
+* Use fixed rate instead of fixed delay for log file compression.
++
+Log file compression was scheduled using a fixed delay. This caused the start
+times to drift over time. Use a fixed rate instead so that the compression
+reoccurs at the same time every day.
+
+* Don't email project watchers on new draft changes.
++
+If a draft change is created by pushing to `refs/drafts/master`, only the reviewers
+explicitly named on the command line (which may be empty) should be notified of
+the change. Users watching the project should not be notified, as the change has
+not yet been published.
+
+* Fix resource exhaustion due to unclosed LDAP connection.
++
+When `auth.type` is set to `LDAP` (not `LDAP_BIND`), two LDAP connections are
+made, but one was not being closed. This eventually caused resource exhaustion
+and LDAP authentications failed.
+
+Access Permissions
+~~~~~~~~~~~~~~~~~~
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2995[Issue 2995]:
+Fix faulty behaviour in `BLOCK` permission.
++
+`BLOCK` can be overruled with `ALLOW` on the same project, however there was a
+bug when a child of the above project duplicates the `ALLOW` permission. In this
+case the `BLOCK` would always win for the child, even though the `BLOCK` was
+overruled in the parent.
+
+Web UI
+~~~~~~
+
+General
+^^^^^^^
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2595[Issue 2595]:
+Make gitweb redirect to login.
++
+Gitweb redirects to the login page if the user isn't currently logged.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2631[Issue 2631]:
+Re-arrange info at footer of Gerrit web UI pages.
++
+Move the Gerrit info link so that there are no links close to the next page link.
+
+* Only create All-Projects ACL once.
++
+If `refs/meta/config` already existed it was overwritten with default configuration
+if a site administrator ran `java -war gerrit.war init -d /some/existing/site --batch`.
+
+
+Change Screen
+^^^^^^^^^^^^^
+
+* Don't linkify trailing dot or comma in messages.
++
+As linkifying trailing dots and trailing commas does more harm than
+good, we only treat dots and commas as being part of urls, if they are
+neither followed by whitespace nor occur at the end of a string.
+
+* Re-enable the 'Cherry Pick' button after canceling the dialog.
++
+If the dialog was canceled, the button remained disabled and could not be
+used again.
+
+* Improve message when removing a reviewer.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=527[Issue 527]:
+Preserve line breaks in inline and review comments.
+
+* Always show 'No Score' as label help for zero votings.
+
+* Only reset the edited commit message text on cancel.
+
+* Only include message on quick approve if reply is open.
+
+* List reviewers with dummy approvals on closed changes.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=2890[Issue 2890]:
+Enable scrollbars for "Edit Commit Message" TextArea.
+
+* Use current time instead of submitter time for cherry-picked commits.
++
+Cherry picking with the submitter time could cause massive clock skew
+in the Git commit graph if the server was shutdown before the submit could
+finish, and restarted hours later.
+
+* Fix exception when clicking on a binary file without being signed in.
+
+
+Side-By-Side Diff
+^^^^^^^^^^^^^^^^^
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2970[Issue 2970]:
+Fix misalignment of side A and side B for long insertion/deletion blocks.
+
+* Give B side full width when A side is hidden.
+
+* Fix scroll alignment when showing hidden A side.
+
+* Bind Shift-N to search-prev in vim mode.
+
+* Allow text selection in diff header.
+
+* Display diff header on mode changes and renames.
+
+* Document Shift-{Left,Right} in `?` help popup.
+
+* Show `[` and `]` shortcut keys in nav arrow tooltips.
+
+* Disable "Render = Slow" mode on files over 4000 lines.
+
+* Keep keyboard bindings alive after click in padding.
+
+* Jump to the first change on either side.
+
+* Expand margin between paragraphs in comments.
+
+* Include content on identical files with mode change.
+
+
+User Settings
+^^^^^^^^^^^^^
+
+* Avoid loading all SSH keys when adding a new one.
+
+
+Secondary Index / Search
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+
+* Omit corrupt changes from search results.
+
+* Allow illegal label names from default search predicate.
+
+REST
+~~~~
+
+General
+^^^^^^^
+
+* Fix REST API responses for 3xx and 4xx classes.
+
+Changes
+^^^^^^^
+
+* Fix inconsistent behaviour in the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-changes.html#add-reviewer[
+add reviewer endpoint]
++
+When adding a single reviewer to a change, it was possible to use the endpoint
+to add a user who had no visibility to the change or whose account was invalid.
+
+
+Changes
+^^^^^^^
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2583[Issue 2583]:
+Reject inline comments on files that do not exist in the patch set.
+
+* Allow forcing mergeability check on
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-changes.html#get-mergeable[
+Get mergeable].
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2622[Issue 2622]:
+Respect patch set visibility for messages.
++
+Messages retrieval didn't check for patch set visbility and thus messages for
+draft patch sets were returned back to the client.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2782[Issue 2782]:
+Add missing documentation of the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-changes.html#get-related-changes[
+Get Related Changes] endpoint.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2723[Issue 2723]:
+Clarify the response info in the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-changes.html#get-change-detail[
+Get Change Detail] endpoint.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2693[Issue 2693]:
+Clarify the response info in the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-changes.html#list-comments[
+List Comments] endpoint.
+
+SSH
+~~~
+
+
+* Prevent double authentication for the same public key.
++
+This is a workaround for link:https://issues.apache.org/jira/browse/SSHD-300[
+SSHD-300].
+
+* Let `kill` SSH command only kill tasks that are visible to the caller.
+
+* Require 'Administrate Server' capability to see server summary output from
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/cmd-show-caches.html[
+`show-caches`] command.
+
+* Include all command arguments in SSH log entry.
++
+The SSH log only included the first argument. This prevented the repository name
+from being logged when `git receive-pack` was executed instead of `git-receive-pack`.
+
+
+Daemon
+~~~~~~
+
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2284[Issue 2284]:
+More detailed error message when failing to upload new change.
++
+When the uploaded change cannot be created on the underlying Git repository, a
+more descriptive error message is displayed on both client and server side. This
+allows to troubleshoot internal errors (e.g. JGit lock failures or other causes)
+and help out in the resolution.
+
+* Enforce HTTP password checking on gitBasicAuth.
+
+* Fix missing commit messages on submodule direct pushes.
++
+The commit message in superproject was missing on submodule's
+directly pushed changes.
+
+
+Plugins
+~~~~~~~
+
+General
+^^^^^^^
+
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2895[Issue 2895]:
+Fix reload of plugins that use DynamicItem.
+
+* Invoke `StartPluginListener` and `ReloadPluginListener` only after start/reload
+is fully done.
+
+* Set `Last-Modified` on cached Documentation resources.
+
+* Return HTTP 304 for not modified SmallResources.
+
+* Fix ChangeListener auto-registered implementations.
+
+Replication
+^^^^^^^^^^^
+
+
+* Move replication logs into a separate file.
+
+* Promote replication scheduled logs to info.
+
+* Show replication ID in the log and in show-queue command.
+
+
+Upgrades
+--------
+
+
+* Update Guava to 17.0
+
+* Update Guice to 4.0-beta5
+
+* Update GWT to 2.6.1
+
+* Update httpclient to 4.3.4
+
+* Update httpcore to 4.3.2
+
+* Update Jcraft SSH to 0.1.51
+
+* Update Jetty to 9.2
+
+* Update JGit to 3.6.2.201501210735-r
+
+* Update log4j to 1.2.17
+
+* Update Servlet API to 8.0.5
+
+* Update slf4j to 1.7.7
+
+* Update Velocity to 1.7
+
diff --git a/ReleaseNotes/ReleaseNotes-2.5.txt b/ReleaseNotes/ReleaseNotes-2.5.txt
index cdef554..4abed47 100644
--- a/ReleaseNotes/ReleaseNotes-2.5.txt
+++ b/ReleaseNotes/ReleaseNotes-2.5.txt
@@ -923,11 +923,11 @@
 working tree dirty.  Eclipse 4 (Juno) still overwrites these files but
 doesn't write the timestamp.  This should help to keep the working tree
 clean.  However, since the timestamp is currently present in these
-files, Eclispe 4 would still make them dirty by overwriting and
+files, Eclipse 4 would still make them dirty by overwriting and
 effectively removing the timestamp.
 +
 This change removes the timestamp from these files. This helps those
-using Eclipse 4 and doesn't make it worse for those still using Eclispe
+using Eclipse 4 and doesn't make it worse for those still using Eclipse
 3.
 
 * Add Maven profile to skip build of plugin modules
diff --git a/ReleaseNotes/ReleaseNotes-2.8.txt b/ReleaseNotes/ReleaseNotes-2.8.txt
index 4f6768c..92cdda2 100644
--- a/ReleaseNotes/ReleaseNotes-2.8.txt
+++ b/ReleaseNotes/ReleaseNotes-2.8.txt
@@ -499,7 +499,10 @@
 and to replicate the project deletions. By default project deletions are *not*
 replicated.
 
-* The `{$name}` placeholder is optional when replicating a single project,
+* link:http://code.google.com/p/gerrit/issues/detail?id=1880[Issue 1880]:
+Make `{name}` placeholder optional when replicating a single project.
++
+The `{$name}` placeholder is optional when replicating a single project,
 allowing a single project to be replicated under a different name.
 
 * Project names can be matched with wildcard or regex patterns in `replication.config`.
@@ -622,7 +625,7 @@
 
 * Do not persist default project state in `project.config`.
 
-* Honor the `gerrit.cannonicalWebUrl` setting when opening the browser after init.
+* Honor the `gerrit.canonicalWebUrl` setting when opening the browser after init.
 
 * Fix 'query disabled' error when Query Limit is set.
 
diff --git a/ReleaseNotes/index.txt b/ReleaseNotes/index.txt
index 45385f9..2614335 100644
--- a/ReleaseNotes/index.txt
+++ b/ReleaseNotes/index.txt
@@ -1,6 +1,20 @@
 Gerrit Code Review - Release Notes
 ==================================
 
+[[2_10]]
+Version 2.10.x
+--------------
+* link:ReleaseNotes-2.10.8.html[2.10.8]
+* link:ReleaseNotes-2.10.7.html[2.10.7]
+* link:ReleaseNotes-2.10.6.html[2.10.6]
+* link:ReleaseNotes-2.10.5.html[2.10.5]
+* link:ReleaseNotes-2.10.4.html[2.10.4]
+* link:ReleaseNotes-2.10.3.1.html[2.10.3.1]
+* link:ReleaseNotes-2.10.3.html[2.10.3]
+* link:ReleaseNotes-2.10.2.html[2.10.2]
+* link:ReleaseNotes-2.10.1.html[2.10.1]
+* link:ReleaseNotes-2.10.html[2.10]
+
 [[2_9]]
 Version 2.9.x
 -------------
diff --git a/VERSION b/VERSION
index c9e9a5d..eaf28f2 100644
--- a/VERSION
+++ b/VERSION
@@ -2,5 +2,4 @@
 # Used by :api_install and :api_deploy targets
 # when talking to the destination repository.
 #
-GERRIT_VERSION = '2.9.4'
-
+GERRIT_VERSION = '2.10.7'
diff --git a/bucklets/gerrit_plugin.bucklet b/bucklets/gerrit_plugin.bucklet
new file mode 100644
index 0000000..eb10456
--- /dev/null
+++ b/bucklets/gerrit_plugin.bucklet
@@ -0,0 +1,15 @@
+#
+# Dummy to make the co-existence of core and standalone plugins possible.
+# Intentionaly left empty as this doesn't suppose to have any side effects
+# in tree build, i. e.:
+#
+# cookbook-plugin/BUCK include this line:
+# include_defs('//bucklets/gerrit_plugin.bucklet')
+#
+# When executing from the Gerrit tree:
+# buck build plugins/cookbook-plugin
+#
+# this line has no effect.
+#
+# When compiling from standalone cookbook-plugin, bucklets directory points
+# to cloned bucklets library that includes real gerrit_plugin.bucklet code.
diff --git a/bucklets/maven_jar.bucklet b/bucklets/maven_jar.bucklet
new file mode 120000
index 0000000..130a747
--- /dev/null
+++ b/bucklets/maven_jar.bucklet
@@ -0,0 +1 @@
+../lib/maven.defs
\ No newline at end of file
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 5c5746b..2b01a22 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -21,6 +21,7 @@
 
 import com.google.common.base.Joiner;
 import com.google.common.primitives.Chars;
+import com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context;
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.api.changes.RevisionApi;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -128,9 +129,9 @@
     userSession = new RestSession(server, user);
     initSsh(admin);
     db = reviewDbProvider.open();
-    atrScope.set(atrScope.newContext(reviewDbProvider, sshSession,
-        identifiedUserFactory.create(Providers.of(db), admin.getId())));
-    sshSession = new SshSession(server, admin);
+    Context ctx = newRequestContext(admin);
+    atrScope.set(ctx);
+    sshSession = ctx.getSession();
     project = new Project.NameKey("p");
     createProject(sshSession, project.get());
     git = cloneProject(sshSession.getUrl() + "/" + project.get());
@@ -145,6 +146,7 @@
     db.close();
     sshSession.close();
     server.stop();
+    TempFileUtil.cleanup();
   }
 
   protected PushOneCommit.Result createChange() throws GitAPIException,
@@ -194,6 +196,19 @@
     return gApi.changes().id(id).get(s);
   }
 
+  protected List<ChangeInfo> query(String q) throws RestApiException {
+    return gApi.changes().query(q).get();
+  }
+
+  private Context newRequestContext(TestAccount account) {
+    return atrScope.newContext(reviewDbProvider, new SshSession(server, admin),
+        identifiedUserFactory.create(Providers.of(db), account.getId()));
+  }
+
+  protected Context setApiUser(TestAccount account) {
+    return atrScope.set(newRequestContext(account));
+  }
+
   protected static Gson newGson() {
     return OutputFormat.JSON_COMPACT.newGson();
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
index 63bdfd2..6ee7efa 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
@@ -130,8 +130,7 @@
     }
   }
 
-  private static final ThreadLocal<Context> current =
-      new ThreadLocal<Context>();
+  private static final ThreadLocal<Context> current = new ThreadLocal<>();
 
   private static Context requireContext() {
     final Context ctx = current.get();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritServer.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritServer.java
index d427509..85da2f5 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritServer.java
@@ -98,7 +98,7 @@
     }
 
     Injector i = createTestInjector(daemon);
-    return new GerritServer(site, i, daemon, daemonService);
+    return new GerritServer(i, daemon, daemonService);
   }
 
   private static File initSite(Config base) throws Exception {
@@ -130,6 +130,7 @@
     cfg.setString("httpd", null, "listenUrl", url);
     cfg.setString("sshd", null, "listenAddress", forceEphemeralPort);
     cfg.setString("cache", null, "directory", null);
+    cfg.setString("gerrit", null, "basePath", "git");
     cfg.setBoolean("sendemail", null, "enable", false);
     cfg.setInt("cache", "projects", "checkFrequency", 0);
     cfg.setInt("plugins", null, "checkFrequency", 0);
@@ -159,7 +160,6 @@
     return InetAddress.getLoopbackAddress();
   }
 
-  private File sitePath;
   private Daemon daemon;
   private ExecutorService daemonService;
   private Injector testInjector;
@@ -167,9 +167,8 @@
   private InetSocketAddress sshdAddress;
   private InetSocketAddress httpAddress;
 
-  private GerritServer(File sitePath, Injector testInjector, Daemon daemon,
+  private GerritServer(Injector testInjector, Daemon daemon,
       ExecutorService daemonService) throws IOException, ConfigInvalidException {
-    this.sitePath = sitePath;
     this.testInjector = testInjector;
     this.daemon = daemon;
     this.daemonService = daemonService;
@@ -208,9 +207,6 @@
       daemonService.shutdownNow();
       daemonService.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
     }
-    if (sitePath != null) {
-      TempFileUtil.recursivelyDelete(sitePath);
-    }
     RepositoryCache.clear();
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/HttpSession.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/HttpSession.java
index b543aa7..5d81900 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/HttpSession.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/HttpSession.java
@@ -18,8 +18,10 @@
 
 import org.apache.http.auth.AuthScope;
 import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.HttpClient;
 import org.apache.http.client.methods.HttpGet;
-import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.apache.http.impl.client.HttpClientBuilder;
 
 import java.io.IOException;
 import java.net.URI;
@@ -28,7 +30,7 @@
 
   protected final String url;
   private final TestAccount account;
-  private DefaultHttpClient client;
+  private HttpClient client;
 
   public HttpSession(GerritServer server, TestAccount account) {
     this.url = CharMatcher.is('/').trimTrailingFrom(server.getUrl());
@@ -40,13 +42,19 @@
     return new HttpResponse(getClient().execute(get));
   }
 
-  protected DefaultHttpClient getClient() {
+  protected HttpClient getClient() {
     if (client == null) {
       URI uri = URI.create(url);
-      client = new DefaultHttpClient();
-      client.getCredentialsProvider().setCredentials(
-          new AuthScope(uri.getHost(), uri.getPort()),
-          new UsernamePasswordCredentials(account.username, account.httpPassword));
+      BasicCredentialsProvider creds = new BasicCredentialsProvider();
+      creds.setCredentials(new AuthScope(uri.getHost(), uri.getPort()),
+          new UsernamePasswordCredentials(account.username,
+              account.httpPassword));
+      client = HttpClientBuilder
+          .create()
+          .setDefaultCredentialsProvider(creds)
+          .setMaxConnPerRoute(10)
+          .setMaxConnTotal(1024)
+          .build();
     }
     return client;
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
index cc19c15..32705ba 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
@@ -56,7 +56,7 @@
 
 public class PushOneCommit {
   public static final String SUBJECT = "test commit";
-  static final String FILE_NAME = "a.txt";
+  public static final String FILE_NAME = "a.txt";
   private static final String FILE_CONTENT = "some content";
 
   public interface Factory {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TempFileUtil.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TempFileUtil.java
index ff0ca7b..0b78f57 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TempFileUtil.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TempFileUtil.java
@@ -16,17 +16,29 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
 
 public class TempFileUtil {
-  public static File createTempDirectory() throws IOException {
-    File tmp = File.createTempFile("gerrit_test_", "");
+  private static List<File> allDirsCreated = new ArrayList<>();
+
+  public synchronized static File createTempDirectory() throws IOException {
+    File tmp = File.createTempFile("gerrit_test_", "").getCanonicalFile();
     if (!tmp.delete() || !tmp.mkdir()) {
       throw new IOException("Cannot create " + tmp.getPath());
     }
+    allDirsCreated.add(tmp);
     return tmp;
   }
 
-  public static void recursivelyDelete(File dir) throws IOException {
+  public static synchronized void cleanup() throws IOException {
+    for (File dir : allDirsCreated) {
+      recursivelyDelete(dir);
+    }
+    allDirsCreated.clear();
+  }
+
+  private static void recursivelyDelete(File dir) throws IOException {
     if (!dir.getPath().equals(dir.getCanonicalPath())) {
       // Directory symlink reaching outside of temporary space.
       return;
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
new file mode 100644
index 0000000..c95f8c3
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -0,0 +1,69 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.api.accounts;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.restapi.RestApiException;
+
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class AccountIT extends AbstractDaemonTest {
+
+  @Test
+  public void get() throws RestApiException {
+    AccountInfo info = gApi
+        .accounts()
+        .id("admin")
+        .get();
+    assertEquals("Administrator", info.name);
+    assertEquals("admin@example.com", info.email);
+    assertEquals("admin", info.username);
+  }
+
+  @Test
+  public void self() throws RestApiException {
+    AccountInfo info = gApi
+        .accounts()
+        .self()
+        .get();
+    assertEquals("Administrator", info.name);
+    assertEquals("admin@example.com", info.email);
+    assertEquals("admin", info.username);
+  }
+
+  @Test
+  public void starUnstarChange() throws GitAPIException,
+      IOException, RestApiException {
+    PushOneCommit.Result r = createChange();
+    String triplet = "p~master~" + r.getChangeId();
+    gApi.accounts()
+        .self()
+        .starChange(triplet);
+    assertTrue(getChange(triplet).starred);
+    gApi.accounts()
+        .self()
+        .unstarChange(triplet);
+    assertNull(getChange(triplet).starred);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/BUCK
new file mode 100644
index 0000000..1152d88
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/BUCK
@@ -0,0 +1,6 @@
+include_defs('//gerrit-acceptance-tests/tests.defs')
+
+acceptance_tests(
+  srcs = glob(['*IT.java']),
+  labels = ['api'],
+)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/BUCK
index aa9703c..1152d88 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/BUCK
@@ -2,5 +2,5 @@
 
 acceptance_tests(
   srcs = glob(['*IT.java']),
-  deps = ['//gerrit-acceptance-tests:lib'],
+  labels = ['api'],
 )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 6dff20a..c79b198 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -15,21 +15,37 @@
 package com.google.gerrit.acceptance.api.change;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
 
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
+import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.common.ApprovalInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeStatus;
+import com.google.gerrit.extensions.common.LabelInfo;
+import com.google.gerrit.extensions.common.ListChangesOption;
+import com.google.gerrit.extensions.common.RevisionInfo;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.Account;
 
 import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.lib.Constants;
 import org.junit.Test;
 
 import java.io.IOException;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Set;
 
 @NoHttpd
 public class ChangeIT extends AbstractDaemonTest {
@@ -48,6 +64,7 @@
     assertEquals(true, c.mergeable);
     assertEquals(r.getChangeId(), c.changeId);
     assertEquals(c.created, c.updated);
+    assertEquals(1, c._number);
   }
 
   @Test
@@ -99,14 +116,166 @@
         .rebase();
   }
 
+  private static Set<Account.Id> getReviewers(ChangeInfo ci) {
+    Set<Account.Id> result = Sets.newHashSet();
+    for (LabelInfo li : ci.labels.values()) {
+      for (ApprovalInfo ai : li.all) {
+        result.add(new Account.Id(ai._accountId));
+      }
+    }
+    return result;
+  }
+
   @Test
   public void addReviewer() throws GitAPIException,
       IOException, RestApiException {
     PushOneCommit.Result r = createChange();
     AddReviewerInput in = new AddReviewerInput();
     in.reviewer = user.email;
+    ChangeApi cApi = gApi.changes().id("p~master~" + r.getChangeId());
+    cApi.addReviewer(in);
+    assertEquals(ImmutableSet.of(user.id), getReviewers(cApi.get()));
+  }
+
+  @Test
+  public void addReviewerToClosedChange() throws GitAPIException,
+      IOException, RestApiException {
+    PushOneCommit.Result r = createChange();
+    ChangeApi cApi = gApi.changes().id("p~master~" + r.getChangeId());
+    cApi.revision(r.getCommit().name())
+        .review(ReviewInput.approve());
+    cApi.revision(r.getCommit().name())
+        .submit();
+
+    assertEquals(ImmutableSet.of(admin.getId()), getReviewers(cApi.get()));
+
+    AddReviewerInput in = new AddReviewerInput();
+    in.reviewer = user.email;
     gApi.changes()
         .id("p~master~" + r.getChangeId())
         .addReviewer(in);
+    assertEquals(ImmutableSet.of(admin.getId(), user.id),
+        getReviewers(cApi.get()));
+  }
+
+  @Test
+  public void createEmptyChange() throws RestApiException {
+    ChangeInfo in = new ChangeInfo();
+    in.branch = Constants.MASTER;
+    in.subject = "Create a change from the API";
+    in.project = project.get();
+    ChangeInfo info = gApi
+        .changes()
+        .create(in)
+        .get();
+    assertEquals(in.project, info.project);
+    assertEquals(in.branch, info.branch);
+    assertEquals(in.subject, info.subject);
+  }
+
+  @Test
+  public void queryChangesNoQuery() throws Exception {
+    PushOneCommit.Result r1 = createChange();
+    PushOneCommit.Result r2 = createChange();
+    List<ChangeInfo> results = gApi.changes().query().get();
+    assertEquals(2, results.size());
+    assertEquals(r2.getChangeId(), results.get(0).changeId);
+    assertEquals(r1.getChangeId(), results.get(1).changeId);
+  }
+
+  @Test
+  public void queryChangesNoResults() throws Exception {
+    createChange();
+    List<ChangeInfo> results = query("status:open");
+    assertEquals(1, results.size());
+    results = query("status:closed");
+    assertTrue(results.isEmpty());
+  }
+
+  @Test
+  public void queryChangesOneTerm() throws Exception {
+    PushOneCommit.Result r1 = createChange();
+    PushOneCommit.Result r2 = createChange();
+    List<ChangeInfo> results = query("status:open");
+    assertEquals(2, results.size());
+    assertEquals(r2.getChangeId(), results.get(0).changeId);
+    assertEquals(r1.getChangeId(), results.get(1).changeId);
+  }
+
+  @Test
+  public void queryChangesMultipleTerms() throws Exception {
+    PushOneCommit.Result r1 = createChange();
+    createChange();
+    List<ChangeInfo> results = query("status:open " + r1.getChangeId());
+    assertEquals(r1.getChangeId(), Iterables.getOnlyElement(results).changeId);
+  }
+
+  @Test
+  public void queryChangesLimit() throws Exception {
+    createChange();
+    PushOneCommit.Result r2 = createChange();
+    List<ChangeInfo> results = gApi.changes().query().withLimit(1).get();
+    assertEquals(1, results.size());
+    assertEquals(r2.getChangeId(), Iterables.getOnlyElement(results).changeId);
+  }
+
+  @Test
+  public void queryChangesStart() throws Exception {
+    PushOneCommit.Result r1 = createChange();
+    createChange();
+    List<ChangeInfo> results = gApi.changes().query().withStart(1).get();
+    assertEquals(r1.getChangeId(), Iterables.getOnlyElement(results).changeId);
+  }
+
+  @Test
+  public void queryChangesNoOptions() throws Exception {
+    PushOneCommit.Result r = createChange();
+    ChangeInfo result = Iterables.getOnlyElement(query(r.getChangeId()));
+    assertNull(result.labels);
+    assertNull(result.messages);
+    assertNull(result.revisions);
+    assertNull(result.actions);
+  }
+
+  @Test
+  public void queryChangesOptions() throws Exception {
+    PushOneCommit.Result r = createChange();
+    ChangeInfo result = Iterables.getOnlyElement(gApi.changes()
+        .query(r.getChangeId())
+        .withOptions(EnumSet.allOf(ListChangesOption.class))
+        .get());
+    assertEquals("Code-Review",
+        Iterables.getOnlyElement(result.labels.keySet()));
+    assertEquals(1, result.messages.size());
+    assertFalse(result.actions.isEmpty());
+
+    RevisionInfo rev = Iterables.getOnlyElement(result.revisions.values());
+    assertEquals(r.getPatchSetId().get(), rev._number);
+    assertFalse(rev.actions.isEmpty());
+  }
+
+  @Test
+  public void queryChangesOwnerWithDifferentUsers() throws Exception {
+    PushOneCommit.Result r = createChange();
+    assertEquals(r.getChangeId(),
+        Iterables.getOnlyElement(query("owner:self")).changeId);
+    setApiUser(user);
+    assertTrue(query("owner:self").isEmpty());
+  }
+
+  @Test
+  public void checkReviewedFlagBeforeAndAfterReview() throws Exception {
+    PushOneCommit.Result r = createChange();
+    AddReviewerInput in = new AddReviewerInput();
+    in.reviewer = user.email;
+    gApi.changes()
+        .id(r.getChangeId())
+        .addReviewer(in);
+
+    setApiUser(user);
+    assertNull(get(r.getChangeId()).reviewed);
+
+    revision(r).review(ReviewInput.recommend());
+    assertTrue(get(r.getChangeId()).reviewed);
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/BUCK
index 8da456d..1152d88 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/BUCK
@@ -2,6 +2,5 @@
 
 acceptance_tests(
   srcs = glob(['*IT.java']),
-  deps = ['//gerrit-acceptance-tests:lib'],
+  labels = ['api'],
 )
-
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/ProjectIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/ProjectIT.java
index 54afd0b..a10e026 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/ProjectIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/ProjectIT.java
@@ -14,20 +14,60 @@
 
 package com.google.gerrit.acceptance.api.project;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.api.projects.ProjectInput;
+import com.google.gerrit.extensions.common.ProjectInfo;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.junit.Test;
 
 import java.io.IOException;
+import java.util.List;
 
 @NoHttpd
 public class ProjectIT extends AbstractDaemonTest  {
 
   @Test
+  public void createProjectFoo() throws RestApiException {
+    String name = "foo";
+    assertEquals(name,
+        gApi.projects()
+            .name(name)
+            .create()
+            .get()
+            .name);
+  }
+
+  @Test(expected = RestApiException.class)
+  public void createProjectFooBar() throws RestApiException {
+    ProjectInput in = new ProjectInput();
+    in.name = "foo";
+    gApi.projects()
+        .name("bar")
+        .create(in);
+  }
+
+  @Test(expected = ResourceConflictException.class)
+  public void createProjectDuplicate() throws RestApiException {
+    ProjectInput in = new ProjectInput();
+    in.name = "baz";
+    gApi.projects()
+        .name("baz")
+        .create(in);
+    gApi.projects()
+        .name("baz")
+        .create(in);
+  }
+
+  @Test
   public void createBranch() throws GitAPIException,
       IOException, RestApiException {
     gApi.projects()
@@ -35,4 +75,45 @@
         .branch("foo")
         .create(new BranchInput());
   }
+
+  @Test
+  public void listProjects() throws Exception {
+    List<ProjectInfo> initialProjects = gApi.projects().list().get();
+
+    gApi.projects().name("foo").create();
+    gApi.projects().name("bar").create();
+
+    List<ProjectInfo> allProjects = gApi.projects().list().get();
+    assertEquals(initialProjects.size() + 2, allProjects.size());
+
+    List<ProjectInfo> projectsWithDescription = gApi.projects().list()
+        .withDescription(true)
+        .get();
+    assertNotNull(projectsWithDescription.get(0).description);
+
+    List<ProjectInfo> projectsWithoutDescription = gApi.projects().list()
+        .withDescription(false)
+        .get();
+    assertNull(projectsWithoutDescription.get(0).description);
+
+    List<ProjectInfo> noMatchingProjects = gApi.projects().list()
+        .withPrefix("fox")
+        .get();
+    assertEquals(0, noMatchingProjects.size());
+
+    List<ProjectInfo> matchingProject = gApi.projects().list()
+        .withPrefix("fo")
+        .get();
+    assertEquals(1, matchingProject.size());
+
+    List<ProjectInfo> limitOneProject = gApi.projects().list()
+        .withLimit(1)
+        .get();
+    assertEquals(1, limitOneProject.size());
+
+    List<ProjectInfo> startAtOneProjects = gApi.projects().list()
+        .withStart(1)
+        .get();
+    assertEquals(allProjects.size() - 1, startAtOneProjects.size());
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/BUCK
index aa9703c..1152d88 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/BUCK
@@ -2,5 +2,5 @@
 
 acceptance_tests(
   srcs = glob(['*IT.java']),
-  deps = ['//gerrit-acceptance-tests:lib'],
+  labels = ['api'],
 )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index 50522cd..9653ffa 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -14,6 +14,11 @@
 
 package com.google.gerrit.acceptance.api.revision;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
@@ -21,6 +26,7 @@
 import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.RevisionApi;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -130,16 +136,87 @@
         .name(project.get())
         .branch(in.destination)
         .create(new BranchInput());
-    ChangeApi cApi = gApi.changes()
-        .id(r.getChangeId())
-        .revision(r.getCommit().name())
+    ChangeApi orig = gApi.changes()
+        .id("p~master~" + r.getChangeId());
+
+    assertEquals(1, orig.get().messages.size());
+    ChangeApi cherry = orig.revision(r.getCommit().name())
         .cherryPick(in);
-    cApi.current()
+    assertEquals(2, orig.get().messages.size());
+
+    assertTrue(cherry.get().subject.contains(in.message));
+    cherry.current()
         .review(ReviewInput.approve());
-    cApi.current()
+    cherry.current()
         .submit();
   }
 
+  @Test
+  public void canRebase()
+      throws GitAPIException, IOException, RestApiException, Exception {
+    PushOneCommit push = pushFactory.create(db, admin.getIdent());
+    PushOneCommit.Result r1 = push.to(git, "refs/for/master");
+    merge(r1);
+
+    push = pushFactory.create(db, admin.getIdent());
+    PushOneCommit.Result r2 = push.to(git, "refs/for/master");
+    assertFalse(gApi.changes()
+        .id(r2.getChangeId())
+        .revision(r2.getCommit().name())
+        .canRebase());
+    merge(r2);
+
+    git.checkout().setName(r1.getCommit().name()).call();
+    push = pushFactory.create(db, admin.getIdent());
+    PushOneCommit.Result r3 = push.to(git, "refs/for/master");
+
+    assertTrue(gApi.changes()
+        .id(r3.getChangeId())
+        .revision(r3.getCommit().name())
+        .canRebase());
+  }
+
+  @Test
+  public void setUnsetReviewedFlag() throws Exception {
+    PushOneCommit push = pushFactory.create(db, admin.getIdent());
+    PushOneCommit.Result r = push.to(git, "refs/for/master");
+
+    gApi.changes()
+        .id(r.getChangeId())
+        .current()
+        .setReviewed(PushOneCommit.FILE_NAME, true);
+
+    assertEquals(PushOneCommit.FILE_NAME,
+        Iterables.getOnlyElement(
+            gApi.changes()
+                .id(r.getChangeId())
+                .current()
+                .reviewed()));
+
+    gApi.changes()
+        .id(r.getChangeId())
+        .current()
+        .setReviewed(PushOneCommit.FILE_NAME, false);
+
+    assertTrue(
+        gApi.changes()
+            .id(r.getChangeId())
+            .current()
+            .reviewed()
+            .isEmpty());
+  }
+
+  protected RevisionApi revision(PushOneCommit.Result r) throws Exception {
+    return gApi.changes()
+        .id(r.getChangeId())
+        .current();
+  }
+
+  private void merge(PushOneCommit.Result r) throws Exception {
+    revision(r).review(ReviewInput.approve());
+    revision(r).submit();
+  }
+
   private PushOneCommit.Result updateChange(PushOneCommit.Result r,
       String content) throws GitAPIException, IOException {
     PushOneCommit push = pushFactory.create(db, admin.getIdent(),
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index 5a8ba0c..5b3795e 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -15,10 +15,14 @@
 package com.google.gerrit.acceptance.git;
 
 import static com.google.gerrit.acceptance.GitUtil.cloneProject;
+import static org.junit.Assert.assertEquals;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.LabelInfo;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gwtorm.server.OrmException;
 
@@ -144,6 +148,43 @@
   }
 
   @Test
+  public void testPushForMasterWithApprovals() throws GitAPIException,
+      IOException, RestApiException {
+    PushOneCommit.Result r = pushTo("refs/for/master/%l=Code-Review");
+    r.assertOkStatus();
+    ChangeInfo ci = get(r.getChangeId());
+    LabelInfo cr = ci.labels.get("Code-Review");
+    assertEquals(1, cr.all.size());
+    assertEquals("Administrator", cr.all.get(0).name);
+    assertEquals(1, cr.all.get(0).value.intValue());
+
+    PushOneCommit push =
+        pushFactory.create(db, admin.getIdent(), PushOneCommit.SUBJECT,
+            "b.txt", "anotherContent", r.getChangeId());
+    r = push.to(git, "refs/for/master/%l=Code-Review+2");
+
+    ci = get(r.getChangeId());
+    cr = ci.labels.get("Code-Review");
+    assertEquals(1, cr.all.size());
+    assertEquals("Administrator", cr.all.get(0).name);
+    assertEquals(2, cr.all.get(0).value.intValue());
+  }
+
+  @Test
+  public void testPushForMasterWithApprovals_MissingLabel() throws GitAPIException,
+      IOException {
+      PushOneCommit.Result r = pushTo("refs/for/master/%l=Verify");
+      r.assertErrorStatus("label \"Verify\" is not a configured label");
+  }
+
+  @Test
+  public void testPushForMasterWithApprovals_ValueOutOfRange() throws GitAPIException,
+      IOException, RestApiException {
+    PushOneCommit.Result r = pushTo("refs/for/master/%l=Code-Review-3");
+    r.assertErrorStatus("label \"Code-Review\": -3 is not a valid value");
+  }
+
+  @Test
   public void testPushForNonExistingBranch() throws GitAPIException,
       OrmException, IOException {
     String branchName = "non-existing";
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/BUCK
index 5976f54..3ead2a1 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/BUCK
@@ -2,12 +2,13 @@
 
 acceptance_tests(
   srcs = ['DraftChangeBlockedIT.java', 'SubmitOnPushIT.java'],
-  deps = ['//gerrit-acceptance-tests:lib'],
+  labels = ['git'],
 )
 
 acceptance_tests(
   srcs = ['HttpPushForReviewIT.java', 'SshPushForReviewIT.java'],
   deps = [':push_for_review'],
+  labels = ['git'],
 )
 
 java_library(
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/DraftChangeBlockedIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/DraftChangeBlockedIT.java
index b69c264..20c8f76 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/DraftChangeBlockedIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/DraftChangeBlockedIT.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.acceptance.git;
 
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
-import static com.google.gerrit.server.project.Util.grant;
+import static com.google.gerrit.server.project.Util.block;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
@@ -49,8 +49,7 @@
   @Before
   public void setUp() throws Exception {
     ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
-    grant(cfg, Permission.PUSH, ANONYMOUS_USERS,
-        "refs/drafts/*").setBlock();
+    block(cfg, Permission.PUSH, ANONYMOUS_USERS, "refs/drafts/*");
     saveProjectConfig(cfg);
     projectCache.evict(cfg.getProject());
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/HttpPushForReviewIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/HttpPushForReviewIT.java
index 4d7897c..71f008a 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/HttpPushForReviewIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/HttpPushForReviewIT.java
@@ -24,7 +24,7 @@
 
 public class HttpPushForReviewIT extends AbstractPushForReview {
   @Before
-  public void selectHttpUrl() throws GitAPIException, IOException {
+  public void selectHttpUrl() throws GitAPIException, IOException, URISyntaxException {
     CredentialsProvider.setDefault(new UsernamePasswordCredentialsProvider(
         admin.username, admin.httpPassword));
     selectProtocol(Protocol.HTTP);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/BUCK
new file mode 100644
index 0000000..00b53f9
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/BUCK
@@ -0,0 +1,7 @@
+include_defs('//gerrit-acceptance-tests/tests.defs')
+
+acceptance_tests(
+  srcs = glob(['*IT.java']),
+  labels = ['pgm'],
+  source_under_test = ['//gerrit-pgm:pgm'],
+)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/ReindexIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/ReindexIT.java
new file mode 100644
index 0000000..4f9ef14
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/ReindexIT.java
@@ -0,0 +1,66 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.pgm;
+
+import static org.junit.Assert.assertEquals;
+
+import com.google.gerrit.acceptance.TempFileUtil;
+import com.google.gerrit.launcher.GerritLauncher;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+
+public class ReindexIT {
+  private File sitePath;
+
+  @Before
+  public void createTempDirectory() throws Exception {
+    sitePath = TempFileUtil.createTempDirectory();
+  }
+
+  @After
+  public void destroySite() throws Exception {
+    if (sitePath != null) {
+      TempFileUtil.cleanup();
+    }
+  }
+
+  @Test
+  public void reindexEmptySite() throws Exception {
+    initSite();
+    runGerrit("reindex", "-d", sitePath.getPath(),
+        "--show-stack-trace");
+  }
+
+  @Test
+  public void reindexEmptySiteWithRecheckMergeable() throws Exception {
+    initSite();
+    runGerrit("reindex", "-d", sitePath.getPath(),
+        "--show-stack-trace",
+        "--recheck-mergeable");
+  }
+
+  private void initSite() throws Exception {
+    runGerrit("init", "-d", sitePath.getPath(),
+        "--batch", "--no-auto-start", "--skip-plugins", "--show-stack-trace");
+  }
+
+  private static void runGerrit(String... args) throws Exception {
+    assertEquals(0, GerritLauncher.mainImpl(args));
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/BUCK
index 77e0419..f081ada 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/BUCK
@@ -2,10 +2,8 @@
 
 acceptance_tests(
   srcs = glob(['*IT.java']),
-  deps = [
-    ':util',
-    '//gerrit-acceptance-tests:lib',
-  ],
+  deps = [':util'],
+  labels = ['rest']
 )
 
 java_library(
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index fa906a4..6c36e37 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -31,12 +31,12 @@
 import com.google.gerrit.acceptance.SshSession;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
+import com.google.gerrit.extensions.common.InheritableBoolean;
+import com.google.gerrit.extensions.common.SubmitType;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
-import com.google.gerrit.reviewdb.client.Project.SubmitType;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
 import com.google.gerrit.server.change.ChangeJson.LabelInfo;
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/BUCK
index c76c9a6..f42a134 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/BUCK
@@ -11,16 +11,18 @@
 acceptance_tests(
   srcs = OTHER_TESTS,
   deps = [
-    '//gerrit-acceptance-tests:lib',
+    ':submit_util',
+    '//lib/joda:joda-time',
   ],
+  labels = ['rest'],
 )
 
 acceptance_tests(
   srcs = SUBMIT_TESTS,
   deps = [
     ':submit_util',
-    '//gerrit-acceptance-tests:lib',
   ],
+  labels = ['rest'],
 )
 
 java_library(
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
index 6352b50..acf50db 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.acceptance.rest.change;
 
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
@@ -23,14 +25,55 @@
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.testutil.ConfigSuite;
 
 import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.lib.Config;
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeUtils;
+import org.joda.time.DateTimeUtils.MillisProvider;
+import org.junit.After;
+import org.junit.Before;
 import org.junit.Test;
+import org.junit.runner.RunWith;
 
 import java.io.IOException;
 import java.util.Iterator;
+import java.util.concurrent.atomic.AtomicLong;
 
+@RunWith(ConfigSuite.class)
 public class ChangeMessagesIT extends AbstractDaemonTest {
+  private String systemTimeZone;
+  private volatile long clockStepMs;
+
+  @ConfigSuite.Config
+  public static Config noteDbEnabled() {
+    Config cfg = new Config();
+    cfg.setBoolean("notedb", null, "write", true);
+    cfg.setBoolean("notedb", "changeMessages", "read", true);
+    return cfg;
+  }
+
+  @Before
+  public void setTimeForTesting() {
+    systemTimeZone = System.setProperty("user.timezone", "US/Eastern");
+    clockStepMs = MILLISECONDS.convert(1, SECONDS);
+    final AtomicLong clockMs = new AtomicLong(
+        new DateTime(2009, 9, 30, 17, 0, 0).getMillis());
+
+    DateTimeUtils.setCurrentMillisProvider(new MillisProvider() {
+      @Override
+      public long getMillis() {
+        return clockMs.getAndAdd(clockStepMs);
+      }
+    });
+  }
+
+  @After
+  public void resetTime() {
+    DateTimeUtils.setCurrentMillisSystem();
+    System.setProperty("user.timezone", systemTimeZone);
+  }
 
   @Test
   public void messagesNotReturnedByDefault() throws Exception {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
new file mode 100644
index 0000000..2026ac1
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -0,0 +1,91 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.change;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeStatus;
+import com.google.gerrit.server.change.ChangeJson;
+
+import org.apache.http.HttpStatus;
+import org.junit.Test;
+
+public class CreateChangeIT extends AbstractDaemonTest {
+
+  @Test
+  public void createEmptyChange_MissingBranch() throws Exception {
+    ChangeInfo ci = new ChangeInfo();
+    ci.project = project.get();
+    RestResponse r = adminSession.post("/changes/", ci);
+    assertEquals(HttpStatus.SC_BAD_REQUEST, r.getStatusCode());
+    assertTrue(r.getEntityContent().contains("branch must be non-empty"));
+  }
+
+  @Test
+  public void createEmptyChange_MissingMessage() throws Exception {
+    ChangeInfo ci = new ChangeInfo();
+    ci.project = project.get();
+    ci.branch = "master";
+    RestResponse r = adminSession.post("/changes/", ci);
+    assertEquals(HttpStatus.SC_BAD_REQUEST, r.getStatusCode());
+    assertTrue(r.getEntityContent().contains("commit message must be non-empty"));
+  }
+
+  @Test
+  public void createEmptyChange_InvalidStatus() throws Exception {
+    ChangeInfo ci = newChangeInfo(ChangeStatus.SUBMITTED);
+    RestResponse r = adminSession.post("/changes/", ci);
+    assertEquals(HttpStatus.SC_BAD_REQUEST, r.getStatusCode());
+    assertTrue(r.getEntityContent().contains("unsupported change status"));
+  }
+
+  @Test
+  public void createNewChange() throws Exception {
+    assertChange(newChangeInfo(ChangeStatus.NEW));
+  }
+
+  @Test
+  public void createDraftChange() throws Exception {
+    assertChange(newChangeInfo(ChangeStatus.DRAFT));
+  }
+
+  private ChangeInfo newChangeInfo(ChangeStatus status) {
+    ChangeInfo in = new ChangeInfo();
+    in.project = project.get();
+    in.branch = "master";
+    in.subject = "Empty change";
+    in.topic = "support-gerrit-workflow-in-browser";
+    in.status = status;
+    return in;
+  }
+
+  private void assertChange(ChangeInfo in) throws Exception {
+    RestResponse r = adminSession.post("/changes/", in);
+    assertEquals(HttpStatus.SC_CREATED, r.getStatusCode());
+
+    ChangeJson.ChangeInfo info = newGson().fromJson(r.getReader(),
+        ChangeJson.ChangeInfo.class);
+    ChangeInfo out = get(info.changeId);
+
+    assertEquals(in.branch, out.branch);
+    assertEquals(in.subject, out.subject);
+    assertEquals(in.topic, out.topic);
+    assertEquals(in.status, out.status);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ListChangesOptionsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ListChangesOptionsIT.java
index 97ae2fd..02ecbd8 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ListChangesOptionsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ListChangesOptionsIT.java
@@ -16,6 +16,7 @@
 
 import static com.google.gerrit.extensions.common.ListChangesOption.ALL_REVISIONS;
 import static com.google.gerrit.extensions.common.ListChangesOption.CURRENT_REVISION;
+import static com.google.gerrit.extensions.common.ListChangesOption.MESSAGES;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
 
@@ -73,6 +74,15 @@
   }
 
   @Test
+  public void currentRevisionAndMessages() throws Exception {
+    ChangeInfo c = get(changeId, CURRENT_REVISION, MESSAGES);
+    assertEquals(1, c.revisions.size());
+    assertEquals(commitId(2), c.currentRevision);
+    assertEquals(ImmutableSet.of(commitId(2)), c.revisions.keySet());
+    assertEquals(3, c.revisions.get(commitId(2))._number);
+  }
+
+  @Test
   public void allRevisions() throws Exception {
     ChangeInfo c = get(changeId, ALL_REVISIONS);
     assertEquals(commitId(2), c.currentRevision);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
index d137e62..12d002e 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
@@ -19,8 +19,8 @@
 import static org.junit.Assert.assertSame;
 
 import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.reviewdb.client.Project.SubmitType;
 import com.google.gwtorm.server.OrmException;
+import com.google.gerrit.extensions.common.SubmitType;
 
 import com.jcraft.jsch.JSchException;
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
index d6fbca4..2ef4ecc 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
@@ -18,7 +18,7 @@
 import static org.junit.Assert.assertEquals;
 
 import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.reviewdb.client.Project.SubmitType;
+import com.google.gerrit.extensions.common.SubmitType;
 
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.revwalk.RevCommit;
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java
index 2a01d84..9512c7a 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java
@@ -18,7 +18,7 @@
 import static org.junit.Assert.assertEquals;
 
 import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.reviewdb.client.Project.SubmitType;
+import com.google.gerrit.extensions.common.SubmitType;
 import com.google.gwtorm.server.OrmException;
 
 import com.jcraft.jsch.JSchException;
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
index 486c529..fde462b 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
@@ -4,7 +4,7 @@
 import static org.junit.Assert.assertEquals;
 
 import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.reviewdb.client.Project.SubmitType;
+import com.google.gerrit.extensions.common.SubmitType;
 import com.google.gwtorm.server.OrmException;
 
 import com.jcraft.jsch.JSchException;
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java
index 8968084..7b4d23d 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java
@@ -18,7 +18,7 @@
 import static org.junit.Assert.assertEquals;
 
 import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.reviewdb.client.Project.SubmitType;
+import com.google.gerrit.extensions.common.SubmitType;
 
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.revwalk.RevCommit;
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/BUCK
new file mode 100644
index 0000000..c89da30
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/BUCK
@@ -0,0 +1,6 @@
+include_defs('//gerrit-acceptance-tests/tests.defs')
+
+acceptance_tests(
+  srcs = glob(['*IT.java']),
+  labels = ['rest']
+)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/CacheOperationsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/CacheOperationsIT.java
new file mode 100644
index 0000000..d2174bc
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/CacheOperationsIT.java
@@ -0,0 +1,166 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.config;
+
+import static com.google.gerrit.server.config.PostCaches.Operation.FLUSH;
+import static com.google.gerrit.server.config.PostCaches.Operation.FLUSH_ALL;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.server.project.Util.allow;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.config.ListCaches.CacheInfo;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.PostCaches;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.inject.Inject;
+
+import org.apache.http.HttpStatus;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+public class CacheOperationsIT extends AbstractDaemonTest {
+
+  @Inject
+  private ProjectCache projectCache;
+
+  @Inject
+  private AllProjectsName allProjects;
+
+  @Inject
+  private MetaDataUpdate.Server metaDataUpdateFactory;
+
+  @Test
+  public void flushAll() throws IOException {
+    RestResponse r = adminSession.get("/config/server/caches/project_list");
+    CacheInfo cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class);
+    assertTrue(cacheInfo.entries.mem.longValue() > 0);
+
+    r = adminSession.post("/config/server/caches/", new PostCaches.Input(FLUSH_ALL));
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    r.consume();
+
+    r = adminSession.get("/config/server/caches/project_list");
+    cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class);
+    assertNull(cacheInfo.entries.mem);
+  }
+
+  @Test
+  public void flushAll_Forbidden() throws IOException {
+    RestResponse r = userSession.post("/config/server/caches/",
+        new PostCaches.Input(FLUSH_ALL));
+    assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode());
+  }
+
+  @Test
+  public void flushAll_BadRequest() throws IOException {
+    RestResponse r = adminSession.post("/config/server/caches/",
+        new PostCaches.Input(FLUSH_ALL, Arrays.asList("projects")));
+    assertEquals(HttpStatus.SC_BAD_REQUEST, r.getStatusCode());
+  }
+
+  @Test
+  public void flush() throws IOException {
+    RestResponse r = adminSession.get("/config/server/caches/project_list");
+    CacheInfo cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class);
+    assertTrue(cacheInfo.entries.mem.longValue() > 0);
+
+    r = adminSession.get("/config/server/caches/projects");
+    cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class);
+    assertTrue(cacheInfo.entries.mem.longValue() > 1);
+
+    r = adminSession.post("/config/server/caches/",
+        new PostCaches.Input(FLUSH, Arrays.asList("accounts", "project_list")));
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    r.consume();
+
+    r = adminSession.get("/config/server/caches/project_list");
+    cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class);
+    assertNull(cacheInfo.entries.mem);
+
+    r = adminSession.get("/config/server/caches/projects");
+    cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class);
+    assertTrue(cacheInfo.entries.mem.longValue() > 1);
+  }
+
+  @Test
+  public void flush_Forbidden() throws IOException {
+    RestResponse r = userSession.post("/config/server/caches/",
+        new PostCaches.Input(FLUSH, Arrays.asList("projects")));
+    assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode());
+  }
+
+  @Test
+  public void flush_BadRequest() throws IOException {
+    RestResponse r = adminSession.post("/config/server/caches/",
+        new PostCaches.Input(FLUSH));
+    assertEquals(HttpStatus.SC_BAD_REQUEST, r.getStatusCode());
+  }
+
+  @Test
+  public void flush_UnprocessableEntity() throws IOException {
+    RestResponse r = adminSession.get("/config/server/caches/projects");
+    CacheInfo cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class);
+    assertTrue(cacheInfo.entries.mem.longValue() > 0);
+
+    r = adminSession.post("/config/server/caches/",
+        new PostCaches.Input(FLUSH, Arrays.asList("projects", "unprocessable")));
+    assertEquals(HttpStatus.SC_UNPROCESSABLE_ENTITY, r.getStatusCode());
+    r.consume();
+
+    r = adminSession.get("/config/server/caches/projects");
+    cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class);
+    assertTrue(cacheInfo.entries.mem.longValue() > 0);
+  }
+
+  @Test
+  public void flushWebSessions_Forbidden() throws IOException {
+    ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
+    AccountGroup.UUID registeredUsers =
+        SystemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
+    allow(cfg, GlobalCapability.VIEW_CACHES, registeredUsers);
+    allow(cfg, GlobalCapability.FLUSH_CACHES, registeredUsers);
+    saveProjectConfig(cfg);
+
+    RestResponse r = userSession.post("/config/server/caches/",
+        new PostCaches.Input(FLUSH, Arrays.asList("projects")));
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    r.consume();
+
+    r = userSession.post("/config/server/caches/",
+        new PostCaches.Input(FLUSH, Arrays.asList("web_sessions")));
+    assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode());
+  }
+
+  private void saveProjectConfig(ProjectConfig cfg) throws IOException {
+    MetaDataUpdate md = metaDataUpdateFactory.create(allProjects);
+    try {
+      cfg.commit(md);
+    } finally {
+      md.close();
+    }
+    projectCache.evict(allProjects);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java
new file mode 100644
index 0000000..aa8d7ba
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java
@@ -0,0 +1,116 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.config;
+
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.server.project.Util.allow;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.ListCaches.CacheInfo;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.inject.Inject;
+
+import org.apache.http.HttpStatus;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class FlushCacheIT extends AbstractDaemonTest {
+
+  @Inject
+  private ProjectCache projectCache;
+
+  @Inject
+  private AllProjectsName allProjects;
+
+  @Inject
+  private MetaDataUpdate.Server metaDataUpdateFactory;
+
+  @Test
+  public void flushCache() throws IOException {
+    RestResponse r = adminSession.get("/config/server/caches/groups");
+    CacheInfo result = newGson().fromJson(r.getReader(), CacheInfo.class);
+    assertTrue(result.entries.mem.longValue() > 0);
+
+    r = adminSession.post("/config/server/caches/groups/flush");
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    r.consume();
+
+    r = adminSession.get("/config/server/caches/groups");
+    result = newGson().fromJson(r.getReader(), CacheInfo.class);
+    assertNull(result.entries.mem);
+  }
+
+  @Test
+  public void flushCache_Forbidden() throws IOException {
+    RestResponse r = userSession.post("/config/server/caches/accounts/flush");
+    assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode());
+  }
+
+  @Test
+  public void flushCache_NotFound() throws IOException {
+    RestResponse r = adminSession.post("/config/server/caches/nonExisting/flush");
+    assertEquals(HttpStatus.SC_NOT_FOUND, r.getStatusCode());
+  }
+
+  @Test
+  public void flushCacheWithGerritPrefix() throws IOException {
+    RestResponse r = adminSession.post("/config/server/caches/gerrit-accounts/flush");
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+  }
+
+  @Test
+  public void flushWebSessionsCache() throws IOException {
+    RestResponse r = adminSession.post("/config/server/caches/web_sessions/flush");
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+  }
+
+  @Test
+  public void flushWebSessionsCache_Forbidden() throws IOException {
+    ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
+    AccountGroup.UUID registeredUsers =
+        SystemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
+    allow(cfg, GlobalCapability.VIEW_CACHES, registeredUsers);
+    allow(cfg, GlobalCapability.FLUSH_CACHES, registeredUsers);
+    saveProjectConfig(cfg);
+
+    RestResponse r = userSession.post("/config/server/caches/accounts/flush");
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    r.consume();
+
+    r = userSession.post("/config/server/caches/web_sessions/flush");
+    assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode());
+  }
+
+  private void saveProjectConfig(ProjectConfig cfg) throws IOException {
+    MetaDataUpdate md = metaDataUpdateFactory.create(allProjects);
+    try {
+      cfg.commit(md);
+    } finally {
+      md.close();
+    }
+    projectCache.evict(allProjects);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/GetCacheIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/GetCacheIT.java
new file mode 100644
index 0000000..398d0c8
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/GetCacheIT.java
@@ -0,0 +1,75 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.config;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.server.config.ListCaches.CacheInfo;
+import com.google.gerrit.server.config.ListCaches.CacheType;
+
+import org.apache.http.HttpStatus;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class GetCacheIT extends AbstractDaemonTest {
+
+  @Test
+  public void getCache() throws IOException {
+    RestResponse r = adminSession.get("/config/server/caches/accounts");
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    CacheInfo result = newGson().fromJson(r.getReader(), CacheInfo.class);
+
+    assertEquals("accounts", result.name);
+    assertEquals(CacheType.MEM, result.type);
+    assertEquals(1, result.entries.mem.longValue());
+    assertNotNull(result.averageGet);
+    assertTrue(result.averageGet.endsWith("s"));
+    assertNull(result.entries.disk);
+    assertNull(result.entries.space);
+    assertTrue(result.hitRatio.mem >= 0);
+    assertTrue(result.hitRatio.mem <= 100);
+    assertNull(result.hitRatio.disk);
+
+    userSession.get("/config/server/version").consume();
+    r = adminSession.get("/config/server/caches/accounts");
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    result = newGson().fromJson(r.getReader(), CacheInfo.class);
+    assertEquals(2, result.entries.mem.longValue());
+  }
+
+  @Test
+  public void getCache_Forbidden() throws IOException {
+    RestResponse r = userSession.get("/config/server/caches/accounts");
+    assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode());
+  }
+
+  @Test
+  public void getCache_NotFound() throws IOException {
+    RestResponse r = adminSession.get("/config/server/caches/nonExisting");
+    assertEquals(HttpStatus.SC_NOT_FOUND, r.getStatusCode());
+  }
+
+  @Test
+  public void getCacheWithGerritPrefix() throws IOException {
+    RestResponse r = adminSession.get("/config/server/caches/gerrit-accounts");
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/GetTaskIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/GetTaskIT.java
new file mode 100644
index 0000000..2feeab8
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/GetTaskIT.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.config;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.server.config.ListTasks.TaskInfo;
+import com.google.gson.reflect.TypeToken;
+
+import org.apache.http.HttpStatus;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.List;
+
+public class GetTaskIT extends AbstractDaemonTest {
+
+  @Test
+  public void getTask() throws IOException {
+    RestResponse r =
+        adminSession.get("/config/server/tasks/" + getLogFileCompressorTaskId());
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    TaskInfo info =
+        newGson().fromJson(r.getReader(),
+            new TypeToken<TaskInfo>() {}.getType());
+    assertNotNull(info.id);
+    Long.parseLong(info.id, 16);
+    assertEquals("Log File Compressor", info.command);
+    assertNotNull(info.startTime);
+  }
+
+  @Test
+  public void getTask_NotFound() throws IOException {
+    RestResponse r =
+        userSession.get("/config/server/tasks/" + getLogFileCompressorTaskId());
+    assertEquals(HttpStatus.SC_NOT_FOUND, r.getStatusCode());
+  }
+
+  private String getLogFileCompressorTaskId() throws IOException {
+    RestResponse r = adminSession.get("/config/server/tasks/");
+    List<TaskInfo> result =
+        newGson().fromJson(r.getReader(),
+            new TypeToken<List<TaskInfo>>() {}.getType());
+    r.consume();
+    for (TaskInfo info : result) {
+      if ("Log File Compressor".equals(info.command)) {
+        return info.id;
+      }
+    }
+    return null;
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/KillTaskIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/KillTaskIT.java
new file mode 100644
index 0000000..90cb7cc
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/KillTaskIT.java
@@ -0,0 +1,64 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.config;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.server.config.ListTasks.TaskInfo;
+import com.google.gson.reflect.TypeToken;
+
+import org.apache.http.HttpStatus;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.List;
+
+public class KillTaskIT extends AbstractDaemonTest {
+
+  @Test
+  public void killTask() throws IOException {
+    RestResponse r = adminSession.get("/config/server/tasks/");
+    List<TaskInfo> result = newGson().fromJson(r.getReader(),
+        new TypeToken<List<TaskInfo>>() {}.getType());
+    r.consume();
+    int taskCount = result.size();
+    assertTrue(taskCount > 0);
+
+    r = adminSession.delete("/config/server/tasks/" + result.get(0).id);
+    assertEquals(HttpStatus.SC_NO_CONTENT, r.getStatusCode());
+    r.consume();
+
+    r = adminSession.get("/config/server/tasks/");
+    result = newGson().fromJson(r.getReader(),
+        new TypeToken<List<TaskInfo>>() {}.getType());
+    r.consume();
+    assertEquals(taskCount - 1, result.size());
+  }
+
+  @Test
+  public void killTask_NotFound() throws IOException {
+    RestResponse r = adminSession.get("/config/server/tasks/");
+    List<TaskInfo> result = newGson().fromJson(r.getReader(),
+        new TypeToken<List<TaskInfo>>() {}.getType());
+    r.consume();
+    assertTrue(result.size() > 0);
+
+    r = userSession.delete("/config/server/tasks/" + result.get(0).id);
+    assertEquals(HttpStatus.SC_NOT_FOUND, r.getStatusCode());
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ListCachesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ListCachesIT.java
new file mode 100644
index 0000000..db7fb7f
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ListCachesIT.java
@@ -0,0 +1,103 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.config;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.Ordering;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.server.config.ListCaches.CacheInfo;
+import com.google.gerrit.server.config.ListCaches.CacheType;
+import com.google.gson.reflect.TypeToken;
+
+import org.apache.http.HttpStatus;
+import org.eclipse.jgit.util.Base64;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+public class ListCachesIT extends AbstractDaemonTest {
+
+  @Test
+  public void listCaches() throws IOException {
+    RestResponse r = adminSession.get("/config/server/caches/");
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    Map<String, CacheInfo> result =
+        newGson().fromJson(r.getReader(),
+            new TypeToken<Map<String, CacheInfo>>() {}.getType());
+
+    assertTrue(result.containsKey("accounts"));
+    CacheInfo accountsCacheInfo = result.get("accounts");
+    assertEquals(CacheType.MEM, accountsCacheInfo.type);
+    assertEquals(1, accountsCacheInfo.entries.mem.longValue());
+    assertNotNull(accountsCacheInfo.averageGet);
+    assertTrue(accountsCacheInfo.averageGet.endsWith("s"));
+    assertNull(accountsCacheInfo.entries.disk);
+    assertNull(accountsCacheInfo.entries.space);
+    assertTrue(accountsCacheInfo.hitRatio.mem >= 0);
+    assertTrue(accountsCacheInfo.hitRatio.mem <= 100);
+    assertNull(accountsCacheInfo.hitRatio.disk);
+
+    userSession.get("/config/server/version").consume();
+    r = adminSession.get("/config/server/caches/");
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    result = newGson().fromJson(r.getReader(),
+        new TypeToken<Map<String, CacheInfo>>() {}.getType());
+    assertEquals(2, result.get("accounts").entries.mem.longValue());
+  }
+
+  @Test
+  public void listCaches_Forbidden() throws IOException {
+    RestResponse r = userSession.get("/config/server/caches/");
+    assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode());
+  }
+
+  @Test
+  public void listCacheNames() throws IOException {
+    RestResponse r = adminSession.get("/config/server/caches/?format=LIST");
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    List<String> result =
+        newGson().fromJson(r.getReader(),
+            new TypeToken<List<String>>() {}.getType());
+    assertTrue(result.contains("accounts"));
+    assertTrue(result.contains("projects"));
+    assertTrue(Ordering.natural().isOrdered(result));
+  }
+
+  @Test
+  public void listCacheNamesTextList() throws IOException {
+    RestResponse r = adminSession.get("/config/server/caches/?format=TEXT_LIST");
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    String result = new String(Base64.decode(r.getEntityContent()), UTF_8.name());
+    List<String> list = Arrays.asList(result.split("\n"));
+    assertTrue(list.contains("accounts"));
+    assertTrue(list.contains("projects"));
+    assertTrue(Ordering.natural().isOrdered(list));
+  }
+
+  @Test
+  public void listCaches_BadRequest() throws IOException {
+    RestResponse r = adminSession.get("/config/server/caches/?format=NONSENSE");
+    assertEquals(HttpStatus.SC_BAD_REQUEST, r.getStatusCode());
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ListTasksIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ListTasksIT.java
new file mode 100644
index 0000000..205a701
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ListTasksIT.java
@@ -0,0 +1,65 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.config;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.server.config.ListTasks.TaskInfo;
+import com.google.gson.reflect.TypeToken;
+
+import org.apache.http.HttpStatus;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.List;
+
+public class ListTasksIT extends AbstractDaemonTest {
+
+  @Test
+  public void listTasks() throws IOException {
+    RestResponse r = adminSession.get("/config/server/tasks/");
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    List<TaskInfo> result =
+        newGson().fromJson(r.getReader(),
+            new TypeToken<List<TaskInfo>>() {}.getType());
+    assertTrue(result.size() > 0);
+    boolean foundLogFileCompressorTask = false;
+    for (TaskInfo info : result) {
+      if ("Log File Compressor".equals(info.command)) {
+        foundLogFileCompressorTask = true;
+      }
+      assertNotNull(info.id);
+      Long.parseLong(info.id, 16);
+      assertNotNull(info.command);
+      assertNotNull(info.startTime);
+    }
+    assertTrue(foundLogFileCompressorTask);
+  }
+
+  @Test
+  public void listTasksWithoutViewQueueCapability() throws IOException {
+    RestResponse r = userSession.get("/config/server/tasks/");
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    List<TaskInfo> result =
+        newGson().fromJson(r.getReader(),
+            new TypeToken<List<TaskInfo>>() {}.getType());
+
+    assertTrue(result.isEmpty());
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/BUCK
index 108cc8d..da34c1d 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/BUCK
@@ -6,6 +6,7 @@
     ':util',
     '//gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account:util',
   ],
+  labels = ['rest']
 )
 
 java_library(
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUCK
index 91511be..fa8b10e 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUCK
@@ -5,8 +5,8 @@
   deps = [
     ':branch',
     ':project',
-    '//gerrit-acceptance-tests:lib',
   ],
+  labels = ['rest']
 )
 
 java_library(
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BanCommitIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BanCommitIT.java
new file mode 100644
index 0000000..f4414a2
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BanCommitIT.java
@@ -0,0 +1,82 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.project;
+
+import static com.google.gerrit.acceptance.GitUtil.add;
+import static com.google.gerrit.acceptance.GitUtil.createCommit;
+import static com.google.gerrit.acceptance.GitUtil.pushHead;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.Iterables;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GitUtil.Commit;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.server.project.BanCommit;
+import com.google.gerrit.server.project.BanCommit.BanResultInfo;
+
+import org.apache.http.HttpStatus;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.transport.PushResult;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class BanCommitIT extends AbstractDaemonTest {
+
+  @Test
+  public void banCommit() throws IOException, GitAPIException {
+    add(git, "a.txt", "some content");
+    Commit c = createCommit(git, admin.getIdent(), "subject");
+
+    RestResponse r =
+        adminSession.put("/projects/" + project.get() + "/ban/",
+            BanCommit.Input.fromCommits(c.getCommit().getName()));
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    BanResultInfo info = newGson().fromJson(r.getReader(), BanResultInfo.class);
+    assertEquals(c.getCommit().getName(), Iterables.getOnlyElement(info.newlyBanned));
+    assertNull(info.alreadyBanned);
+    assertNull(info.ignored);
+
+    PushResult pushResult = pushHead(git, "refs/heads/master", false);
+    assertTrue(pushResult.getRemoteUpdate("refs/heads/master").getMessage()
+        .startsWith("contains banned commit"));
+  }
+
+  @Test
+  public void banAlreadyBannedCommit() throws IOException, GitAPIException {
+    RestResponse r =
+        adminSession.put("/projects/" + project.get() + "/ban/",
+            BanCommit.Input.fromCommits("a8a477efffbbf3b44169bb9a1d3a334cbbd9aa96"));
+    r.consume();
+
+    r = adminSession.put("/projects/" + project.get() + "/ban/",
+        BanCommit.Input.fromCommits("a8a477efffbbf3b44169bb9a1d3a334cbbd9aa96"));
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    BanResultInfo info = newGson().fromJson(r.getReader(), BanResultInfo.class);
+    assertEquals("a8a477efffbbf3b44169bb9a1d3a334cbbd9aa96", Iterables.getOnlyElement(info.alreadyBanned));
+    assertNull(info.newlyBanned);
+    assertNull(info.ignored);
+  }
+
+  @Test
+  public void banCommit_Forbidden() throws IOException {
+    RestResponse r =
+        userSession.put("/projects/" + project.get() + "/ban/",
+            BanCommit.Input.fromCommits("a8a477efffbbf3b44169bb9a1d3a334cbbd9aa96"));
+    assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode());
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
index a4bcdf2..23cd278 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
@@ -14,18 +14,20 @@
 
 package com.google.gerrit.acceptance.rest.project;
 
+import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.server.project.Util.allow;
+import static com.google.gerrit.server.project.Util.block;
 import static org.junit.Assert.assertEquals;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.server.config.AllProjectsNameProvider;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
 
@@ -44,7 +46,7 @@
   private ProjectCache projectCache;
 
   @Inject
-  private AllProjectsNameProvider allProjects;
+  private AllProjectsName allProjects;
 
   private Branch.NameKey branch;
 
@@ -130,29 +132,26 @@
   }
 
   private void blockCreateReference() throws IOException, ConfigInvalidException {
-    MetaDataUpdate md = metaDataUpdateFactory.create(allProjects.get());
-    md.setMessage(String.format("Block %s", Permission.CREATE));
-    ProjectConfig config = ProjectConfig.read(md);
-    AccessSection s = config.getAccessSection("refs/*", true);
-    Permission p = s.getPermission(Permission.CREATE, true);
-    PermissionRule rule = new PermissionRule(config.resolve(
-        SystemGroupBackend.getGroup(SystemGroupBackend.ANONYMOUS_USERS)));
-    rule.setBlock();
-    p.add(rule);
-    config.commit(md);
-    projectCache.evict(config.getProject());
+    ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
+    block(cfg, Permission.CREATE, ANONYMOUS_USERS, "refs/*");
+    saveProjectConfig(allProjects, cfg);
+    projectCache.evict(cfg.getProject());
   }
 
   private void grantOwner() throws IOException, ConfigInvalidException {
-    MetaDataUpdate md = metaDataUpdateFactory.create(project);
-    md.setMessage(String.format("Grant %s", Permission.OWNER));
-    ProjectConfig config = ProjectConfig.read(md);
-    AccessSection s = config.getAccessSection("refs/*", true);
-    Permission p = s.getPermission(Permission.OWNER, true);
-    PermissionRule rule = new PermissionRule(config.resolve(
-        SystemGroupBackend.getGroup(SystemGroupBackend.REGISTERED_USERS)));
-    p.add(rule);
-    config.commit(md);
-    projectCache.evict(config.getProject());
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    allow(cfg, Permission.OWNER, REGISTERED_USERS, "refs/*");
+    saveProjectConfig(project, cfg);
+    projectCache.evict(cfg.getProject());
+  }
+
+  private void saveProjectConfig(Project.NameKey p, ProjectConfig cfg)
+      throws IOException {
+    MetaDataUpdate md = metaDataUpdateFactory.create(p);
+    try {
+      cfg.commit(md);
+    } finally {
+      md.close();
+    }
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
index fe3d3af..d441f96 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
@@ -24,17 +24,20 @@
 import com.google.common.collect.Sets;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.extensions.api.projects.ProjectApi;
+import com.google.gerrit.extensions.api.projects.ProjectInput;
+import com.google.gerrit.extensions.common.InheritableBoolean;
+import com.google.gerrit.extensions.common.ProjectInfo;
+import com.google.gerrit.extensions.common.SubmitType;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
-import com.google.gerrit.reviewdb.client.Project.SubmitType;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.project.CreateProject;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectJson.ProjectInfo;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -65,6 +68,21 @@
   @Inject
   private GitRepositoryManager git;
 
+  @Inject
+  private GerritApi gApi;
+
+  @Test
+  public void testCreateProjectApi() throws RestApiException, IOException {
+    final String newProjectName = "newProject";
+    ProjectApi projectApi = gApi.projects().name(newProjectName).create();
+    ProjectInfo p = projectApi.get();
+    assertEquals(newProjectName, p.name);
+    ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
+    assertNotNull(projectState);
+    assertProjectInfo(projectState.getProject(), p);
+    assertHead(newProjectName, "refs/heads/master");
+  }
+
   @Test
   public void testCreateProject() throws IOException {
     final String newProjectName = "newProject";
@@ -80,7 +98,7 @@
 
   @Test
   public void testCreateProjectWithNameMismatch_BadRequest() throws IOException {
-    CreateProject.Input in = new CreateProject.Input();
+    ProjectInput in = new ProjectInput();
     in.name = "otherName";
     RestResponse r = adminSession.put("/projects/someName", in);
     assertEquals(HttpStatus.SC_BAD_REQUEST, r.getStatusCode());
@@ -89,7 +107,7 @@
   @Test
   public void testCreateProjectWithProperties() throws IOException {
     final String newProjectName = "newProject";
-    CreateProject.Input in = new CreateProject.Input();
+    ProjectInput in = new ProjectInput();
     in.description = "Test description";
     in.submitType = SubmitType.CHERRY_PICK;
     in.useContributorAgreements = InheritableBoolean.TRUE;
@@ -115,16 +133,17 @@
     RestResponse r = adminSession.put("/projects/" + parentName);
     r.consume();
     final String childName = "child";
-    CreateProject.Input in = new CreateProject.Input();
+    ProjectInput in = new ProjectInput();
     in.parent = parentName;
     r = adminSession.put("/projects/" + childName, in);
     Project project = projectCache.get(new Project.NameKey(childName)).getProject();
     assertEquals(in.parent, project.getParentName());
   }
 
+  @Test
   public void testCreateChildProjectUnderNonExistingParent_UnprocessableEntity()
       throws IOException {
-    CreateProject.Input in = new CreateProject.Input();
+    ProjectInput in = new ProjectInput();
     in.parent = "non-existing-project";
     RestResponse r = adminSession.put("/projects/child", in);
     assertEquals(HttpStatus.SC_UNPROCESSABLE_ENTITY, r.getStatusCode());
@@ -133,7 +152,7 @@
   @Test
   public void testCreateProjectWithOwner() throws IOException {
     final String newProjectName = "newProject";
-    CreateProject.Input in = new CreateProject.Input();
+    ProjectInput in = new ProjectInput();
     in.owners = Lists.newArrayListWithCapacity(3);
     in.owners.add("Anonymous Users"); // by name
     in.owners.add(SystemGroupBackend.REGISTERED_USERS.get()); // by UUID
@@ -148,9 +167,10 @@
     assertProjectOwners(expectedOwnerIds, projectState);
   }
 
+  @Test
   public void testCreateProjectWithNonExistingOwner_UnprocessableEntity()
       throws IOException {
-    CreateProject.Input in = new CreateProject.Input();
+    ProjectInput in = new ProjectInput();
     in.owners = Collections.singletonList("non-existing-group");
     RestResponse r = adminSession.put("/projects/newProject", in);
     assertEquals(HttpStatus.SC_UNPROCESSABLE_ENTITY, r.getStatusCode());
@@ -159,7 +179,7 @@
   @Test
   public void testCreatePermissionOnlyProject() throws IOException {
     final String newProjectName = "newProject";
-    CreateProject.Input in = new CreateProject.Input();
+    ProjectInput in = new ProjectInput();
     in.permissionsOnly = true;
     adminSession.put("/projects/" + newProjectName, in);
     assertHead(newProjectName, RefNames.REFS_CONFIG);
@@ -168,7 +188,7 @@
   @Test
   public void testCreateProjectWithEmptyCommit() throws IOException {
     final String newProjectName = "newProject";
-    CreateProject.Input in = new CreateProject.Input();
+    ProjectInput in = new ProjectInput();
     in.createEmptyCommit = true;
     adminSession.put("/projects/" + newProjectName, in);
     assertEmptyCommit(newProjectName, "refs/heads/master");
@@ -177,7 +197,7 @@
   @Test
   public void testCreateProjectWithBranches() throws IOException {
     final String newProjectName = "newProject";
-    CreateProject.Input in = new CreateProject.Input();
+    ProjectInput in = new ProjectInput();
     in.createEmptyCommit = true;
     in.branches = Lists.newArrayListWithCapacity(3);
     in.branches.add("refs/heads/test");
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
index 020fd43..0d6ec5b 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
@@ -14,18 +14,20 @@
 
 package com.google.gerrit.acceptance.rest.project;
 
+import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.server.project.Util.allow;
+import static com.google.gerrit.server.project.Util.block;
 import static org.junit.Assert.assertEquals;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.server.config.AllProjectsNameProvider;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
 
@@ -45,7 +47,7 @@
   private ProjectCache projectCache;
 
   @Inject
-  private AllProjectsNameProvider allProjects;
+  private AllProjectsName allProjects;
 
   private Branch.NameKey branch;
 
@@ -125,30 +127,25 @@
   }
 
   private void blockForcePush() throws IOException, ConfigInvalidException {
-    MetaDataUpdate md = metaDataUpdateFactory.create(allProjects.get());
-    md.setMessage(String.format("Block force %s", Permission.PUSH));
-    ProjectConfig config = ProjectConfig.read(md);
-    AccessSection s = config.getAccessSection("refs/heads/*", true);
-    Permission p = s.getPermission(Permission.PUSH, true);
-    PermissionRule rule = new PermissionRule(config.resolve(
-        SystemGroupBackend.getGroup(SystemGroupBackend.ANONYMOUS_USERS)));
-    rule.setForce(true);
-    rule.setBlock();
-    p.add(rule);
-    config.commit(md);
-    projectCache.evict(config.getProject());
+    ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
+    block(cfg, Permission.PUSH, ANONYMOUS_USERS, "refs/heads/*").setForce(true);
+    saveProjectConfig(allProjects, cfg);
+    projectCache.evict(cfg.getProject());
   }
 
   private void grantOwner() throws IOException, ConfigInvalidException {
-    MetaDataUpdate md = metaDataUpdateFactory.create(project);
-    md.setMessage(String.format("Grant %s", Permission.OWNER));
-    ProjectConfig config = ProjectConfig.read(md);
-    AccessSection s = config.getAccessSection("refs/*", true);
-    Permission p = s.getPermission(Permission.OWNER, true);
-    PermissionRule rule = new PermissionRule(config.resolve(
-        SystemGroupBackend.getGroup(SystemGroupBackend.REGISTERED_USERS)));
-    p.add(rule);
-    config.commit(md);
-    projectCache.evict(config.getProject());
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    allow(cfg, Permission.OWNER, REGISTERED_USERS, "refs/*");
+    saveProjectConfig(project, cfg);
+    projectCache.evict(cfg.getProject());
+  }
+
+  private void saveProjectConfig(Project.NameKey p, ProjectConfig cfg) throws IOException {
+    MetaDataUpdate md = metaDataUpdateFactory.create(p);
+    try {
+      cfg.commit(md);
+    } finally {
+      md.close();
+    }
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetChildProjectIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetChildProjectIT.java
index b037ff9..10d59d2d 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetChildProjectIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetChildProjectIT.java
@@ -21,10 +21,10 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.SshSession;
+import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectJson.ProjectInfo;
 import com.google.inject.Inject;
 
 import com.jcraft.jsch.JSchException;
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetCommitIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetCommitIT.java
new file mode 100644
index 0000000..2aacf20
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetCommitIT.java
@@ -0,0 +1,111 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.project;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.common.CommitInfo;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.project.ListBranches.BranchInfo;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.inject.Inject;
+
+import org.apache.http.HttpStatus;
+import org.eclipse.jgit.lib.ObjectId;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class GetCommitIT extends AbstractDaemonTest {
+
+  @Inject
+  private ProjectCache projectCache;
+
+  @Inject
+  private AllProjectsName allProjects;
+
+  @Inject
+  private MetaDataUpdate.Server metaDataUpdateFactory;
+
+  @Test
+  public void getCommit() throws IOException {
+    RestResponse r =
+        adminSession.get("/projects/" + project.get() + "/branches/"
+            + IdString.fromDecoded(RefNames.REFS_CONFIG).encoded());
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    BranchInfo branchInfo =
+        newGson().fromJson(r.getReader(), BranchInfo.class);
+    r.consume();
+
+    r = adminSession.get("/projects/" + project.get() + "/commits/"
+        + branchInfo.revision);
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    CommitInfo commitInfo =
+        newGson().fromJson(r.getReader(), CommitInfo.class);
+    assertEquals(branchInfo.revision, commitInfo.commit);
+    assertEquals("Created project", commitInfo.subject);
+    assertEquals("Created project\n", commitInfo.message);
+    assertNotNull(commitInfo.author);
+    assertEquals("Administrator", commitInfo.author.name);
+    assertNotNull(commitInfo.committer);
+    assertEquals("Gerrit Code Review", commitInfo.committer.name);
+    assertTrue(commitInfo.parents.isEmpty());
+  }
+
+  @Test
+  public void getNonExistingCommit_NotFound() throws IOException {
+    RestResponse r = adminSession.get("/projects/" + project.get() + "/commits/"
+        + ObjectId.zeroId().name());
+    assertEquals(HttpStatus.SC_NOT_FOUND, r.getStatusCode());
+  }
+
+  @Test
+  public void getNonVisibleCommit_NotFound() throws IOException {
+    RestResponse r =
+        adminSession.get("/projects/" + project.get() + "/branches/"
+            + IdString.fromDecoded(RefNames.REFS_CONFIG).encoded());
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    BranchInfo branchInfo =
+        newGson().fromJson(r.getReader(), BranchInfo.class);
+    r.consume();
+
+    ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
+    cfg.getAccessSection("refs/*", false).removePermission(Permission.READ);
+    saveProjectConfig(cfg);
+    projectCache.evict(cfg.getProject());
+
+    r = adminSession.get("/projects/" + project.get() + "/commits/"
+        + branchInfo.revision);
+    assertEquals(HttpStatus.SC_NOT_FOUND, r.getStatusCode());
+  }
+
+  private void saveProjectConfig(ProjectConfig cfg) throws IOException {
+    MetaDataUpdate md = metaDataUpdateFactory.create(allProjects);
+    try {
+      cfg.commit(md);
+    } finally {
+      md.close();
+    }
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
index 52bb623..e6fde73 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
@@ -16,19 +16,18 @@
 
 import static com.google.gerrit.acceptance.GitUtil.createProject;
 import static com.google.gerrit.acceptance.rest.project.BranchAssert.assertBranches;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.server.project.Util.block;
 import static org.junit.Assert.assertEquals;
 
 import com.google.common.collect.Lists;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.project.ListBranches.BranchInfo;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gson.reflect.TypeToken;
@@ -142,17 +141,10 @@
 
   private void blockRead(Project.NameKey project, String ref)
       throws RepositoryNotFoundException, IOException, ConfigInvalidException {
-    MetaDataUpdate md = metaDataUpdateFactory.create(project);
-    md.setMessage("Grant submit on " + ref);
-    ProjectConfig config = ProjectConfig.read(md);
-    AccessSection s = config.getAccessSection(ref, true);
-    Permission p = s.getPermission(Permission.READ, true);
-    PermissionRule rule = new PermissionRule(config.resolve(
-        SystemGroupBackend.getGroup(SystemGroupBackend.REGISTERED_USERS)));
-    rule.setBlock();
-    p.add(rule);
-    config.commit(md);
-    projectCache.evict(config.getProject());
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    block(cfg, Permission.READ, REGISTERED_USERS, ref);
+    saveProjectConfig(project, cfg);
+    projectCache.evict(cfg.getProject());
   }
 
   private static List<BranchInfo> toBranchInfoList(RestResponse r)
@@ -168,4 +160,13 @@
     PushOneCommit push = pushFactory.create(db, admin.getIdent());
     return push.to(git, ref);
   }
+
+  private void saveProjectConfig(Project.NameKey p, ProjectConfig cfg) throws IOException {
+    MetaDataUpdate md = metaDataUpdateFactory.create(p);
+    try {
+      cfg.commit(md);
+    } finally {
+      md.close();
+    }
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java
index c61764b..466a239 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java
@@ -21,9 +21,9 @@
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.AllProjectsName;
-import com.google.gerrit.server.project.ProjectJson.ProjectInfo;
 import com.google.gson.reflect.TypeToken;
 import com.google.inject.Inject;
 
@@ -53,7 +53,7 @@
     assertEquals(HttpStatus.SC_OK, r.getStatusCode());
     List<ProjectInfo> projectInfoList = toProjectInfoList(r);
     // Project 'p' was already created in the base class
-    assertTrue(projectInfoList.size() == 1);
+    assertTrue(projectInfoList.size() == 2);
   }
 
   @Test
@@ -67,7 +67,11 @@
 
     RestResponse r = GET("/projects/" + allProjects.get() + "/children/");
     assertEquals(HttpStatus.SC_OK, r.getStatusCode());
-    assertProjects(Arrays.asList(existingProject, child1, child2), toProjectInfoList(r));
+    assertProjects(
+        Arrays.asList(
+            new Project.NameKey("All-Users"),
+            existingProject, child1, child2),
+        toProjectInfoList(r));
   }
 
   @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
new file mode 100644
index 0000000..923d752
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
@@ -0,0 +1,236 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.project;
+
+import static com.google.gerrit.acceptance.GitUtil.createProject;
+import static com.google.gerrit.acceptance.rest.project.ProjectAssert.assertProjects;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.extensions.api.projects.ProjectInput;
+import com.google.gerrit.extensions.common.ProjectInfo;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gson.reflect.TypeToken;
+import com.google.inject.Inject;
+
+import com.jcraft.jsch.JSchException;
+
+import org.apache.http.HttpStatus;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Map;
+
+public class ListProjectsIT extends AbstractDaemonTest {
+
+  @Inject
+  private AllProjectsName allProjects;
+
+  @Inject
+  private AllUsersName allUsers;
+
+  @Test
+  public void listProjects() throws IOException, JSchException {
+    Project.NameKey someProject = new Project.NameKey("some-project");
+    createProject(sshSession, someProject.get());
+
+    RestResponse r = GET("/projects/");
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    Map<String, ProjectInfo> result = toProjectInfoMap(r);
+    assertProjects(Arrays.asList(allUsers, someProject, project),
+        result.values());
+  }
+
+  @Test
+  public void listProjectsWithBranch() throws IOException, JSchException {
+    RestResponse r = GET("/projects/?b=master");
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    Map<String, ProjectInfo> result = toProjectInfoMap(r);
+    assertNotNull(result.get(project.get()));
+    assertNotNull(result.get(project.get()).branches);
+    assertEquals(1, result.get(project.get()).branches.size());
+    assertNotNull(result.get(project.get()).branches.get("master"));
+  }
+
+  @Test
+  public void listProjectWithDescription() throws RestApiException, IOException {
+    ProjectInput projectInput = new ProjectInput();
+    projectInput.name = "some-project";
+    projectInput.description = "Description of some-project";
+    gApi.projects().name(projectInput.name).create(projectInput);
+
+    // description not be included in the results by default.
+    RestResponse r = GET("/projects/");
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    Map<String, ProjectInfo> result = toProjectInfoMap(r);
+    assertNotNull(result.get(projectInput.name));
+    assertNull(result.get(projectInput.name).description);
+
+    r = GET("/projects/?d");
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    result = toProjectInfoMap(r);
+    assertNotNull(result.get(projectInput.name));
+    assertEquals(projectInput.description,
+        result.get(projectInput.name).description);
+  }
+
+  @Test
+  public void listProjectsWithLimit() throws IOException, JSchException {
+    for (int i = 0; i < 5; i++) {
+      createProject(sshSession, new Project.NameKey("someProject" + i).get());
+    }
+
+    RestResponse r = GET("/projects/");
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    Map<String, ProjectInfo> result = toProjectInfoMap(r);
+    assertEquals(7, result.size()); // 5 plus 2 existing projects: p and
+                                    // All-Users
+
+    r = GET("/projects/?n=2");
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    result = toProjectInfoMap(r);
+    assertEquals(2, result.size());
+  }
+
+  @Test
+  public void listProjectsWithPrefix() throws IOException, JSchException {
+    Project.NameKey someProject = new Project.NameKey("some-project");
+    createProject(sshSession, someProject.get());
+    Project.NameKey someOtherProject =
+        new Project.NameKey("some-other-project");
+    createProject(sshSession, someOtherProject.get());
+    Project.NameKey projectAwesome = new Project.NameKey("project-awesome");
+    createProject(sshSession, projectAwesome.get());
+
+    RestResponse r = GET("/projects/?p=some");
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    Map<String, ProjectInfo> result = toProjectInfoMap(r);
+    assertProjects(Arrays.asList(someProject, someOtherProject),
+        result.values());
+  }
+
+  @Test
+  public void listProjectsWithRegex() throws IOException, JSchException {
+    Project.NameKey someProject = new Project.NameKey("some-project");
+    createProject(sshSession, someProject.get());
+    Project.NameKey someOtherProject =
+        new Project.NameKey("some-other-project");
+    createProject(sshSession, someOtherProject.get());
+    Project.NameKey projectAwesome = new Project.NameKey("project-awesome");
+    createProject(sshSession, projectAwesome.get());
+
+    RestResponse r = GET("/projects/?r=.*some");
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    Map<String, ProjectInfo> result = toProjectInfoMap(r);
+    assertProjects(Arrays.asList(projectAwesome), result.values());
+
+    r = GET("/projects/?r=[.*some");
+    assertEquals(HttpStatus.SC_BAD_REQUEST, r.getStatusCode());
+
+    r = GET("/projects/?r=.*");
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    result = toProjectInfoMap(r);
+    assertProjects(Arrays.asList(someProject, someOtherProject, projectAwesome,
+        project, allUsers), result.values());
+  }
+
+  @Test
+  public void listProjectsWithSkip() throws IOException, JSchException {
+    for (int i = 0; i < 5; i++) {
+      createProject(sshSession, new Project.NameKey("someProject" + i).get());
+    }
+
+    RestResponse r = GET("/projects/");
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    Map<String, ProjectInfo> result = toProjectInfoMap(r);
+    assertEquals(7, result.size()); // 5 plus 2 existing projects: p and
+                                    // All-Users
+
+    r = GET("/projects/?S=6");
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    result = toProjectInfoMap(r);
+    assertEquals(1, result.size());
+  }
+
+  @Test
+  public void listProjectsWithSubstring() throws IOException, JSchException {
+    Project.NameKey someProject = new Project.NameKey("some-project");
+    createProject(sshSession, someProject.get());
+    Project.NameKey someOtherProject =
+        new Project.NameKey("some-other-project");
+    createProject(sshSession, someOtherProject.get());
+    Project.NameKey projectAwesome = new Project.NameKey("project-awesome");
+    createProject(sshSession, projectAwesome.get());
+
+    RestResponse r = GET("/projects/?m=some");
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    Map<String, ProjectInfo> result = toProjectInfoMap(r);
+    assertProjects(
+        Arrays.asList(someProject, someOtherProject, projectAwesome),
+        result.values());
+  }
+
+  @Test
+  public void listProjectsWithTree() throws IOException, JSchException {
+    Project.NameKey someParentProject =
+        new Project.NameKey("some-parent-project");
+    createProject(sshSession, someParentProject.get());
+    Project.NameKey someChildProject =
+        new Project.NameKey("some-child-project");
+    createProject(sshSession, someChildProject.get(), someParentProject);
+
+    RestResponse r = GET("/projects/?tree");
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    Map<String, ProjectInfo> result = toProjectInfoMap(r);
+    assertNotNull(result.get(someChildProject.get()));
+    assertEquals(someParentProject.get(),
+        result.get(someChildProject.get()).parent);
+  }
+
+  @Test
+  public void listProjectWithType() throws RestApiException, IOException {
+    RestResponse r = GET("/projects/?type=PERMISSIONS");
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    Map<String, ProjectInfo> result = toProjectInfoMap(r);
+    assertEquals(1, result.size());
+    assertNotNull(result.get(allProjects.get()));
+
+    r = GET("/projects/?type=ALL");
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    result = toProjectInfoMap(r);
+    assertEquals(3, result.size());
+    assertProjects(Arrays.asList(allProjects, allUsers, project),
+        result.values());
+  }
+
+  private static Map<String, ProjectInfo> toProjectInfoMap(RestResponse r)
+      throws IOException {
+    Map<String, ProjectInfo> result =
+        newGson().fromJson(r.getReader(),
+            new TypeToken<Map<String, ProjectInfo>>() {}.getType());
+    return result;
+  }
+
+  private RestResponse GET(String endpoint) throws IOException {
+    return adminSession.get(endpoint);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectAssert.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectAssert.java
index 788a54b..3354cb8 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectAssert.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectAssert.java
@@ -21,24 +21,26 @@
 import com.google.common.base.Predicate;
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
+import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.project.ProjectJson.ProjectInfo;
 import com.google.gerrit.server.project.ProjectState;
 
-import java.util.List;
+import java.util.Collection;
 import java.util.Set;
 
 public class ProjectAssert {
 
   public static void assertProjects(Iterable<Project.NameKey> expected,
-      List<ProjectInfo> actual) {
+      Collection<ProjectInfo> actual) {
     for (final Project.NameKey p : expected) {
       ProjectInfo info = Iterables.find(actual, new Predicate<ProjectInfo>() {
         @Override
         public boolean apply(ProjectInfo info) {
-          return new Project.NameKey(info.name).equals(p);
+          // 'name' is not set if returned in a map, use the id instead.
+          return new Project.NameKey(info.name != null ? info.name : Url
+              .decode(info.id)).equals(p);
         }}, null);
       assertNotNull("missing project: " + p, info);
       actual.remove(info);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/BUCK
index 688e649..fce853b 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/BUCK
@@ -2,7 +2,5 @@
 
 acceptance_tests(
   srcs = glob(['*IT.java']),
-  deps = [
-    '//gerrit-acceptance-tests:lib',
-  ],
+  labels = ['server'],
 )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/BUCK
index 688e649..fce853b 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/BUCK
@@ -2,7 +2,5 @@
 
 acceptance_tests(
   srcs = glob(['*IT.java']),
-  deps = [
-    '//gerrit-acceptance-tests:lib',
-  ],
+  labels = ['server'],
 )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/CustomLabelIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/CustomLabelIT.java
index f38ac89..2a20a2c 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/CustomLabelIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/CustomLabelIT.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.acceptance.server.project;
 
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
+import static com.google.gerrit.server.project.Util.allow;
 import static com.google.gerrit.server.project.Util.category;
-import static com.google.gerrit.server.project.Util.grant;
 import static com.google.gerrit.server.project.Util.value;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
@@ -64,7 +64,7 @@
     ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
     AccountGroup.UUID anonymousUsers =
         SystemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID();
-    grant(cfg, Permission.forLabel(Q.getName()), -1, 1, anonymousUsers,
+    allow(cfg, Permission.forLabel(Q.getName()), -1, 1, anonymousUsers,
         "refs/heads/*");
     saveProjectConfig(cfg);
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/LabelTypeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/LabelTypeIT.java
index 6506040..bc311fd 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/LabelTypeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/LabelTypeIT.java
@@ -30,14 +30,23 @@
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.testutil.ConfigSuite;
 import com.google.inject.Inject;
 
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Repository;
 import org.junit.Before;
 import org.junit.Test;
 
 @NoHttpd
 public class LabelTypeIT extends AbstractDaemonTest {
+  @ConfigSuite.Config
+  public static Config noteDbEnabled() {
+    Config cfg = new Config();
+    cfg.setBoolean("notedb", null, "write", true);
+    cfg.setBoolean("notedb", "patchSetApprovals", "read", true);
+    return cfg;
+  }
 
   @Inject
   private GitRepositoryManager repoManager;
@@ -61,6 +70,7 @@
     codeReview.setCopyMaxScore(false);
     codeReview.setCopyAllScoresOnTrivialRebase(false);
     codeReview.setCopyAllScoresIfNoCodeChange(false);
+    codeReview.setDefaultValue((short)-1);
     saveProjectConfig(cfg);
   }
 
@@ -79,7 +89,7 @@
     saveLabelConfig();
     PushOneCommit.Result r = createChange();
     revision(r).review(ReviewInput.reject());
-    assertApproval(r, -2);
+    //assertApproval(r, -2);
     r = amendChange(r.getChangeId());
     assertApproval(r, -2);
   }
@@ -317,6 +327,7 @@
 
   private void doAssertApproval(int expected, ChangeInfo c) {
     LabelInfo cr = c.labels.get("Code-Review");
+    assertEquals(-1, (int) cr.defaultValue);
     assertEquals(1, cr.all.size());
     assertEquals("Administrator", cr.all.get(0).name);
     assertEquals(expected, cr.all.get(0).value.intValue());
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BUCK
index aa9703c..74b26ba6 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BUCK
@@ -3,4 +3,5 @@
 acceptance_tests(
   srcs = glob(['*IT.java']),
   deps = ['//gerrit-acceptance-tests:lib'],
+  labels = ['ssh'],
 )
diff --git a/gerrit-acceptance-tests/tests.defs b/gerrit-acceptance-tests/tests.defs
index 0131887..7bd2430 100644
--- a/gerrit-acceptance-tests/tests.defs
+++ b/gerrit-acceptance-tests/tests.defs
@@ -1,4 +1,4 @@
-# these need as workaround for the 'verify: false' bug in Jcraft SSH library
+# These are needed as workaround for the 'verify: false' bug in Jcraft SSH library
 BOUNCYCASTLE = [
   '//lib/bouncycastle:bcpkix',
   '//lib/bouncycastle:bcpg',
@@ -7,6 +7,8 @@
 def acceptance_tests(
     srcs,
     deps = [],
+    labels = [],
+    source_under_test = [],
     vm_args = ['-Xmx256m']):
   from os import environ, path
   if not environ.get('NO_BOUNCYCASTLE'):
@@ -23,8 +25,8 @@
         '//gerrit-httpd:httpd',
         '//gerrit-sshd:sshd',
         '//gerrit-server:server',
-      ],
-      labels = [
+      ] + source_under_test,
+      labels = labels + [
         'acceptance',
         'slow',
       ],
diff --git a/gerrit-antlr/BUCK b/gerrit-antlr/BUCK
index 57e7e1d..03c3c1e 100644
--- a/gerrit-antlr/BUCK
+++ b/gerrit-antlr/BUCK
@@ -17,8 +17,8 @@
 
 java_library(
   name = 'lib',
-  srcs = [genfile('query_antlr.src.zip')],
-  deps = PARSER_DEPS + [':query_antlr'],
+  srcs = [':query_antlr'],
+  deps = PARSER_DEPS,
 )
 
 # Hack necessary to expose ANTLR generated code as JAR to Eclipse.
@@ -28,9 +28,10 @@
   deps = [':lib'],
   out = 'query_parser.jar',
 )
+
 prebuilt_jar(
   name = 'query_parser',
-  binary_jar = genfile('query_parser.jar'),
-  deps = PARSER_DEPS + [':query_link'],
+  binary_jar = ':query_link',
+  deps = PARSER_DEPS,
   visibility = ['PUBLIC'],
 )
diff --git a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
index 85a051b..65bb034 100644
--- a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
+++ b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
@@ -14,20 +14,22 @@
 
 package com.google.gerrit.server.cache.h2;
 
-import com.google.common.base.Preconditions;
 import com.google.common.cache.Cache;
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
 import com.google.common.collect.Lists;
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
 import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.server.cache.CacheBinding;
 import com.google.gerrit.server.cache.PersistentCacheFactory;
 import com.google.gerrit.server.cache.h2.H2CacheImpl.SqlStore;
 import com.google.gerrit.server.cache.h2.H2CacheImpl.ValueHolder;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.plugins.Plugin;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import com.google.inject.TypeLiteral;
 
@@ -37,6 +39,7 @@
 
 import java.io.File;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
@@ -48,17 +51,18 @@
 
   private final DefaultCacheFactory defaultFactory;
   private final Config config;
-   private final File cacheDir;
+  private final File cacheDir;
   private final List<H2CacheImpl<?, ?>> caches;
+  private final DynamicMap<Cache<?, ?>> cacheMap;
   private final ExecutorService executor;
   private final ScheduledExecutorService cleanup;
-  private volatile boolean started;
 
   @Inject
   H2CacheFactory(
       DefaultCacheFactory defaultCacheFactory,
       @GerritServerConfig Config cfg,
-      SitePaths site) {
+      SitePaths site,
+      DynamicMap<Cache<?, ?>> cacheMap) {
     defaultFactory = defaultCacheFactory;
     config = cfg;
 
@@ -79,6 +83,7 @@
     }
 
     caches = Lists.newLinkedList();
+    this.cacheMap = cacheMap;
 
     if (cacheDir != null) {
       executor = Executors.newFixedThreadPool(
@@ -100,7 +105,6 @@
 
   @Override
   public void start() {
-    started = true;
     if (executor != null) {
       for (final H2CacheImpl<?, ?> cache : caches) {
         executor.execute(new Runnable() {
@@ -141,26 +145,30 @@
         log.warn("Interrupted waiting for disk cache to shutdown");
       }
     }
-    for (H2CacheImpl<?, ?> cache : caches) {
-      cache.stop();
+    synchronized (caches) {
+      for (H2CacheImpl<?, ?> cache : caches) {
+        cache.stop();
+      }
     }
   }
 
   @SuppressWarnings({"unchecked", "cast"})
   @Override
   public <K, V> Cache<K, V> build(CacheBinding<K, V> def) {
-    Preconditions.checkState(!started, "cache must be built before start");
     long limit = config.getLong("cache", def.name(), "diskLimit", 128 << 20);
 
     if (cacheDir == null || limit <= 0) {
       return defaultFactory.build(def);
     }
 
-    SqlStore<K, V> store = newSqlStore(def.name(), def.keyType(), limit);
+    SqlStore<K, V> store = newSqlStore(def.name(), def.keyType(), limit,
+        def.expireAfterWrite(TimeUnit.SECONDS));
     H2CacheImpl<K, V> cache = new H2CacheImpl<K, V>(
         executor, store, def.keyType(),
         (Cache<K, ValueHolder<V>>) defaultFactory.create(def, true).build());
-    caches.add(cache);
+    synchronized (caches) {
+      caches.add(cache);
+    }
     return cache;
   }
 
@@ -169,14 +177,14 @@
   public <K, V> LoadingCache<K, V> build(
       CacheBinding<K, V> def,
       CacheLoader<K, V> loader) {
-    Preconditions.checkState(!started, "cache must be built before start");
     long limit = config.getLong("cache", def.name(), "diskLimit", 128 << 20);
 
     if (cacheDir == null || limit <= 0) {
       return defaultFactory.build(def, loader);
     }
 
-    SqlStore<K, V> store = newSqlStore(def.name(), def.keyType(), limit);
+    SqlStore<K, V> store = newSqlStore(def.name(), def.keyType(), limit,
+        def.expireAfterWrite(TimeUnit.SECONDS));
     Cache<K, ValueHolder<V>> mem = (Cache<K, ValueHolder<V>>)
         defaultFactory.create(def, true)
         .build((CacheLoader<K, V>) new H2CacheImpl.Loader<K, V>(
@@ -187,12 +195,27 @@
     return cache;
   }
 
+  @Override
+  public void onStop(Plugin plugin) {
+    synchronized (caches) {
+      for (Map.Entry<String, Provider<Cache<?, ?>>> entry :
+          cacheMap.byPlugin(plugin.getName()).entrySet()) {
+        Cache<?, ?> cache = entry.getValue().get();
+        if (caches.remove(cache)) {
+          ((H2CacheImpl<?, ?>) cache).stop();
+        }
+      }
+    }
+  }
+
   private <V, K> SqlStore<K, V> newSqlStore(
       String name,
       TypeLiteral<K> keyType,
-      long maxSize) {
+      long maxSize,
+      Long expireAfterWrite) {
     File db = new File(cacheDir, name).getAbsoluteFile();
     String url = "jdbc:h2:" + db.toURI().toString();
-    return new SqlStore<K, V>(url, keyType, maxSize);
+    return new SqlStore<>(url, keyType, maxSize,
+        expireAfterWrite == null ? 0 : expireAfterWrite.longValue());
   }
 }
diff --git a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
index 4aca42b..5563988 100644
--- a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
+++ b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
@@ -11,6 +11,7 @@
 import com.google.common.hash.Funnel;
 import com.google.common.hash.Funnels;
 import com.google.common.hash.PrimitiveSink;
+import com.google.gerrit.server.cache.PersistentCache;
 import com.google.gerrit.server.util.TimeUtil;
 import com.google.inject.TypeLiteral;
 
@@ -63,7 +64,8 @@
  *
  * @see H2CacheFactory
  */
-public class H2CacheImpl<K, V> extends AbstractLoadingCache<K, V> {
+public class H2CacheImpl<K, V> extends AbstractLoadingCache<K, V> implements
+    PersistentCache {
   private static final Logger log = LoggerFactory.getLogger(H2CacheImpl.class);
 
   private final Executor executor;
@@ -115,7 +117,7 @@
 
   @Override
   public void put(final K key, V val) {
-    final ValueHolder<V> h = new ValueHolder<V>(val);
+    final ValueHolder<V> h = new ValueHolder<>(val);
     h.created = TimeUtil.nowMs();
     mem.put(key, h);
     executor.execute(new Runnable() {
@@ -156,6 +158,7 @@
     return mem.stats();
   }
 
+  @Override
   public DiskStats diskStats() {
     return store.diskStats();
   }
@@ -193,29 +196,6 @@
     }, delay, TimeUnit.MILLISECONDS);
   }
 
-  public static class DiskStats {
-    long size;
-    long space;
-    long hitCount;
-    long missCount;
-
-    public long size() {
-      return size;
-    }
-
-    public long space() {
-      return space;
-    }
-
-    public long hitCount() {
-      return hitCount;
-    }
-
-    public long requestCount() {
-      return hitCount + missCount;
-    }
-  }
-
   static class ValueHolder<V> {
     final V value;
     long created;
@@ -246,7 +226,7 @@
         }
       }
 
-      final ValueHolder<V> h = new ValueHolder<V>(loader.load(key));
+      final ValueHolder<V> h = new ValueHolder<>(loader.load(key));
       h.created = TimeUtil.nowMs();
       executor.execute(new Runnable() {
         @Override
@@ -302,7 +282,7 @@
       return (KeyType<K>) OTHER;
     }
 
-    static final KeyType<?> OTHER = new KeyType<Object>();
+    static final KeyType<?> OTHER = new KeyType<>();
     static final KeyType<String> STRING = new KeyType<String>() {
       @Override
       String columnType() {
@@ -333,20 +313,23 @@
     private final String url;
     private final KeyType<K> keyType;
     private final long maxSize;
+    private final long expireAfterWrite;
     private final BlockingQueue<SqlHandle> handles;
     private final AtomicLong hitCount = new AtomicLong();
     private final AtomicLong missCount = new AtomicLong();
     private volatile BloomFilter<K> bloomFilter;
     private int estimatedSize;
 
-    SqlStore(String jdbcUrl, TypeLiteral<K> keyType, long maxSize) {
+    SqlStore(String jdbcUrl, TypeLiteral<K> keyType, long maxSize,
+        long expireAfterWrite) {
       this.url = jdbcUrl;
       this.keyType = KeyType.create(keyType);
       this.maxSize = maxSize;
+      this.expireAfterWrite = expireAfterWrite;
 
       int cores = Runtime.getRuntime().availableProcessors();
       int keep = Math.min(cores, 16);
-      this.handles = new ArrayBlockingQueue<SqlHandle>(keep);
+      this.handles = new ArrayBlockingQueue<>(keep);
     }
 
     synchronized void open() {
@@ -428,7 +411,7 @@
       try {
         c = acquire();
         if (c.get == null) {
-          c.get = c.conn.prepareStatement("SELECT v FROM data WHERE k=?");
+          c.get = c.conn.prepareStatement("SELECT v, created FROM data WHERE k=?");
         }
         keyType.set(c.get, 1, key);
         ResultSet r = c.get.executeQuery();
@@ -438,9 +421,16 @@
             return null;
           }
 
+          Timestamp created = r.getTimestamp(2);
+          if (expired(created)) {
+            invalidate(key);
+            missCount.incrementAndGet();
+            return null;
+          }
+
           @SuppressWarnings("unchecked")
           V val = (V) r.getObject(1);
-          ValueHolder<V> h = new ValueHolder<V>(val);
+          ValueHolder<V> h = new ValueHolder<>(val);
           h.clean = true;
           hitCount.incrementAndGet();
           touch(c, key);
@@ -458,6 +448,14 @@
       }
     }
 
+    private boolean expired(Timestamp created) {
+      if (expireAfterWrite == 0) {
+        return false;
+      }
+      long age = TimeUtil.nowMs() - created.getTime();
+      return 1000 * expireAfterWrite < age;
+    }
+
     private void touch(SqlHandle c, K key) throws SQLException {
       if (c.touch == null) {
         c.touch =c.conn.prepareStatement("UPDATE data SET accessed=? WHERE k=?");
@@ -572,12 +570,14 @@
           r = s.executeQuery("SELECT"
               + " k"
               + ",OCTET_LENGTH(k) + OCTET_LENGTH(v)"
+              + ",created"
               + " FROM data"
               + " ORDER BY accessed");
           try {
             while (maxSize < used && r.next()) {
               K key = keyType.get(r, 1);
-              if (mem.getIfPresent(key) != null) {
+              Timestamp created = r.getTimestamp(3);
+              if (mem.getIfPresent(key) != null && !expired(created)) {
                 touch(c, key);
               } else {
                 invalidate(c, key);
@@ -599,9 +599,8 @@
     }
 
     DiskStats diskStats() {
-      DiskStats d = new DiskStats();
-      d.hitCount = hitCount.get();
-      d.missCount = missCount.get();
+      long size = 0;
+      long space = 0;
       SqlHandle c = null;
       try {
         c = acquire();
@@ -613,8 +612,8 @@
               + " FROM data");
           try {
             if (r.next()) {
-              d.size = r.getLong(1);
-              d.space = r.getLong(2);
+              size = r.getLong(1);
+              space = r.getLong(2);
             }
           } finally {
             r.close();
@@ -628,7 +627,7 @@
       } finally {
         release(c);
       }
-      return d;
+      return new DiskStats(size, space, hitCount.get(), missCount.get());
     }
 
     private SqlHandle acquire() throws SQLException {
diff --git a/gerrit-common/BUCK b/gerrit-common/BUCK
index 9ed1624..3ba22c5 100644
--- a/gerrit-common/BUCK
+++ b/gerrit-common/BUCK
@@ -6,6 +6,11 @@
   SRC + 'common/auth/SignInRequired.java',
 ]
 
+EXCLUDES = [
+  SRC + 'common/PluginData.java',
+  SRC + 'common/FileUtil.java',
+]
+
 java_library(
   name = 'annotations',
   srcs = ANNOTATIONS,
@@ -14,15 +19,14 @@
 
 gwt_module(
   name = 'client',
-  srcs = glob([SRC + 'common/**/*.java']),
-  gwtxml = SRC + 'Common.gwt.xml',
+  srcs = glob([SRC + 'common/**/*.java'], excludes = EXCLUDES),
+  gwt_xml = SRC + 'Common.gwt.xml',
   deps = [
+    ':annotations',
+    '//gerrit-extension-api:client',
     '//gerrit-patch-jgit:client',
     '//gerrit-prettify:client',
     '//gerrit-reviewdb:client',
-  ],
-  compile_deps = [
-    ':annotations',
     '//lib:gwtjsonrpc',
     '//lib:gwtorm',
     '//lib/jgit:jgit',
@@ -35,11 +39,13 @@
   srcs = glob([SRC + 'common/**/*.java'], excludes = ANNOTATIONS),
   deps = [
     ':annotations',
+    '//gerrit-extension-api:api',
     '//gerrit-patch-jgit:server',
     '//gerrit-prettify:server',
     '//gerrit-reviewdb:server',
     '//lib:gwtjsonrpc',
     '//lib:gwtorm',
+    '//lib:guava',
     '//lib/jgit:jgit',
   ],
   visibility = ['PUBLIC'],
@@ -50,6 +56,7 @@
   srcs = glob(['src/test/java/**/*.java']),
   deps = [
     ':client',
+    '//lib:guava',
     '//lib:junit',
   ],
   source_under_test = [':client'],
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/Die.java b/gerrit-common/src/main/java/com/google/gerrit/common/Die.java
similarity index 95%
rename from gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/Die.java
rename to gerrit-common/src/main/java/com/google/gerrit/common/Die.java
index 96e2ec8..6a1f304 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/Die.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/Die.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.pgm.util;
+package com.google.gerrit.common;
 
 public class Die extends RuntimeException {
   private static final long serialVersionUID = 1L;
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/FileUtil.java b/gerrit-common/src/main/java/com/google/gerrit/common/FileUtil.java
new file mode 100644
index 0000000..bed10d6
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/FileUtil.java
@@ -0,0 +1,66 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.common;
+
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.IO;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.Arrays;
+
+public class FileUtil {
+  public static boolean modified(FileBasedConfig cfg) throws IOException {
+    byte[] curVers;
+    try {
+      curVers = IO.readFully(cfg.getFile());
+    } catch (FileNotFoundException notFound) {
+      return true;
+    }
+
+    byte[] newVers = Constants.encode(cfg.toText());
+    return !Arrays.equals(curVers, newVers);
+  }
+
+  public static void mkdir(final File path) {
+    if (!path.isDirectory() && !path.mkdir()) {
+      throw new Die("Cannot make directory " + path);
+    }
+  }
+
+  public static void chmod(final int mode, final File path) {
+    path.setReadable(false, false /* all */);
+    path.setWritable(false, false /* all */);
+    path.setExecutable(false, false /* all */);
+
+    path.setReadable((mode & 0400) == 0400, true /* owner only */);
+    path.setWritable((mode & 0200) == 0200, true /* owner only */);
+    if (path.isDirectory() || (mode & 0100) == 0100) {
+      path.setExecutable(true, true /* owner only */);
+    }
+
+    if ((mode & 0044) == 0044) {
+      path.setReadable(true, false /* all */);
+    }
+    if ((mode & 0011) == 0011) {
+      path.setExecutable(true, false /* all */);
+    }
+  }
+
+  private FileUtil() {
+  }
+}
\ No newline at end of file
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java b/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
index e4199c2..c0382da 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
@@ -44,6 +44,7 @@
   public static final String ADMIN_PROJECTS = "/admin/projects/";
   public static final String ADMIN_CREATE_PROJECT = "/admin/create-project/";
   public static final String ADMIN_PLUGINS = "/admin/plugins/";
+  public static final String MY_GROUPS = "/groups/self";
 
   public static String toChange(final ChangeInfo c) {
     return toChange(c.getId());
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/PluginData.java b/gerrit-common/src/main/java/com/google/gerrit/common/PluginData.java
new file mode 100644
index 0000000..fc5bb56
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/PluginData.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.package com.google.gerrit.common;
+
+package com.google.gerrit.common;
+
+import com.google.common.base.Objects;
+
+import java.io.File;
+
+public class PluginData {
+  public final String name;
+  public final String version;
+  public final File pluginFile;
+
+  public PluginData(String name, String version, File pluginFile) {
+    this.name = name;
+    this.version = version;
+    this.pluginFile = pluginFile;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj instanceof PluginData) {
+      PluginData o = (PluginData) obj;
+      return Objects.equal(name, o.name) && Objects.equal(version, o.version)
+          && Objects.equal(pluginFile, o.pluginFile);
+    }
+    return super.equals(obj);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(name, version, pluginFile);
+  }
+}
\ No newline at end of file
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/auth/openid/OpenIdUrls.java b/gerrit-common/src/main/java/com/google/gerrit/common/auth/openid/OpenIdUrls.java
index 706f465..79c17f4 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/auth/openid/OpenIdUrls.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/auth/openid/OpenIdUrls.java
@@ -18,7 +18,6 @@
   public static final String OPENID_IDENTIFIER = "openid_identifier";
   public static final String LASTID_COOKIE = "gerrit.last_openid";
 
+  public static final String URL_LAUNCHPAD = "https://login.launchpad.net/+openid";
   public static final String URL_YAHOO = "https://me.yahoo.com";
-  public static final String URL_GOOGLE =
-      "https://www.google.com/accounts/o8/id";
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountInfo.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountInfo.java
index 6b8c075..8614be5 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountInfo.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountInfo.java
@@ -21,6 +21,7 @@
   protected Account.Id id;
   protected String fullName;
   protected String preferredEmail;
+  protected String username;
 
   protected AccountInfo() {
   }
@@ -45,6 +46,7 @@
     id = a.getId();
     fullName = a.getFullName();
     preferredEmail = a.getPreferredEmail();
+    username = a.getUserName();
   }
 
   /** @return the unique local id of the account */
@@ -70,6 +72,11 @@
     preferredEmail = email;
   }
 
+  /** @return the username of the account holder */
+  public String getUsername() {
+    return username;
+  }
+
   /**
    * Formats an account name.
    * <p>
@@ -87,7 +94,7 @@
   }
 
   /**
-   * Formats an account as an name and an email address.
+   * Formats an account as a name and an email address.
    * <p>
    * Example output:
    * <ul>
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java
index 3342bc2..e067f06 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.common.data;
 
+import com.google.gerrit.extensions.common.SubmitType;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
 
 import java.util.List;
 import java.util.Set;
@@ -41,7 +41,7 @@
   protected List<PatchSet> patchSets;
   protected Set<PatchSet.Id> patchSetsWithDraftComments;
   protected List<SubmitRecord> submitRecords;
-  protected Project.SubmitType submitType;
+  protected SubmitType submitType;
   protected SubmitTypeRecord submitTypeRecord;
   protected boolean canSubmit;
   protected List<ChangeMessage> messages;
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 38afaab..66309ea 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
@@ -22,6 +22,7 @@
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.reviewdb.client.Project;
 
+import java.util.List;
 import java.util.Set;
 
 public class GerritConfig implements Cloneable {
@@ -33,7 +34,7 @@
   protected String httpPasswordUrl;
   protected String reportBugUrl;
   protected String reportBugText;
-  protected boolean gitBasicAuth;
+  protected boolean httpPasswordSettingsEnabled = true;
 
   protected GitwebConfig gitweb;
   protected boolean useContributorAgreements;
@@ -53,6 +54,7 @@
   protected int suggestFrom;
   protected int changeUpdateDelay;
   protected AccountGeneralPreferences.ChangeScreen changeScreen;
+  protected List<String> archiveFormats;
   protected int largeChangeSize;
   protected boolean newFeatures;
 
@@ -112,12 +114,12 @@
     reportBugText = t;
   }
 
-  public boolean isGitBasicAuth() {
-    return gitBasicAuth;
+  public boolean isHttpPasswordSettingsEnabled() {
+    return httpPasswordSettingsEnabled;
   }
 
-  public void setGitBasicAuth(boolean gba) {
-    gitBasicAuth = gba;
+  public void setHttpPasswordSettingsEnabled(boolean httpPasswordSettingsEnabled) {
+    this.httpPasswordSettingsEnabled = httpPasswordSettingsEnabled;
   }
 
   public String getEditFullNameUrl() {
@@ -291,6 +293,14 @@
     this.largeChangeSize = largeChangeSize;
   }
 
+  public List<String> getArchiveFormats() {
+    return archiveFormats;
+  }
+
+  public void setArchiveFormats(List<String> formats) {
+    archiveFormats = formats;
+  }
+
   public boolean getNewFeatures() {
     return newFeatures;
   }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java
index c596c1e..5018df9 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java
@@ -60,20 +60,6 @@
     return name;
   }
 
-  public static String defaultAbbreviation(String name) {
-    StringBuilder abbr = new StringBuilder();
-    for (int i = 0; i < name.length(); i++) {
-      char c = name.charAt(i);
-      if (c >= 'A' && c <= 'Z') {
-        abbr.append(c);
-      }
-    }
-    if (abbr.length() == 0) {
-      abbr.append(Character.toUpperCase(name.charAt(0)));
-    }
-    return abbr.toString();
-  }
-
   private static List<LabelValue> sortValues(List<LabelValue> values) {
     values = new ArrayList<>(values);
     if (values.size() <= 1) {
@@ -102,12 +88,12 @@
 
   protected String name;
 
-  protected String abbreviation;
   protected String functionName;
   protected boolean copyMinScore;
   protected boolean copyMaxScore;
   protected boolean copyAllScoresOnTrivialRebase;
   protected boolean copyAllScoresIfNoCodeChange;
+  protected short defaultValue;
 
   protected List<LabelValue> values;
   protected short maxNegative;
@@ -125,8 +111,8 @@
     this.name = checkName(name);
     canOverride = true;
     values = sortValues(valueList);
+    defaultValue = 0;
 
-    abbreviation = defaultAbbreviation(name);
     functionName = "MaxWithBlock";
 
     maxNegative = Short.MIN_VALUE;
@@ -149,14 +135,6 @@
     return psa.getLabelId().get().equalsIgnoreCase(name);
   }
 
-  public String getAbbreviation() {
-    return abbreviation;
-  }
-
-  public void setAbbreviation(String abbreviation) {
-    this.abbreviation = abbreviation;
-  }
-
   public String getFunctionName() {
     return functionName;
   }
@@ -200,6 +178,14 @@
     return v.getValue() > 0 ? v : null;
   }
 
+  public short getDefaultValue() {
+    return defaultValue;
+  }
+
+  public void setDefaultValue(short defaultValue) {
+    this.defaultValue = defaultValue;
+  }
+
   public boolean isCopyMinScore() {
     return copyMinScore;
   }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelTypes.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelTypes.java
index b47445e..18928f2 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelTypes.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelTypes.java
@@ -33,7 +33,7 @@
 
   public LabelTypes(final List<? extends LabelType> approvals) {
     labelTypes =
-        Collections.unmodifiableList(new ArrayList<LabelType>(approvals));
+        Collections.unmodifiableList(new ArrayList<>(approvals));
   }
 
   public List<LabelType> getLabelTypes() {
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
index 0326e02..2379b4a 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
@@ -48,7 +48,7 @@
   private static final int labelAsIndex;
 
   static {
-    NAMES_LC = new ArrayList<String>();
+    NAMES_LC = new ArrayList<>();
     NAMES_LC.add(OWNER.toLowerCase());
     NAMES_LC.add(READ.toLowerCase());
     NAMES_LC.add(ABANDON.toLowerCase());
@@ -229,7 +229,7 @@
 
   private void initRules() {
     if (rules == null) {
-      rules = new ArrayList<PermissionRule>(4);
+      rules = new ArrayList<>(4);
     }
   }
 
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAccess.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAccess.java
index ea0c8d2..7041dcc 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAccess.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAccess.java
@@ -29,7 +29,6 @@
   protected Set<String> ownerOf;
   protected boolean isConfigVisible;
   protected boolean canUpload;
-  protected boolean canChangeParent;
   protected LabelTypes labelTypes;
   protected Map<String, String> capabilities;
   protected Map<AccountGroup.UUID, GroupInfo> groupInfo;
@@ -110,14 +109,6 @@
     this.canUpload = canUpload;
   }
 
-  public boolean canChangeParent() {
-    return canChangeParent;
-  }
-
-  public void setCanChangeParent(boolean canChangeParent) {
-    this.canChangeParent = canChangeParent;
-  }
-
   public LabelTypes getLabelTypes() {
     return labelTypes;
   }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitTypeRecord.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitTypeRecord.java
index 4eea798..7b19f4c 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitTypeRecord.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitTypeRecord.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.extensions.common.SubmitType;
 
 /**
  * Describes the submit type for a change.
@@ -31,7 +31,7 @@
     RULE_ERROR
   }
 
-  public static SubmitTypeRecord OK(Project.SubmitType type) {
+  public static SubmitTypeRecord OK(SubmitType type) {
     SubmitTypeRecord r = new SubmitTypeRecord();
     r.status = Status.OK;
     r.type = type;
@@ -39,7 +39,7 @@
   }
 
   public Status status;
-  public Project.SubmitType type;
+  public SubmitType type;
   public String errorMessage;
 
   public String toString() {
diff --git a/gerrit-common/src/test/java/com/google/gerrit/common/data/ParameterizedStringTest.java b/gerrit-common/src/test/java/com/google/gerrit/common/data/ParameterizedStringTest.java
index 615f49f..b350a27 100644
--- a/gerrit-common/src/test/java/com/google/gerrit/common/data/ParameterizedStringTest.java
+++ b/gerrit-common/src/test/java/com/google/gerrit/common/data/ParameterizedStringTest.java
@@ -14,15 +14,17 @@
 
 package com.google.gerrit.common.data;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.ImmutableMap;
+
 import org.junit.Test;
 
 import java.util.HashMap;
 import java.util.Map;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-
 public class ParameterizedStringTest {
   @Test
   public void testEmptyString() {
@@ -394,4 +396,32 @@
     assertEquals("foo@example.com", p.bind(a)[0]);
     assertEquals("foo@example.com", p.replace(a));
   }
+
+  @Test
+  public void testReplaceSubmitTooltipWithVariables() {
+    ParameterizedString p = new ParameterizedString(
+        "Submit patch set ${patchSet} into ${branch}");
+    assertEquals(2, p.getParameterNames().size());
+    assertTrue(p.getParameterNames().contains("patchSet"));
+
+    Map<String, String> params = ImmutableMap.of(
+        "patchSet", "42",
+        "branch", "foo");
+    assertNotNull(p.bind(params));
+    assertEquals(2, p.bind(params).length);
+    assertEquals("42", p.bind(params)[0]);
+    assertEquals("foo", p.bind(params)[1]);
+    assertEquals("Submit patch set 42 into foo", p.replace(params));
+  }
+
+  @Test
+  public void testReplaceSubmitTooltipWithoutVariables() {
+    ParameterizedString p = new ParameterizedString(
+            "Submit patch set 40 into master");
+    Map<String, String> params = ImmutableMap.of(
+        "patchSet", "42",
+        "branch", "foo");
+    assertEquals(0, p.bind(params).length);
+    assertEquals("Submit patch set 40 into master", p.replace(params));
+  }
 }
diff --git a/gerrit-extension-api/BUCK b/gerrit-extension-api/BUCK
index 0302afd..aad79d7 100644
--- a/gerrit-extension-api/BUCK
+++ b/gerrit-extension-api/BUCK
@@ -4,10 +4,13 @@
 gwt_module(
   name = 'client',
   srcs = glob([
+    SRC + 'api/projects/ProjectState.java',
+    SRC + 'common/InheritableBoolean.java',
+    SRC + 'common/ListChangesOption.java',
+    SRC + 'common/SubmitType.java',
     SRC + 'webui/GerritTopMenu.java',
-    SRC + 'common/ListChangesOption.java'
   ]),
-  gwtxml = SRC + 'Extensions.gwt.xml',
+  gwt_xml = SRC + 'Extensions.gwt.xml',
   visibility = ['PUBLIC'],
 )
 
@@ -28,10 +31,10 @@
   visibility = ['PUBLIC'],
 )
 
-java_library2(
+java_library(
   name = 'api',
   srcs = glob([SRC + '**/*.java']),
-  compile_deps = ['//lib/guice:guice'],
+  provided_deps = ['//lib/guice:guice'],
   visibility = ['PUBLIC'],
 )
 
@@ -45,7 +48,7 @@
   name = 'extension-api-javadoc',
   title = 'Gerrit Review Extension API Documentation',
   pkg = 'com.google.gerrit.extensions',
-  paths = ['$SRCDIR/src/main/java'],
+  paths = ['src/main/java'],
   srcs = SRCS,
   deps = [
     '//lib/guice:javax-inject',
diff --git a/gerrit-extension-api/pom.xml b/gerrit-extension-api/pom.xml
index a2e3b4c..8e99a3f 100644
--- a/gerrit-extension-api/pom.xml
+++ b/gerrit-extension-api/pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-extension-api</artifactId>
-  <version>2.9.5</version>
+  <version>2.10.8</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Extension API</name>
   <description>API for Gerrit Extensions</description>
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/Extensions.gwt.xml b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/Extensions.gwt.xml
index df53ef1..757e046 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/Extensions.gwt.xml
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/Extensions.gwt.xml
@@ -14,6 +14,7 @@
  limitations under the License.
 -->
 <module>
-  <source path='webui' />
+  <source path='api' />
   <source path='common' />
+  <source path='webui' />
 </module>
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RootRelative.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RootRelative.java
new file mode 100644
index 0000000..a812b53
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RootRelative.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.annotations;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation applied to HttpServletRequest and HttpServletResponse
+ * when they are inherited from Gerrit instead of being injected by
+ * a plugin's ServletModule.  This means that the path returned by
+ * 'javax.servlet.http.HttpServletRequest#getPathInfo()' is
+ * relative to the Gerrit root instead of a path within the plugin's
+ * URL space.
+ */
+@Target({ElementType.PARAMETER, ElementType.FIELD})
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface RootRelative {
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/GerritApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/GerritApi.java
index 844807b..cc5807b 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/GerritApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/GerritApi.java
@@ -14,10 +14,34 @@
 
 package com.google.gerrit.extensions.api;
 
+import com.google.gerrit.extensions.api.accounts.Accounts;
 import com.google.gerrit.extensions.api.changes.Changes;
 import com.google.gerrit.extensions.api.projects.Projects;
+import com.google.gerrit.extensions.restapi.NotImplementedException;
 
 public interface GerritApi {
+  public Accounts accounts();
   public Changes changes();
   public Projects projects();
+
+  /**
+   * A default implementation which allows source compatibility
+   * when adding new methods to the interface.
+   **/
+  public class NotImplemented implements GerritApi {
+    @Override
+    public Accounts accounts() {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public Changes changes() {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public Projects projects() {
+      throw new NotImplementedException();
+    }
+  }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/AccountApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
new file mode 100644
index 0000000..d571cfd
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.api.accounts;
+
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.restapi.NotImplementedException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+
+public interface AccountApi {
+  AccountInfo get() throws RestApiException;
+
+  void starChange(String id) throws RestApiException;
+  void unstarChange(String id) throws RestApiException;
+
+  /**
+   * A default implementation which allows source compatibility
+   * when adding new methods to the interface.
+   **/
+  public class NotImplemented implements AccountApi {
+    @Override
+    public AccountInfo get() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void starChange(String id) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void unstarChange(String id) throws RestApiException {
+      throw new NotImplementedException();
+    }
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/Accounts.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/Accounts.java
new file mode 100644
index 0000000..749b12a
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/Accounts.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.api.accounts;
+
+import com.google.gerrit.extensions.restapi.NotImplementedException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+
+public interface Accounts {
+  AccountApi id(String id) throws RestApiException;
+  AccountApi self() throws RestApiException;
+
+  /**
+   * A default implementation which allows source compatibility
+   * when adding new methods to the interface.
+   **/
+  public class NotImplemented implements Accounts {
+    @Override
+    public AccountApi id(String id) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public AccountApi self() throws RestApiException {
+      throw new NotImplementedException();
+    }
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
index f0a9e4d..3382b76 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ListChangesOption;
+import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 
 import java.util.EnumSet;
@@ -45,4 +46,85 @@
   ChangeInfo get() throws RestApiException;
   /** {@code get} with {@link ListChangesOption} set to NONE. */
   ChangeInfo info() throws RestApiException;
+
+  /**
+   * A default implementation which allows source compatibility
+   * when adding new methods to the interface.
+   **/
+  public class NotImplemented implements ChangeApi {
+    @Override
+    public String id() {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public RevisionApi current() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public RevisionApi revision(int id) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public RevisionApi revision(String id) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void abandon() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void abandon(AbandonInput in) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void restore() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void restore(RestoreInput in) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public ChangeApi revert() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public ChangeApi revert(RevertInput in) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void addReviewer(AddReviewerInput in) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void addReviewer(String in) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public ChangeInfo get(EnumSet<ListChangesOption> options) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public ChangeInfo get() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public ChangeInfo info() throws RestApiException {
+      throw new NotImplementedException();
+    }
+  }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/Changes.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/Changes.java
index 48e9fd3..201a0bd 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/Changes.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/Changes.java
@@ -14,11 +14,113 @@
 
 package com.google.gerrit.extensions.api.changes;
 
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ListChangesOption;
+import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 
+import java.util.Arrays;
+import java.util.EnumSet;
+import java.util.List;
+
 public interface Changes {
   ChangeApi id(int id) throws RestApiException;
   ChangeApi id(String triplet) throws RestApiException;
   ChangeApi id(String project, String branch, String id)
       throws RestApiException;
+  ChangeApi create(ChangeInfo in) throws RestApiException;
+
+  QueryRequest query();
+  QueryRequest query(String query);
+
+  public abstract class QueryRequest {
+    private String query;
+    private int limit;
+    private int start;
+    private EnumSet<ListChangesOption> options = EnumSet.noneOf(ListChangesOption.class);
+
+    public abstract List<ChangeInfo> get() throws RestApiException;
+
+    public QueryRequest withQuery(String query) {
+      this.query = query;
+      return this;
+    }
+
+    public QueryRequest withLimit(int limit) {
+      this.limit = limit;
+      return this;
+    }
+
+    public QueryRequest withStart(int start) {
+      this.start = start;
+      return this;
+    }
+
+    public QueryRequest withOption(ListChangesOption options) {
+      this.options.add(options);
+      return this;
+    }
+
+    public QueryRequest withOptions(ListChangesOption... options) {
+      this.options.addAll(Arrays.asList(options));
+      return this;
+    }
+
+    public QueryRequest withOptions(EnumSet<ListChangesOption> options) {
+      this.options = options;
+      return this;
+    }
+
+    public String getQuery() {
+      return query;
+    }
+
+    public int getLimit() {
+      return limit;
+    }
+
+    public int getStart() {
+      return start;
+    }
+
+    public EnumSet<ListChangesOption> getOptions() {
+      return options;
+    }
+  }
+
+  /**
+   * A default implementation which allows source compatibility
+   * when adding new methods to the interface.
+   **/
+  public class NotImplemented implements Changes {
+    @Override
+    public ChangeApi id(int id) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public ChangeApi id(String triplet) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public ChangeApi id(String project, String branch, String id) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public ChangeApi create(ChangeInfo in) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public QueryRequest query() {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public QueryRequest query(String query) {
+      throw new NotImplementedException();
+    }
+  }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
index 993fa14..cf2d930 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.extensions.api.changes;
 
+import com.google.gerrit.extensions.common.Comment;
 import com.google.gerrit.extensions.restapi.DefaultInput;
 
 import java.util.LinkedHashMap;
@@ -26,7 +27,7 @@
   public String message;
 
   public Map<String, Short> labels;
-  public Map<String, List<Comment>> comments;
+  public Map<String, List<CommentInput>> comments;
 
   /**
    * If true require all labels to be within the user's permitted ranges based
@@ -67,24 +68,7 @@
     NONE, OWNER, OWNER_REVIEWERS, ALL
   }
 
-  public static enum Side {
-    PARENT, REVISION
-  }
-
-  public static class Comment {
-    public String id;
-    public Side side;
-    public int line;
-    public String inReplyTo;
-    public String message;
-    public Range range;
-
-    public static class Range {
-      public int startLine;
-      public int startCharacter;
-      public int endLine;
-      public int endCharacter;
-    }
+  public static class CommentInput extends Comment {
   }
 
   public ReviewInput message(String msg) {
@@ -97,7 +81,7 @@
       throw new IllegalArgumentException();
     }
     if (labels == null) {
-      labels = new LinkedHashMap<String, Short>(4);
+      labels = new LinkedHashMap<>(4);
     }
     labels.put(name, value);
     return this;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
index 4f90be2..20bcea9 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
@@ -14,8 +14,11 @@
 
 package com.google.gerrit.extensions.api.changes;
 
+import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 
+import java.util.Set;
+
 public interface RevisionApi {
   void delete() throws RestApiException;
   void review(ReviewInput in) throws RestApiException;
@@ -26,4 +29,64 @@
   void publish() throws RestApiException;
   ChangeApi cherryPick(CherryPickInput in) throws RestApiException;
   ChangeApi rebase() throws RestApiException;
+  boolean canRebase();
+
+  void setReviewed(String path, boolean reviewed) throws RestApiException;
+  Set<String> reviewed() throws RestApiException;
+
+  /**
+   * A default implementation which allows source compatibility
+   * when adding new methods to the interface.
+   **/
+  public class NotImplemented implements RevisionApi {
+    @Override
+    public void delete() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void review(ReviewInput in) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void submit() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void submit(SubmitInput in) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void publish() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public ChangeApi cherryPick(CherryPickInput in) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public ChangeApi rebase() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public boolean canRebase() {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void setReviewed(String path, boolean reviewed) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public Set<String> reviewed() throws RestApiException {
+      throw new NotImplementedException();
+    }
+  }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchApi.java
index 2f1533f..f88a2cb 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchApi.java
@@ -14,8 +14,20 @@
 
 package com.google.gerrit.extensions.api.projects;
 
+import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 
 public interface BranchApi {
   BranchApi create(BranchInput in) throws RestApiException;
+
+  /**
+   * A default implementation which allows source compatibility
+   * when adding new methods to the interface.
+   **/
+  public class NotImplemented implements BranchApi {
+    @Override
+    public BranchApi create(BranchInput in) throws RestApiException {
+      throw new NotImplementedException();
+    }
+  }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
index 1c7209d..d013c5d 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
@@ -14,6 +14,39 @@
 
 package com.google.gerrit.extensions.api.projects;
 
+import com.google.gerrit.extensions.common.ProjectInfo;
+import com.google.gerrit.extensions.restapi.NotImplementedException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+
 public interface ProjectApi {
+  ProjectApi create() throws RestApiException;
+  ProjectApi create(ProjectInput in) throws RestApiException;
+  ProjectInfo get();
   BranchApi branch(String ref);
+
+  /**
+   * A default implementation which allows source compatibility
+   * when adding new methods to the interface.
+   **/
+  public class NotImplemented implements ProjectApi {
+    @Override
+    public ProjectApi create() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public ProjectApi create(ProjectInput in) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public ProjectInfo get() {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public BranchApi branch(String ref) {
+      throw new NotImplementedException();
+    }
+  }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectInput.java
new file mode 100644
index 0000000..74b2be87
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectInput.java
@@ -0,0 +1,43 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.api.projects;
+
+import com.google.gerrit.extensions.common.InheritableBoolean;
+import com.google.gerrit.extensions.common.SubmitType;
+
+import java.util.List;
+import java.util.Map;
+
+public class ProjectInput {
+  public String name;
+  public String parent;
+  public String description;
+  public boolean permissionsOnly;
+  public boolean createEmptyCommit;
+  public SubmitType submitType;
+  public List<String> branches;
+  public List<String> owners;
+  public InheritableBoolean useContributorAgreements;
+  public InheritableBoolean useSignedOffBy;
+  public InheritableBoolean useContentMerge;
+  public InheritableBoolean requireChangeId;
+  public String maxObjectSizeLimit;
+  public Map<String, Map<String, ConfigValue>> pluginConfigValues;
+
+  public static class ConfigValue {
+    public String value;
+    public List<String> values;
+  }
+}
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_58.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
similarity index 65%
copy from gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_58.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
index 72b0c8b..407b7c7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_58.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2011 The Android Open Source Project
+// Copyright (C) 2014 The Android Open 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,14 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.schema;
+package com.google.gerrit.extensions.api.projects;
 
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_58 extends SchemaVersion {
-  @Inject
-  Schema_58(Provider<Schema_57> prior) {
-    super(prior);
-  }
-}
+public enum ProjectState {
+  ACTIVE,
+  READ_ONLY,
+  HIDDEN
+}
\ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/Projects.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/Projects.java
index a3a4137..9c0cfd8 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/Projects.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/Projects.java
@@ -14,8 +14,75 @@
 
 package com.google.gerrit.extensions.api.projects;
 
+import com.google.gerrit.extensions.common.ProjectInfo;
+import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 
+import java.util.List;
+
 public interface Projects {
   ProjectApi name(String name) throws RestApiException;
+
+  ListRequest list();
+
+  public abstract class ListRequest {
+    private boolean description;
+    private String prefix;
+    private int limit;
+    private int start;
+
+    public abstract List<ProjectInfo> get() throws RestApiException;
+
+    public ListRequest withDescription(boolean description) {
+      this.description = description;
+      return this;
+    }
+
+    public ListRequest withPrefix(String prefix) {
+      this.prefix = prefix;
+      return this;
+    }
+
+    public ListRequest withLimit(int limit) {
+      this.limit = limit;
+      return this;
+    }
+
+    public ListRequest withStart(int start) {
+      this.start = start;
+      return this;
+    }
+
+    public boolean getDescription() {
+      return description;
+    }
+
+    public String getPrefix() {
+      return prefix;
+    }
+
+    public int getLimit() {
+      return limit;
+    }
+
+    public int getStart() {
+      return start;
+    }
+  }
+
+  /**
+   * A default implementation which allows source compatibility
+   * when adding new methods to the interface.
+   **/
+  public class NotImplemented implements Projects {
+    @Override
+    public ProjectApi name(String name) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public ListRequest list() {
+      throw new NotImplementedException();
+    }
+  }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthServiceProvider.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthServiceProvider.java
new file mode 100644
index 0000000..9be2630
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthServiceProvider.java
@@ -0,0 +1,65 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.auth.oauth;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+import java.io.IOException;
+
+/* Contract that OAuth provider must implement */
+@ExtensionPoint
+public interface OAuthServiceProvider {
+
+  /**
+   * Returns the URL where you should redirect your users to authenticate
+   * your application.
+   *
+   * @return the OAuth service URL to redirect your users for authentication
+   */
+  String getAuthorizationUrl();
+
+  /**
+   * Retrieve the access token
+   *
+   * @param verifier verifier code
+   * @return access token
+   */
+  OAuthToken getAccessToken(OAuthVerifier verifier);
+
+  /**
+   * After establishing of secure communication channel, this method supossed to
+   * access the protected resoure and retrieve the username.
+   *
+   * @param token
+   * @return OAuth user information
+   * @throws IOException
+   */
+  OAuthUserInfo getUserInfo(OAuthToken token) throws IOException;
+
+  /**
+   * Returns the OAuth version of the service.
+   *
+   * @return oauth version as string
+   */
+  String getVersion();
+
+  /**
+   * Returns the name of this service. This name is resented the user to choose
+   * between multiple service providers
+   *
+   * @return name of the service
+   */
+  String getName();
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthToken.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthToken.java
new file mode 100644
index 0000000..901951e
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthToken.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.auth.oauth;
+
+/* OAuth token */
+public class OAuthToken {
+
+  private final String token;
+  private final String secret;
+  private final String raw;
+
+  public OAuthToken(String token, String secret, String raw) {
+    this.token = token;
+    this.secret = secret;
+    this.raw = raw;
+  }
+
+  public String getToken() {
+    return token;
+  }
+
+  public String getSecret() {
+    return secret;
+  }
+
+  public String getRaw() {
+    return raw;
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthUserInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthUserInfo.java
new file mode 100644
index 0000000..388ce36
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthUserInfo.java
@@ -0,0 +1,56 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.auth.oauth;
+
+public class OAuthUserInfo {
+
+  private final String externalId;
+  private final String userName;
+  private final String emailAddress;
+  private final String displayName;
+  private final String claimedIdentity;
+
+  public OAuthUserInfo(String externalId,
+      String userName,
+      String emailAddress,
+      String displayName,
+      String claimedIdentity) {
+    this.externalId = externalId;
+    this.userName = userName;
+    this.emailAddress = emailAddress;
+    this.displayName = displayName;
+    this.claimedIdentity = claimedIdentity;
+  }
+
+  public String getExternalId() {
+    return externalId;
+  }
+
+  public String getUserName() {
+    return userName;
+  }
+
+  public String getEmailAddress() {
+    return emailAddress;
+  }
+
+  public String getDisplayName() {
+    return displayName;
+  }
+
+  public String getClaimedIdentity() {
+    return claimedIdentity;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_58.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthVerifier.java
similarity index 64%
copy from gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_58.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthVerifier.java
index 72b0c8b..33c45c5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_58.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthVerifier.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2011 The Android Open Source Project
+// Copyright (C) 2015 The Android Open 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,14 +12,18 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.schema;
+package com.google.gerrit.extensions.auth.oauth;
 
-import com.google.inject.Inject;
-import com.google.inject.Provider;
+/* OAuth verifier */
+public class OAuthVerifier {
 
-public class Schema_58 extends SchemaVersion {
-  @Inject
-  Schema_58(Provider<Schema_57> prior) {
-    super(prior);
+  private final String value;
+
+  public OAuthVerifier(String value) {
+    this.value = value;
+  }
+
+  public String getValue() {
+    return value;
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
index f85684e..653ec37 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
@@ -39,4 +39,5 @@
   public Map<String, LabelInfo> labels;
   public Collection<ChangeMessageInfo> messages;
   public Map<String, RevisionInfo> revisions;
+  public int _number;
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/Comment.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/Comment.java
new file mode 100644
index 0000000..b7ad9a7
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/Comment.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+import java.sql.Timestamp;
+
+public abstract class Comment {
+  public String id;
+  public String path;
+  public Side side;
+  public int line;
+  public Range range;
+  public String inReplyTo;
+  public Timestamp updated;
+  public String message;
+
+  public static enum Side {
+    PARENT, REVISION
+  }
+
+  public static class Range {
+    public int startLine;
+    public int startCharacter;
+    public int endLine;
+    public int endCharacter;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_58.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/CommentInfo.java
similarity index 65%
copy from gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_58.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/CommentInfo.java
index 72b0c8b..cef1718 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_58.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/CommentInfo.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2011 The Android Open Source Project
+// Copyright (C) 2014 The Android Open 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,14 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.schema;
+package com.google.gerrit.extensions.common;
 
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_58 extends SchemaVersion {
-  @Inject
-  Schema_58(Provider<Schema_57> prior) {
-    super(prior);
-  }
+public class CommentInfo extends Comment {
+  public AccountInfo author;
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_58.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/InheritableBoolean.java
similarity index 65%
copy from gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_58.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/InheritableBoolean.java
index 72b0c8b..676c4d3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_58.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/InheritableBoolean.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2011 The Android Open Source Project
+// Copyright (C) 2014 The Android Open 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,14 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.schema;
+package com.google.gerrit.extensions.common;
 
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_58 extends SchemaVersion {
-  @Inject
-  Schema_58(Provider<Schema_57> prior) {
-    super(prior);
-  }
-}
+public enum InheritableBoolean {
+  TRUE,
+  FALSE,
+  INHERIT
+}
\ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/LabelInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/LabelInfo.java
index fd6008f..1e4edcd 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/LabelInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/LabelInfo.java
@@ -25,6 +25,7 @@
   public List<ApprovalInfo> all;
   public Map<String, String> values;
   public Short value;
+  public Short defaultValue;
   public Boolean optional;
   public Boolean blocking;
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ListChangesOption.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ListChangesOption.java
index c23e312..f9f8b62 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ListChangesOption.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ListChangesOption.java
@@ -49,7 +49,10 @@
   DRAFT_COMMENTS(12),
 
   /** Include download commands for the caller. */
-  DOWNLOAD_COMMANDS(13);
+  DOWNLOAD_COMMANDS(13),
+
+  /** Include patch set weblinks. */
+  WEB_LINKS(14);
 
   private final int value;
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ProjectInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ProjectInfo.java
new file mode 100644
index 0000000..bb07e44
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ProjectInfo.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+import com.google.gerrit.extensions.api.projects.ProjectState;
+
+import java.util.List;
+import java.util.Map;
+
+public class ProjectInfo {
+  public String id;
+  public String name;
+  public String parent;
+  public String description;
+  public ProjectState state;
+  public Map<String, String> branches;
+  public List<WebLinkInfo> webLinks;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RevisionInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RevisionInfo.java
index ea5b068..8f61aa2 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RevisionInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RevisionInfo.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.extensions.common;
 
+import java.util.List;
 import java.util.Map;
 
 public class RevisionInfo {
@@ -25,4 +26,5 @@
   public CommitInfo commit;
   public Map<String, FileInfo> files;
   public Map<String, ActionInfo> actions;
+  public List<WebLinkInfo> webLinks;
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_58.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SubmitType.java
similarity index 65%
copy from gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_58.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SubmitType.java
index 72b0c8b..95a9693 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_58.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SubmitType.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2011 The Android Open Source Project
+// Copyright (C) 2014 The Android Open 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,14 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.schema;
+package com.google.gerrit.extensions.common;
 
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_58 extends SchemaVersion {
-  @Inject
-  Schema_58(Provider<Schema_57> prior) {
-    super(prior);
-  }
-}
+public enum SubmitType {
+  FAST_FORWARD_ONLY,
+  MERGE_IF_NECESSARY,
+  REBASE_IF_NECESSARY,
+  MERGE_ALWAYS,
+  CHERRY_PICK
+}
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_58.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/WebLinkInfo.java
similarity index 66%
copy from gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_58.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/WebLinkInfo.java
index 72b0c8b..7695c8c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_58.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/WebLinkInfo.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2011 The Android Open Source Project
+// Copyright (C) 2014 The Android Open 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,14 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.schema;
+package com.google.gerrit.extensions.common;
 
-import com.google.inject.Inject;
-import com.google.inject.Provider;
+public class WebLinkInfo {
+  public String name;
+  public String url;
 
-public class Schema_58 extends SchemaVersion {
-  @Inject
-  Schema_58(Provider<Schema_57> prior) {
-    super(prior);
+  public WebLinkInfo(String name, String url) {
+    this.name = name;
+    this.url = url;
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/UsageDataPublishedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/UsageDataPublishedListener.java
new file mode 100644
index 0000000..365d056
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/UsageDataPublishedListener.java
@@ -0,0 +1,45 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.events;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+import java.sql.Timestamp;
+import java.util.List;
+
+/** Notified when usage data is published */
+@ExtensionPoint
+public interface UsageDataPublishedListener {
+
+  public interface Event {
+    MetaData getMetaData();
+    Timestamp getInstant();
+    List<Data> getData();
+  }
+
+  public interface Data {
+    long getValue();
+    String getProjectName();
+  }
+
+  public interface MetaData {
+    public String getName();
+    public String getUnitName();
+    public String getUnitSymbol();
+    public String getDescription();
+  }
+
+  void onUsageDataPublished(Event event);
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicItem.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicItem.java
index aa1dc76..7edfaed 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicItem.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicItem.java
@@ -78,7 +78,7 @@
     Key<DynamicItem<T>> key = (Key<DynamicItem<T>>) Key.get(
         Types.newParameterizedType(DynamicItem.class, member.getType()));
     binder.bind(key)
-      .toProvider(new DynamicItemProvider<T>(member, key))
+      .toProvider(new DynamicItemProvider<>(member, key))
       .in(Scopes.SINGLETON);
   }
 
@@ -111,10 +111,10 @@
   DynamicItem(Key<DynamicItem<T>> key, Provider<T> provider, String pluginName) {
     NamedProvider<T> in = null;
     if (provider != null) {
-      in = new NamedProvider<T>(provider, pluginName);
+      in = new NamedProvider<>(provider, pluginName);
     }
     this.key = key;
-    this.ref = new AtomicReference<NamedProvider<T>>(in);
+    this.ref = new AtomicReference<>(in);
   }
 
   /**
@@ -148,19 +148,22 @@
    * @return handle to remove the item at a later point in time.
    */
   public RegistrationHandle set(Provider<T> impl, String pluginName) {
-    final NamedProvider<T> item = new NamedProvider<T>(impl, pluginName);
-    while (!ref.compareAndSet(null, item)) {
-      NamedProvider<T> old = ref.get();
-      if (old != null) {
+    final NamedProvider<T> item = new NamedProvider<>(impl, pluginName);
+    NamedProvider<T> old = null;
+    while (!ref.compareAndSet(old, item)) {
+      old = ref.get();
+      if (old != null && !"gerrit".equals(old.pluginName)) {
         throw new ProvisionException(String.format(
             "%s already provided by %s, ignoring plugin %s",
             key.getTypeLiteral(), old.pluginName, pluginName));
       }
     }
+
+    final NamedProvider<T> defaultItem = old;
     return new RegistrationHandle() {
       @Override
       public void remove() {
-        ref.compareAndSet(item, null);
+        ref.compareAndSet(item, defaultItem);
       }
     };
   }
@@ -177,25 +180,34 @@
    */
   public ReloadableRegistrationHandle<T> set(Key<T> key, Provider<T> impl,
       String pluginName) {
-    final NamedProvider<T> item = new NamedProvider<T>(impl, pluginName);
-    while (!ref.compareAndSet(null, item)) {
-      NamedProvider<T> old = ref.get();
-      if (old != null) {
+    final NamedProvider<T> item = new NamedProvider<>(impl, pluginName);
+    NamedProvider<T> old = null;
+    while (!ref.compareAndSet(old, item)) {
+      old = ref.get();
+      if (old != null
+          && !"gerrit".equals(old.pluginName)
+          && !pluginName.equals(old.pluginName)) {
+        // We allow to replace:
+        // 1. Gerrit core items, e.g. websession cache
+        //    can be replaced by plugin implementation
+        // 2. Reload of current plugin
         throw new ProvisionException(String.format(
             "%s already provided by %s, ignoring plugin %s",
             this.key.getTypeLiteral(), old.pluginName, pluginName));
       }
     }
-    return new ReloadableHandle(key, item);
+    return new ReloadableHandle(key, item, old);
   }
 
   private class ReloadableHandle implements ReloadableRegistrationHandle<T> {
     private final Key<T> key;
     private final NamedProvider<T> item;
+    private final NamedProvider<T> defaultItem;
 
-    ReloadableHandle(Key<T> key, NamedProvider<T> item) {
+    ReloadableHandle(Key<T> key, NamedProvider<T> item, NamedProvider<T> defaultItem) {
       this.key = key;
       this.item = item;
+      this.defaultItem = defaultItem;
     }
 
     @Override
@@ -205,14 +217,14 @@
 
     @Override
     public void remove() {
-      ref.compareAndSet(item, null);
+      ref.compareAndSet(item, defaultItem);
     }
 
     @Override
     public ReloadableHandle replace(Key<T> newKey, Provider<T> newItem) {
-      NamedProvider<T> n = new NamedProvider<T>(newItem, item.pluginName);
+      NamedProvider<T> n = new NamedProvider<>(newItem, item.pluginName);
       if (ref.compareAndSet(item, n)) {
-        return new ReloadableHandle(newKey, n);
+        return new ReloadableHandle(newKey, n, defaultItem);
       }
       return null;
     }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicItemProvider.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicItemProvider.java
index 1074ee5..9b09d15 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicItemProvider.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicItemProvider.java
@@ -37,7 +37,7 @@
   }
 
   public DynamicItem<T> get() {
-    return new DynamicItem<T>(key, find(injector, type), "gerrit");
+    return new DynamicItem<>(key, find(injector, type), "gerrit");
   }
 
   private static <T> Provider<T> find(Injector src, TypeLiteral<T> type) {
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMap.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMap.java
index ea4a751..4251891 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMap.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMap.java
@@ -81,14 +81,14 @@
     Key<DynamicMap<T>> key = (Key<DynamicMap<T>>) Key.get(
         Types.newParameterizedType(DynamicMap.class, member.getType()));
     binder.bind(key)
-        .toProvider(new DynamicMapProvider<T>(member))
+        .toProvider(new DynamicMapProvider<>(member))
         .in(Scopes.SINGLETON);
   }
 
   final ConcurrentMap<NamePair, Provider<T>> items;
 
   DynamicMap() {
-    items = new ConcurrentHashMap<NamePair, Provider<T>>(
+    items = new ConcurrentHashMap<>(
         16 /* initial size */,
         0.75f /* load factor */,
         1 /* concurrency level of 1, load/unload is single threaded */);
@@ -115,7 +115,7 @@
    * @return sorted set of active plugins that supply at least one item.
    */
   public SortedSet<String> plugins() {
-    SortedSet<String> r = new TreeSet<String>();
+    SortedSet<String> r = new TreeSet<>();
     for (NamePair p : items.keySet()) {
       r.add(p.pluginName);
     }
@@ -129,7 +129,7 @@
    * @return items exported by a plugin, keyed by the export name.
    */
   public SortedMap<String, Provider<T>> byPlugin(String pluginName) {
-    SortedMap<String, Provider<T>> r = new TreeMap<String, Provider<T>>();
+    SortedMap<String, Provider<T>> r = new TreeMap<>();
     for (Map.Entry<NamePair, Provider<T>> e : items.entrySet()) {
       if (e.getKey().pluginName.equals(pluginName)) {
         r.put(e.getKey().exportName, e.getValue());
@@ -206,4 +206,8 @@
       return false;
     }
   }
+
+  public static <T> DynamicMap<T> emptyMap() {
+    return new DynamicMap<T>() {};
+  }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMapProvider.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMapProvider.java
index c6e4701..2554673 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMapProvider.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMapProvider.java
@@ -34,7 +34,7 @@
 
   public DynamicMap<T> get() {
     PrivateInternals_DynamicMapImpl<T> m =
-        new PrivateInternals_DynamicMapImpl<T>();
+        new PrivateInternals_DynamicMapImpl<>();
     List<Binding<T>> bindings = injector.findBindingsByType(type);
     if (bindings != null) {
       for (Binding<T> b : bindings) {
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java
index b2f19e5..628745a 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java
@@ -72,7 +72,7 @@
     Key<DynamicSet<T>> key = (Key<DynamicSet<T>>) Key.get(
         Types.newParameterizedType(DynamicSet.class, member.getType()));
     binder.bind(key)
-      .toProvider(new DynamicSetProvider<T>(member))
+      .toProvider(new DynamicSetProvider<>(member))
       .in(Scopes.SINGLETON);
   }
 
@@ -136,7 +136,7 @@
   private final CopyOnWriteArrayList<AtomicReference<Provider<T>>> items;
 
   DynamicSet(Collection<AtomicReference<Provider<T>>> base) {
-    items = new CopyOnWriteArrayList<AtomicReference<Provider<T>>>(base);
+    items = new CopyOnWriteArrayList<>(base);
   }
 
   @Override
@@ -194,8 +194,7 @@
    * @return handle to remove the item at a later point in time.
    */
   public RegistrationHandle add(final Provider<T> item) {
-    final AtomicReference<Provider<T>> ref =
-        new AtomicReference<Provider<T>>(item);
+    final AtomicReference<Provider<T>> ref = new AtomicReference<>(item);
     items.add(ref);
     return new RegistrationHandle() {
       @Override
@@ -218,7 +217,7 @@
    *         without it ever leaving the collection.
    */
   public ReloadableRegistrationHandle<T> add(Key<T> key, Provider<T> item) {
-    AtomicReference<Provider<T>> ref = new AtomicReference<Provider<T>>(item);
+    AtomicReference<Provider<T>> ref = new AtomicReference<>(item);
     items.add(ref);
     return new ReloadableHandle(ref, key, item);
   }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSetProvider.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSetProvider.java
index 21fa1b8..9ea96d4 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSetProvider.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSetProvider.java
@@ -36,7 +36,7 @@
   }
 
   public DynamicSet<T> get() {
-    return new DynamicSet<T>(find(injector, type));
+    return new DynamicSet<>(find(injector, type));
   }
 
   private static <T> List<AtomicReference<Provider<T>>> find(
@@ -47,16 +47,12 @@
     if (cnt == 0) {
       return Collections.emptyList();
     }
-    List<AtomicReference<Provider<T>>> r = newList(cnt);
+    List<AtomicReference<Provider<T>>> r = new ArrayList<>(cnt);
     for (Binding<T> b : bindings) {
       if (b.getKey().getAnnotation() != null) {
-        r.add(new AtomicReference<Provider<T>>(b.getProvider()));
+        r.add(new AtomicReference<>(b.getProvider()));
       }
     }
     return r;
   }
-
-  private static <T> List<AtomicReference<Provider<T>>> newList(int cnt) {
-    return new ArrayList<AtomicReference<Provider<T>>>(cnt);
-  }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicTypes.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicTypes.java
index 8bc57ab..96538e1 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicTypes.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicTypes.java
@@ -31,7 +31,7 @@
 /** <b>DO NOT USE</b> */
 public class PrivateInternals_DynamicTypes {
   public static Map<TypeLiteral<?>, DynamicItem<?>> dynamicItemsOf(Injector src) {
-    Map<TypeLiteral<?>, DynamicItem<?>> m = newHashMap();
+    Map<TypeLiteral<?>, DynamicItem<?>> m = new HashMap<>();
     for (Map.Entry<Key<?>, Binding<?>> e : src.getBindings().entrySet()) {
       TypeLiteral<?> type = e.getKey().getTypeLiteral();
       if (type.getRawType() == DynamicItem.class) {
@@ -47,7 +47,7 @@
   }
 
   public static Map<TypeLiteral<?>, DynamicSet<?>> dynamicSetsOf(Injector src) {
-    Map<TypeLiteral<?>, DynamicSet<?>> m = newHashMap();
+    Map<TypeLiteral<?>, DynamicSet<?>> m = new HashMap<>();
     for (Map.Entry<Key<?>, Binding<?>> e : src.getBindings().entrySet()) {
       TypeLiteral<?> type = e.getKey().getTypeLiteral();
       if (type.getRawType() == DynamicSet.class) {
@@ -63,7 +63,7 @@
   }
 
   public static Map<TypeLiteral<?>, DynamicMap<?>> dynamicMapsOf(Injector src) {
-    Map<TypeLiteral<?>, DynamicMap<?>> m = newHashMap();
+    Map<TypeLiteral<?>, DynamicMap<?>> m = new HashMap<>();
     for (Map.Entry<Key<?>, Binding<?>> e : src.getBindings().entrySet()) {
       TypeLiteral<?> type = e.getKey().getTypeLiteral();
       if (type.getRawType() == DynamicMap.class) {
@@ -85,7 +85,7 @@
       return Collections.emptyList();
     }
 
-    List<RegistrationHandle> handles = new ArrayList<RegistrationHandle>(4);
+    List<RegistrationHandle> handles = new ArrayList<>(4);
     try {
       for (Map.Entry<TypeLiteral<?>, DynamicItem<?>> e : items.entrySet()) {
         @SuppressWarnings("unchecked")
@@ -95,9 +95,7 @@
         DynamicItem<Object> item = (DynamicItem<Object>) e.getValue();
 
         for (Binding<Object> b : bindings(src, type)) {
-          if (b.getKey().getAnnotation() != null) {
-            handles.add(item.set(b.getKey(), b.getProvider(), pluginName));
-          }
+          handles.add(item.set(b.getKey(), b.getProvider(), pluginName));
         }
       }
     } catch (RuntimeException e) {
@@ -117,7 +115,7 @@
       return Collections.emptyList();
     }
 
-    List<RegistrationHandle> handles = new ArrayList<RegistrationHandle>(4);
+    List<RegistrationHandle> handles = new ArrayList<>(4);
     try {
       for (Map.Entry<TypeLiteral<?>, DynamicSet<?>> e : sets.entrySet()) {
         @SuppressWarnings("unchecked")
@@ -150,7 +148,7 @@
       return Collections.emptyList();
     }
 
-    List<RegistrationHandle> handles = new ArrayList<RegistrationHandle>(4);
+    List<RegistrationHandle> handles = new ArrayList<>(4);
     try {
       for (Map.Entry<TypeLiteral<?>, DynamicMap<?>> e : maps.entrySet()) {
         @SuppressWarnings("unchecked")
@@ -185,7 +183,7 @@
 
       @Override
       public void start() {
-        handles = new ArrayList<RegistrationHandle>(4);
+        handles = new ArrayList<>(4);
         Injector parent = self.getParent();
         while (parent != null) {
           handles.addAll(attachSets(self, dynamicSetsOf(parent)));
@@ -213,10 +211,6 @@
     }
   }
 
-  private static <K,V> Map<K, V> newHashMap() {
-    return new HashMap<K,V>();
-  }
-
   private static <T> List<Binding<T>> bindings(Injector src, TypeLiteral<T> type) {
     return src.findBindingsByType(type);
   }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/Die.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/NotImplementedException.java
similarity index 65%
copy from gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/Die.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/NotImplementedException.java
index 96e2ec8..10d0a14 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/Die.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/NotImplementedException.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2009 The Android Open Source Project
+// Copyright (C) 2014 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,16 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.pgm.util;
+package com.google.gerrit.extensions.restapi;
 
-public class Die extends RuntimeException {
+/** Method is not implemented in currently used implementation. */
+public class NotImplementedException extends UnsupportedOperationException {
   private static final long serialVersionUID = 1L;
 
-  public Die(final String why) {
-    super(why);
-  }
-
-  public Die(final String why, final Throwable cause) {
-    super(why, cause);
+  public NotImplementedException() {
+    super("Not implemented.");
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Response.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Response.java
index 848004d..314c898 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Response.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Response.java
@@ -21,12 +21,12 @@
 
   /** HTTP 200 OK: pointless wrapper for type safety. */
   public static <T> Response<T> ok(T value) {
-    return new Impl<T>(200, value);
+    return new Impl<>(200, value);
   }
 
   /** HTTP 201 Created: typically used when a new resource is made. */
   public static <T> Response<T> created(T value) {
-    return new Impl<T>(201, value);
+    return new Impl<>(201, value);
   }
 
   /** HTTP 204 No Content: typically used when the resource is deleted. */
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestApiModule.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestApiModule.java
index b3a0e18..7708a5c 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestApiModule.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestApiModule.java
@@ -31,47 +31,47 @@
 
   protected <R extends RestResource>
   ReadViewBinder<R> get(TypeLiteral<RestView<R>> viewType) {
-    return new ReadViewBinder<R>(view(viewType, GET, "/"));
+    return new ReadViewBinder<>(view(viewType, GET, "/"));
   }
 
   protected <R extends RestResource>
   ModifyViewBinder<R> put(TypeLiteral<RestView<R>> viewType) {
-    return new ModifyViewBinder<R>(view(viewType, PUT, "/"));
+    return new ModifyViewBinder<>(view(viewType, PUT, "/"));
   }
 
   protected <R extends RestResource>
   ModifyViewBinder<R> post(TypeLiteral<RestView<R>> viewType) {
-    return new ModifyViewBinder<R>(view(viewType, POST, "/"));
+    return new ModifyViewBinder<>(view(viewType, POST, "/"));
   }
 
   protected <R extends RestResource>
   ModifyViewBinder<R> delete(TypeLiteral<RestView<R>> viewType) {
-    return new ModifyViewBinder<R>(view(viewType, DELETE, "/"));
+    return new ModifyViewBinder<>(view(viewType, DELETE, "/"));
   }
 
   protected <R extends RestResource>
   ReadViewBinder<R> get(TypeLiteral<RestView<R>> viewType, String name) {
-    return new ReadViewBinder<R>(view(viewType, GET, name));
+    return new ReadViewBinder<>(view(viewType, GET, name));
   }
 
   protected <R extends RestResource>
   ModifyViewBinder<R> put(TypeLiteral<RestView<R>> viewType, String name) {
-    return new ModifyViewBinder<R>(view(viewType, PUT, name));
+    return new ModifyViewBinder<>(view(viewType, PUT, name));
   }
 
   protected <R extends RestResource>
   ModifyViewBinder<R> post(TypeLiteral<RestView<R>> viewType, String name) {
-    return new ModifyViewBinder<R>(view(viewType, POST, name));
+    return new ModifyViewBinder<>(view(viewType, POST, name));
   }
 
   protected <R extends RestResource>
   ModifyViewBinder<R> delete(TypeLiteral<RestView<R>> viewType, String name) {
-    return new ModifyViewBinder<R>(view(viewType, DELETE, name));
+    return new ModifyViewBinder<>(view(viewType, DELETE, name));
   }
 
   protected <P extends RestResource>
   ChildCollectionBinder<P> child(TypeLiteral<RestView<P>> type, String name) {
-    return new ChildCollectionBinder<P>(view(type, GET, name));
+    return new ChildCollectionBinder<>(view(type, GET, name));
   }
 
   protected <R extends RestResource>
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/PatchSetWebLink.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/PatchSetWebLink.java
new file mode 100644
index 0000000..b6086f2
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/PatchSetWebLink.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.gerrit.extensions.webui;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+@ExtensionPoint
+public interface PatchSetWebLink extends WebLink {
+
+  /**
+   * URL to patch set in external service.
+   *
+   * @param projectName Name of the project
+   * @param commit Commit of the patch set
+   * @return url to patch set in external service.
+   */
+  String getPatchSetUrl(final String projectName, final String commit);
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/ProjectWebLink.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/ProjectWebLink.java
new file mode 100644
index 0000000..61e9982
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/ProjectWebLink.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.webui;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+@ExtensionPoint
+public interface ProjectWebLink extends WebLink {
+
+  /**
+   * URL to project in external service.
+   *
+   * @param projectName Name of the project
+   * @return url to project in external service.
+   */
+  String getProjectUrl(String projectName);
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/UiResult.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/UiResult.java
new file mode 100644
index 0000000..106db04
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/UiResult.java
@@ -0,0 +1,59 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.webui;
+
+import java.net.URI;
+
+/** Default result for {@link UiAction}s with no JavaScript. */
+public class UiResult {
+  /** Display an alert message to the user. */
+  public static UiResult alert(String message) {
+    UiResult r = new UiResult();
+    r.alert = message;
+    return r;
+  }
+
+  /** Launch URL in a new window. */
+  public static UiResult openUrl(URI uri) {
+    return openUrl(uri.toString());
+  }
+
+  /** Launch URL in a new window. */
+  public static UiResult openUrl(String url) {
+    UiResult r = new UiResult();
+    r.url = url;
+    r.openWindow = true;
+    return r;
+  }
+
+  /** Redirect the browser to a new URL. */
+  public static UiResult redirectUrl(String url) {
+    UiResult r = new UiResult();
+    r.url = url;
+    return r;
+  }
+
+  /** Alert the user with a message. */
+  protected String alert;
+
+  /** If present redirect browser to this URL. */
+  protected String url;
+
+  /** When true open {@link #url} in a new tab/window. */
+  protected Boolean openWindow;
+
+  private UiResult() {
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_58.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/WebLink.java
similarity index 65%
copy from gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_58.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/WebLink.java
index 72b0c8b..19d9ab7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_58.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/WebLink.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2011 The Android Open Source Project
+// Copyright (C) 2014 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -11,15 +11,14 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
+package com.google.gerrit.extensions.webui;
 
-package com.google.gerrit.server.schema;
+public interface WebLink {
 
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_58 extends SchemaVersion {
-  @Inject
-  Schema_58(Provider<Schema_57> prior) {
-    super(prior);
-  }
+  /**
+   * The link-name displayed in UI.
+   *
+   * @return name of link.
+   */
+  String getLinkName();
 }
diff --git a/gerrit-gwtexpui/BUCK b/gerrit-gwtexpui/BUCK
index 76d0741..e06bf17 100644
--- a/gerrit-gwtexpui/BUCK
+++ b/gerrit-gwtexpui/BUCK
@@ -3,7 +3,7 @@
 gwt_module(
   name = 'Clippy',
   srcs = glob([SRC + 'clippy/client/*.java']),
-  gwtxml = SRC + 'clippy/Clippy.gwt.xml',
+  gwt_xml = SRC + 'clippy/Clippy.gwt.xml',
   resources = [
     SRC + 'clippy/client/clippy.css',
     SRC + 'clippy/client/clippy.swf',
@@ -11,8 +11,6 @@
   deps = [
     ':SafeHtml',
     ':UserAgent',
-  ],
-  compile_deps = [
     '//lib/gwt:user',
     '//lib:LICENSE-clippy',
   ],
@@ -30,7 +28,7 @@
 gwt_module(
   name = 'GlobalKey',
   srcs = glob([SRC + 'globalkey/client/*.java']),
-  gwtxml = SRC + 'globalkey/GlobalKey.gwt.xml',
+  gwt_xml = SRC + 'globalkey/GlobalKey.gwt.xml',
   resources = [
     SRC + 'globalkey/client/KeyConstants.properties',
     SRC + 'globalkey/client/key.css',
@@ -38,33 +36,33 @@
   deps = [
     ':SafeHtml',
     ':UserAgent',
+    '//lib/gwt:user',
   ],
-  compile_deps = ['//lib/gwt:user'],
   visibility = ['PUBLIC'],
 )
 
-java_library2(
+java_library(
   name = 'linker_server',
   srcs = glob([SRC + 'linker/server/*.java']),
-  compile_deps = ['//lib:servlet-api-3_1'],
+  provided_deps = ['//lib:servlet-api-3_1'],
   visibility = ['PUBLIC'],
 )
 
 gwt_module(
   name = 'Progress',
   srcs = glob([SRC + 'progress/client/*.java']),
-  gwtxml = SRC + 'progress/Progress.gwt.xml',
+  gwt_xml = SRC + 'progress/Progress.gwt.xml',
   resources = [SRC + 'progress/client/progress.css'],
-  compile_deps = ['//lib/gwt:user'],
+  deps = ['//lib/gwt:user'],
   visibility = ['PUBLIC'],
 )
 
 gwt_module(
   name = 'SafeHtml',
   srcs = glob([SRC + 'safehtml/client/*.java']),
-  gwtxml = SRC + 'safehtml/SafeHtml.gwt.xml',
+  gwt_xml = SRC + 'safehtml/SafeHtml.gwt.xml',
   resources = [SRC + 'safehtml/client/safehtml.css'],
-  compile_deps = ['//lib/gwt:user'],
+  deps = ['//lib/gwt:user'],
   visibility = ['PUBLIC'],
 )
 
@@ -74,25 +72,25 @@
     'src/test/java/com/google/gwtexpui/safehtml/client/**/*.java',
   ]),
   deps = [
-    ':SafeHtml_lib',
+    ':SafeHtml',
     '//lib:junit',
     '//lib/gwt:user',
     '//lib/gwt:dev',
   ],
-  source_under_test = [':SafeHtml_lib'],
+  source_under_test = [':SafeHtml'],
 )
 
 gwt_module(
   name = 'UserAgent',
   srcs = glob([SRC + 'user/client/*.java']),
-  gwtxml = SRC + 'user/User.gwt.xml',
-  compile_deps = ['//lib/gwt:user'],
+  gwt_xml = SRC + 'user/User.gwt.xml',
+  deps = ['//lib/gwt:user'],
   visibility = ['PUBLIC'],
 )
 
-java_library2(
+java_library(
   name = 'server',
   srcs = glob([SRC + 'server/*.java']),
-  compile_deps = ['//lib:servlet-api-3_1'],
+  provided_deps = ['//lib:servlet-api-3_1'],
   visibility = ['PUBLIC'],
 )
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
index 0f6992d..af80b3c 100644
--- 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
@@ -44,8 +44,7 @@
     final ArtifactSet returnTo = new ArtifactSet();
     int index = 0;
 
-    final HashMap<String, PublicResource> css =
-        new HashMap<String, PublicResource>();
+    final HashMap<String, PublicResource> css = new HashMap<>();
 
     for (final StandardStylesheetReference ssr : artifacts
         .<StandardStylesheetReference> find(StandardStylesheetReference.class)) {
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
index 15caa34..a0fee2b 100644
--- 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
@@ -74,6 +74,12 @@
       case KeyCodes.KEY_ESCAPE:
         namedKey(b, KeyConstants.I.keyEsc());
         break;
+      case KeyCodes.KEY_LEFT:
+        namedKey(b, KeyConstants.I.keyLeft());
+        break;
+      case KeyCodes.KEY_RIGHT:
+        namedKey(b, KeyConstants.I.keyRight());
+        break;
       default:
         b.openSpan();
         b.setStyleName(KeyResources.I.css().helpKey());
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
index 6600a18..e2fec27 100644
--- 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
@@ -35,7 +35,7 @@
   }
 
   public KeyCommandSet(final String setName) {
-    map = new HashMap<Integer, KeyCommand>();
+    map = new HashMap<>();
     name = setName;
   }
 
@@ -79,7 +79,7 @@
 
   public void add(final KeyCommandSet set) {
     if (sets == null) {
-      sets = new ArrayList<KeyCommandSet>();
+      sets = new ArrayList<>();
     }
     assert !sets.contains(set);
     sets.add(set);
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
index b4cb41e..d26ca8c 100644
--- 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
@@ -35,4 +35,6 @@
   String keyShift();
   String keyEnter();
   String keyEsc();
+  String keyLeft();
+  String keyRight();
 }
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
index 2e12b07..76a0318 100644
--- 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
@@ -13,3 +13,5 @@
 keyShift = Shift
 keyEnter = Enter
 keyEsc = Esc
+keyLeft = Left
+keyRight = Right
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
index 67a5ef4..b015274 100644
--- 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
@@ -75,7 +75,7 @@
 
     final FlowPanel body = new FlowPanel();
     body.add(header);
-    DOM.appendChild(body.getElement(), DOM.createElement("hr"));
+    body.getElement().appendChild(DOM.createElement("hr"));
     body.add(lists);
 
     focus = new FocusPanel(body);
@@ -133,8 +133,7 @@
    *         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>();
+    LinkedHashMap<String, KeyCommandSet> byName = new LinkedHashMap<>();
     for (final KeyCommandSet set : GlobalKey.active.all.getSets()) {
       KeyCommandSet v = byName.get(set.getName());
       if (v == null) {
@@ -171,7 +170,7 @@
       lists.resizeRows(row + keys.size());
     }
 
-    Map<KeyCommand, Integer> rows = new HashMap<KeyCommand, Integer>();
+    Map<KeyCommand, Integer> rows = new HashMap<>();
     FORMAT_KEYS: for (int i = 0; i < keys.size(); i++) {
       final KeyCommand k = keys.get(i);
       if (rows.containsKey(k)) {
@@ -234,7 +233,7 @@
   }
 
   private List<KeyCommand> sort(final KeyCommandSet set) {
-    final List<KeyCommand> keys = new ArrayList<KeyCommand>(set.getKeys());
+    final List<KeyCommand> keys = new ArrayList<>(set.getKeys());
     Collections.sort(keys, new Comparator<KeyCommand>() {
       @Override
       public int compare(KeyCommand arg0, KeyCommand arg1) {
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
index 46d7f51..b08b29f 100644
--- 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
@@ -23,7 +23,7 @@
   private static final HashMap<String, Tag> TAGS;
   static {
     final Tag src = new SrcTag();
-    TAGS = new HashMap<String, Tag>();
+    TAGS = new HashMap<>();
     TAGS.put("a", new AnchorTag());
     TAGS.put("form", new FormTag());
     TAGS.put("img", src);
@@ -31,8 +31,8 @@
     TAGS.put("frame", src);
   }
 
-  private final ArrayList<String> names = new ArrayList<String>();
-  private final ArrayList<String> values = new ArrayList<String>();
+  private final ArrayList<String> names = new ArrayList<>();
+  private final ArrayList<String> values = new ArrayList<>();
 
   private Tag tag = ANY;
   private int live;
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
index ed4e6cb..216add1 100644
--- 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
@@ -45,7 +45,7 @@
           final Response response) {
         final String qpat = getQueryPattern(request.getQuery());
         final boolean html = isHTML();
-        final ArrayList<Suggestion> r = new ArrayList<Suggestion>();
+        final ArrayList<Suggestion> r = new ArrayList<>();
         for (final Suggestion s : response.getSuggestions()) {
           r.add(new BoldSuggestion(qpat, s, html));
         }
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
index 143ecef..a71120a 100644
--- 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
@@ -122,8 +122,9 @@
   /** Convert bare http:// and https:// URLs into &lt;a href&gt; tags. */
   public SafeHtml linkify() {
     final String part = "(?:" +
-    "[a-zA-Z0-9$_.+!*',%;:@=?#/~-]" +
+    "[a-zA-Z0-9$_+!*'%;:@=?#/~-]" +
     "|&(?!lt;|gt;)" +
+    "|[.,](?!(?:\\s|$))" +
     ")";
     return replaceAll(
         "(https?://" +
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/DialogVisibleEvent.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/DialogVisibleEvent.java
index 80a940a..74218b4 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/DialogVisibleEvent.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/DialogVisibleEvent.java
@@ -22,7 +22,7 @@
 
   public static Type<DialogVisibleHandler> getType() {
     if (TYPE == null) {
-      TYPE = new Type<DialogVisibleHandler>();
+      TYPE = new Type<>();
     }
     return TYPE;
   }
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
index 749df17..75c3745 100644
--- 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
@@ -60,6 +60,38 @@
     assertEquals("A &lt;<a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a>&gt; B", n.asString());
   }
 
+  @Test
+  public void testLinkify_TrailingPlainLetter() {
+    final SafeHtml o = html("A http://go.here/foo B");
+    final SafeHtml n = o.linkify();
+    assertNotSame(o, n);
+    assertEquals("A <a href=\"http://go.here/foo\" target=\"_blank\">http://go.here/foo</a> B", n.asString());
+  }
+
+  @Test
+  public void testLinkify_TrailingDot() {
+    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());
+  }
+
+  @Test
+  public void testLinkify_TrailingComma() {
+    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());
+  }
+
+  @Test
+  public void testLinkify_TrailingDotDot() {
+    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());
+  }
+
   private static SafeHtml html(String text) {
     return new SafeHtmlBuilder().append(text).toSafeHtml();
   }
diff --git a/gerrit-gwtui-common/BUCK b/gerrit-gwtui-common/BUCK
index 76f2db8..f4c2358 100644
--- a/gerrit-gwtui-common/BUCK
+++ b/gerrit-gwtui-common/BUCK
@@ -3,8 +3,8 @@
 gwt_module(
   name = 'client',
   srcs = glob([SRC + 'client/**/*.java']),
-  gwtxml = SRC + 'GerritGwtUICommon.gwt.xml',
-  compile_deps = ['//lib/gwt:user'],
+  gwt_xml = SRC + 'GerritGwtUICommon.gwt.xml',
+  deps = ['//lib/gwt:user'],
   visibility = ['PUBLIC'],
 )
 
@@ -14,11 +14,11 @@
   visibility = ['PUBLIC'],
 )
 
-java_library2(
+java_library(
   name = 'client-lib2',
   srcs = glob(['src/main/**/*.java']),
   resources = glob(['src/main/**/*']),
-  compile_deps = ['//lib/gwt:user'],
+  provided_deps = ['//lib/gwt:user'],
   visibility = ['PUBLIC'],
 )
 
diff --git a/gerrit-gwtui/BUCK b/gerrit-gwtui/BUCK
index e978323..89b7ef7 100644
--- a/gerrit-gwtui/BUCK
+++ b/gerrit-gwtui/BUCK
@@ -1,20 +1,22 @@
 include_defs('//gerrit-gwtui/gwt.defs')
 include_defs('//tools/gwt-constants.defs')
+from multiprocessing import cpu_count
+
+DEPS = [
+  '//gerrit-gwtexpui:CSS',
+  '//lib:gwtjsonrpc',
+]
 
 genrule(
   name = 'ui_optdbg',
   cmd = 'cd $TMP;' +
-    'unzip -q $SRCDIR/ui_dbg.zip;' +
+    'unzip -q $(location :ui_dbg);' +
     'mv' +
     ' gerrit_ui/gerrit_ui.nocache.js' +
-    ' gerrit_ui/gerrit_dbg.nocache.js;' +
-    'unzip -qo $SRCDIR/ui_opt.zip;' +
+    ' gerrit_ui/dbg_gerrit_ui.nocache.js;' +
+    'unzip -qo $(location :ui_opt);' +
     'mkdir -p $(dirname $OUT);' +
     'zip -qr $OUT .',
-  srcs = [
-    genfile('ui_dbg.zip'),
-    genfile('ui_opt.zip'),
-  ],
   deps = [
     ':ui_dbg',
     ':ui_opt',
@@ -23,28 +25,37 @@
   visibility = ['PUBLIC'],
 )
 
-gwt_application(
+gwt_binary(
   name = 'ui_opt',
-  module_target = MODULE,
-  compiler_opts = GWT_COMPILER_OPTS,
-  deps = APP_DEPS + [':ui_dbg'],
+  modules = [MODULE],
+  module_deps = [':ui_module'],
+  deps = DEPS + [':ui_dbg'],
+  local_workers = cpu_count(),
+  strict = True,
+  experimental_args = GWT_COMPILER_ARGS,
+  vm_args = GWT_JVM_ARGS,
 )
 
-gwt_application(
+gwt_binary(
   name = 'ui_dbg',
-  module_target = MODULE,
-  compiler_opts = DEBUG_OPTS + ['-strict'],
-  deps = APP_DEPS,
+  modules = [MODULE],
+  style = 'PRETTY',
+  optimize = 0,
+  module_deps = [':ui_module'],
+  deps = DEPS,
+  local_workers = cpu_count(),
+  strict = True,
+  experimental_args = GWT_COMPILER_ARGS,
+  vm_args = GWT_JVM_ARGS,
   visibility = ['//:eclipse'],
 )
 
 gwt_user_agent_permutations(
   name = 'ui',
   module_name = 'gerrit_ui',
-  module_target = MODULE,
-  compiler_opts = DEBUG_OPTS + ['-draftCompile'],
-  browsers = BROWSERS,
-  deps = APP_DEPS,
+  modules = [MODULE],
+  module_deps = [':ui_module'],
+  deps = DEPS,
   visibility = ['//:'],
 )
 
@@ -53,9 +64,11 @@
 gwt_module(
   name = 'ui_module',
   srcs = glob(['src/main/java/**/*.java']),
-  gwtxml = 'src/main/java/%s.gwt.xml' % MODULE.replace('.', '/'),
+  gwt_xml = 'src/main/java/%s.gwt.xml' % MODULE.replace('.', '/'),
   resources = glob(['src/main/java/**/*'], excludes = DIFFY),
   deps = [
+    ':diffy_logo',
+    ':freebie_application_icon_set',
     '//gerrit-gwtexpui:Clippy',
     '//gerrit-gwtexpui:GlobalKey',
     '//gerrit-gwtexpui:Progress',
@@ -67,9 +80,6 @@
     '//gerrit-patch-jgit:client',
     '//gerrit-prettify:client',
     '//gerrit-reviewdb:client',
-  ],
-  compile_deps = [
-    ':diffy_logo',
     '//gerrit-gwtexpui:CSS',
     '//lib:gwtjsonrpc',
     '//lib:gwtjsonrpc_src',
@@ -86,11 +96,18 @@
 
 prebuilt_jar(
   name = 'diffy_logo',
-  binary_jar = genfile('diffy_images.jar'),
+  binary_jar = ':diffy_image_files_ln',
   deps = [
     '//lib:LICENSE-diffy',
     '//lib:LICENSE-CC-BY3.0',
-    ':diffy_image_files_ln',
+  ],
+)
+
+java_library(
+  name = 'freebie_application_icon_set',
+  deps = [
+    '//lib:LICENSE-freebie_application_icon_set',
+    '//lib:LICENSE-CC-BY3.0',
   ],
 )
 
@@ -113,16 +130,16 @@
     'src/main/java/com/google/gerrit/GerritGwtUI.gwt.xml',
   ],
   deps = [
-    ':ui_module_lib',
-    '//gerrit-common:client_lib',
-    '//gerrit-extension-api:client_lib',
+    ':ui_module',
+    '//gerrit-common:client',
+    '//gerrit-extension-api:client',
     '//lib:junit',
     '//lib/gwt:dev',
     '//lib/gwt:user',
     '//lib/gwt:gwt-test-utils',
     '//lib/jgit:jgit',
   ],
-  source_under_test = [':ui_module_lib'],
+  source_under_test = [':ui_module'],
   vm_args = ['-Xmx512m'],
   visibility = ['//tools/eclipse:classpath'],
 )
diff --git a/gerrit-gwtui/gwt.defs b/gerrit-gwtui/gwt.defs
index e407854..0e10116 100644
--- a/gerrit-gwtui/gwt.defs
+++ b/gerrit-gwtui/gwt.defs
@@ -26,36 +26,35 @@
 }
 MODULE = 'com.google.gerrit.GerritGwtUI'
 
-DEBUG_OPTS = [
-  '-style', 'PRETTY',
-  '-optimize', '0',
-]
-
-APP_DEPS = [':ui_module']
-
 def gwt_user_agent_permutations(
     name,
     module_name,
-    module_target,
-    compiler_opts = [],
+    modules,
+    style = 'PRETTY',
+    optimize = 0,
+    draft_compile = True,
+    module_deps = [],
     deps = [],
-    browsers = [],
+    browsers = BROWSERS,
     visibility = []):
+  from multiprocessing import cpu_count
   for ua in browsers:
     impl = ua
     if ua in ALIASES:
       impl = ALIASES[ua]
     xml = ''.join([
       "<module rename-to='%s'>" % module_name,
-      "<inherits name='%s'/>" % module_target,
+      "<inherits name='%s'/>" % modules[0],
       "<set-property name='user.agent' value='%s'/>" % impl,
       "<set-property name='locale' value='default'/>",
       "</module>",
     ])
-    gwt = '%s_%s.gwt.xml' % (module_target.replace('.', '/'), ua)
-    jar = '%s_%s.gwtxml.jar' % (name, ua)
+    gwt = '%s_%s.gwt.xml' % (modules[0].replace('.', '/'), ua)
+    gwt_name = '%s_%s' % (name, ua)
+    jar = '%s.gwtxml.jar' % (gwt_name)
+
     genrule(
-      name = '%s_%s_gwtxml_gen' % (name, ua),
+      name = '%s_gwtxml_gen' % gwt_name,
       cmd = 'cd $TMP;' +
         ('mkdir -p $(dirname %s);' % gwt) +
         ('echo "%s">%s;' % (xml, gwt)) +
@@ -63,15 +62,21 @@
       out = jar,
     )
     prebuilt_jar(
-      name = '%s_%s_gwtxml_lib' % (name, ua),
-      binary_jar = genfile(jar),
-      deps = [':%s_%s_gwtxml_gen' % (name, ua)],
+      name = '%s_gwtxml_lib' % gwt_name,
+      binary_jar = ':%s_gwtxml_gen' % gwt_name,
+      gwt_jar = ':%s_gwtxml_gen' % gwt_name,
     )
-    gwt_application(
-      name = '%s_%s' % (name, ua),
-      module_target = module_target + '_' + ua,
-      compiler_opts = compiler_opts,
-      deps = deps + [':%s_%s_gwtxml_lib' % (name, ua)],
+    gwt_binary(
+      name = gwt_name,
+      modules = [modules[0] + '_' + ua],
+      style = style,
+      optimize = optimize,
+      draft_compile = draft_compile,
+      module_deps = module_deps + [':%s_gwtxml_lib' % gwt_name],
+      deps = deps,
+      local_workers = cpu_count(),
+      strict = True,
+      experimental_args = GWT_COMPILER_ARGS,
+      vm_args = GWT_JVM_ARGS,
       visibility = visibility,
     )
-
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
index 666c3e2..2ad9a5e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
@@ -21,6 +21,7 @@
 import static com.google.gerrit.common.PageLinks.ADMIN_PROJECTS;
 import static com.google.gerrit.common.PageLinks.DASHBOARDS;
 import static com.google.gerrit.common.PageLinks.MINE;
+import static com.google.gerrit.common.PageLinks.MY_GROUPS;
 import static com.google.gerrit.common.PageLinks.PROJECTS;
 import static com.google.gerrit.common.PageLinks.QUERY;
 import static com.google.gerrit.common.PageLinks.REGISTER;
@@ -55,6 +56,7 @@
 import com.google.gerrit.client.admin.CreateGroupScreen;
 import com.google.gerrit.client.admin.CreateProjectScreen;
 import com.google.gerrit.client.admin.GroupListScreen;
+import com.google.gerrit.client.admin.MyGroupsListScreen;
 import com.google.gerrit.client.admin.PluginListScreen;
 import com.google.gerrit.client.admin.ProjectAccessScreen;
 import com.google.gerrit.client.admin.ProjectBranchesScreen;
@@ -233,7 +235,12 @@
       extension(token);
 
     } else if (matchExact(MINE, token)) {
-      Gerrit.display(token, mine(token));
+      String defaultScreenToken = Gerrit.getDefaultScreenToken();
+      if (defaultScreenToken != null && !MINE.equals(defaultScreenToken)) {
+        select(defaultScreenToken);
+      } else {
+        Gerrit.display(token, mine(token));
+      }
 
     } else if (matchPrefix("/dashboard/", token)) {
       dashboard(token);
@@ -253,6 +260,9 @@
     } else if (matchPrefix("/admin/", token)) {
       admin(token);
 
+    } else if (matchExact(MY_GROUPS, token)) {
+      Gerrit.display(token, new MyGroupsListScreen());
+
     } else if (/* DEPRECATED URL */matchPrefix("/c2/", token)) {
       changeScreen2 = true;
       change(token);
@@ -440,6 +450,17 @@
       return;
     }
 
+    if (rest.equals("self")) {
+      if (Gerrit.isSignedIn()) {
+        Gerrit.display(token, new AccountDashboardScreen(Gerrit.getUserAccount().getId()));
+      } else {
+        Screen s = new AccountDashboardScreen(null);
+        s.setRequiresSignIn(true);
+        Gerrit.display(token, s);
+      }
+      return;
+    }
+
     if (rest.startsWith("?")) {
       Gerrit.display(token, new CustomDashboardScreen(rest.substring(1)));
       return;
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 4b6b092..5b51b48 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
@@ -180,21 +180,23 @@
 
   public static AccountInfo asInfo(Account acct) {
     if (acct == null) {
-      return AccountInfo.create(0, null, null);
+      return AccountInfo.create(0, null, null, null);
     }
     return AccountInfo.create(
         acct.getId() != null ? acct.getId().get() : 0,
         acct.getFullName(),
-        acct.getPreferredEmail());
+        acct.getPreferredEmail(),
+        acct.getUserName());
   }
 
   public static AccountInfo asInfo(com.google.gerrit.common.data.AccountInfo acct) {
     if (acct == null) {
-      return AccountInfo.create(0, null, null);
+      return AccountInfo.create(0, null, null, null);
     }
     return AccountInfo.create(
         acct.getId() != null ? acct.getId().get() : 0,
         acct.getFullName(),
-        acct.getPreferredEmail());
+        acct.getPreferredEmail(),
+        acct.getUsername());
   }
 }
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 b7857e4..27a4b53 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
@@ -18,8 +18,10 @@
 import static com.google.gerrit.common.data.GlobalCapability.CREATE_PROJECT;
 import static com.google.gerrit.common.data.GlobalCapability.VIEW_PLUGINS;
 
+import com.google.gerrit.client.account.AccountApi;
 import com.google.gerrit.client.account.AccountCapabilities;
 import com.google.gerrit.client.account.AccountInfo;
+import com.google.gerrit.client.account.Preferences;
 import com.google.gerrit.client.admin.ProjectScreen;
 import com.google.gerrit.client.api.ApiGlue;
 import com.google.gerrit.client.api.PluginLoader;
@@ -30,14 +32,15 @@
 import com.google.gerrit.client.extensions.TopMenuItem;
 import com.google.gerrit.client.extensions.TopMenuList;
 import com.google.gerrit.client.patches.PatchScreen;
+import com.google.gerrit.client.rpc.CallbackGroup;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.LinkMenuBar;
 import com.google.gerrit.client.ui.LinkMenuItem;
 import com.google.gerrit.client.ui.MorphingTabPanel;
 import com.google.gerrit.client.ui.PatchLink;
+import com.google.gerrit.client.ui.ProjectLinkMenuItem;
 import com.google.gerrit.client.ui.Screen;
-import com.google.gerrit.client.ui.ScreenLoadEvent;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.GerritConfig;
 import com.google.gerrit.common.data.GitwebConfig;
@@ -48,7 +51,6 @@
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
 import com.google.gerrit.reviewdb.client.AuthType;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.aria.client.Roles;
 import com.google.gwt.core.client.EntryPoint;
 import com.google.gwt.core.client.GWT;
@@ -70,6 +72,7 @@
 import com.google.gwt.user.client.History;
 import com.google.gwt.user.client.Window;
 import com.google.gwt.user.client.Window.Location;
+import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.FocusPanel;
@@ -104,6 +107,7 @@
   private static GerritConfig myConfig;
   private static HostPageData.Theme myTheme;
   private static Account myAccount;
+  private static String defaultScreenToken;
   private static AccountDiffPreference myAccountDiffPref;
   private static String xGerritAuth;
 
@@ -211,6 +215,10 @@
     }
   }
 
+  public static void selectMenu(LinkMenuBar bar) {
+    menuLeft.selectTab(menuLeft.getWidgetIndex(bar));
+  }
+
   /**
    * Update the current history token after a screen change.
    * <p>
@@ -265,6 +273,10 @@
     return topMenu.isVisible();
   }
 
+  public static String getDefaultScreenToken() {
+    return defaultScreenToken;
+  }
+
   public static RootPanel getBottomMenu() {
     return bottomMenu;
   }
@@ -324,7 +336,8 @@
     } else if (token.startsWith("/")) {
       token = token.substring(1);
     }
-    return selfRedirect("/login/" + token);
+
+    return selfRedirect("login/") + URL.encodePathSegment("#/" + token);
   }
 
   public static String selfRedirect(String suffix) {
@@ -449,8 +462,6 @@
       vs = "dev";
     }
 
-    btmmenu.add(new InlineLabel(C.keyHelp()));
-    btmmenu.add(new InlineLabel(" | "));
     btmmenu.add(new InlineHTML(M.poweredBy(vs)));
 
     final String reportBugText = getConfig().getReportBugText();
@@ -461,6 +472,8 @@
     a.setStyleName("");
     btmmenu.add(new InlineLabel(" | "));
     btmmenu.add(a);
+    btmmenu.add(new InlineLabel(" | "));
+    btmmenu.add(new InlineLabel(C.keyHelp()));
   }
 
   private void onModuleLoad2(HostPageData hpd) {
@@ -537,7 +550,7 @@
 
     applyUserPreferences();
     populateBottomMenu(bottomMenu, hpd);
-    refreshMenuBar();
+    refreshMenuBar(false);
 
     History.addValueChangeHandler(new ValueChangeHandler<String>() {
       @Override
@@ -547,18 +560,27 @@
     });
     JumpKeys.register(body);
 
-    String token = History.getToken();
-    if (token.isEmpty()) {
-      token = isSignedIn()
-          ? PageLinks.MINE
-          : PageLinks.toChangeQuery("status:open");
-    }
-
     saveDefaultTheme();
     if (hpd.messages != null) {
       new MessageOfTheDayBar(hpd.messages).show();
     }
-    PluginLoader.load(hpd.plugins, token);
+    CallbackGroup cbg = new CallbackGroup();
+    if (isSignedIn()) {
+      AccountApi.self().view("preferences").get(cbg.add(createMyMenuBarCallback()));
+    }
+    PluginLoader.load(hpd.plugins,
+        cbg.addFinal(new GerritCallback<VoidResult>() {
+          @Override
+          public void onSuccess(VoidResult result) {
+            String token = History.getToken();
+            if (token.isEmpty()) {
+              token = isSignedIn()
+                  ? PageLinks.MINE
+                  : PageLinks.toChangeQuery("status:open");
+            }
+            display(token);
+          }
+        }));
   }
 
   private void saveDefaultTheme() {
@@ -568,6 +590,10 @@
   }
 
   public static void refreshMenuBar() {
+    refreshMenuBar(true);
+  }
+
+  private static void refreshMenuBar(boolean populateMyMenu) {
     menuLeft.clear();
     menuRight.clear();
 
@@ -585,14 +611,12 @@
     menuLeft.add(m, C.menuAll());
 
     if (signedIn) {
-      m = new LinkMenuBar();
-      menuBars.put(GerritTopMenu.MY.menuName, m);
-      addLink(m, C.menuMyChanges(), PageLinks.MINE);
-      addLink(m, C.menuMyDrafts(), PageLinks.toChangeQuery("is:draft"));
-      addLink(m, C.menuMyDraftComments(), PageLinks.toChangeQuery("has:draft"));
-      addLink(m, C.menuMyWatchedChanges(), PageLinks.toChangeQuery("is:watched status:open"));
-      addLink(m, C.menuMyStarredChanges(), PageLinks.toChangeQuery("is:starred"));
-      menuLeft.add(m, C.menuMine());
+      LinkMenuBar myBar = new LinkMenuBar();
+      menuBars.put(GerritTopMenu.MY.menuName, myBar);
+      if (populateMyMenu) {
+        AccountApi.self().view("preferences").get(createMyMenuBarCallback());
+      }
+      menuLeft.add(myBar, C.menuMine());
       menuLeft.selectTab(1);
     } else {
       menuLeft.selectTab(0);
@@ -609,22 +633,21 @@
     addDiffLink(diffBar, C.menuDiffPatchSets(), PatchScreen.TopView.PATCH_SETS);
     addDiffLink(diffBar, C.menuDiffFiles(), PatchScreen.TopView.FILES);
 
-    final LinkMenuBar projectsBar = new LinkMenuBar() {
-      @Override
-      public void onScreenLoad(ScreenLoadEvent event) {
-        if (event.getScreen() instanceof ProjectScreen) {
-          menuLeft.selectTab(menuLeft.getWidgetIndex(this));
-        }
-      }
-    };
+    final LinkMenuBar projectsBar = new LinkMenuBar();
     menuBars.put(GerritTopMenu.PROJECTS.menuName, projectsBar);
     addLink(projectsBar, C.menuProjectsList(), PageLinks.ADMIN_PROJECTS);
-    addProjectLink(projectsBar, C.menuProjectsInfo(), ProjectScreen.INFO);
-    addProjectLink(projectsBar, C.menuProjectsBranches(), ProjectScreen.BRANCH);
-    addProjectLink(projectsBar, C.menuProjectsAccess(), ProjectScreen.ACCESS);
+    projectsBar.addItem(new ProjectLinkMenuItem(C.menuProjectsInfo(), ProjectScreen.INFO));
+    projectsBar.addItem(new ProjectLinkMenuItem(C.menuProjectsBranches(), ProjectScreen.BRANCH));
+    projectsBar.addItem(new ProjectLinkMenuItem(C.menuProjectsAccess(), ProjectScreen.ACCESS));
     final LinkMenuItem dashboardsMenuItem =
-        addProjectLink(projectsBar, C.menuProjectsDashboards(),
-            ProjectScreen.DASHBOARDS);
+        new ProjectLinkMenuItem(C.menuProjectsDashboards(),
+            ProjectScreen.DASHBOARDS) {
+      protected boolean match(String token) {
+        return super.match(token) ||
+            (!getTargetHistoryToken().isEmpty() && ("/admin" + token).startsWith(getTargetHistoryToken()));
+      }
+    };
+    projectsBar.addItem(dashboardsMenuItem);
     menuLeft.add(projectsBar, C.menuProjects());
 
     if (signedIn) {
@@ -695,6 +718,15 @@
           });
           break;
 
+        case OAUTH:
+          menuRight.addItem(C.menuSignIn(), new Command() {
+            @Override
+            public void execute() {
+              doSignIn(History.getToken());
+            }
+          });
+          break;
+
         case OPENID_SSO:
           menuRight.addItem(C.menuSignIn(), new Command() {
             public void execute() {
@@ -748,6 +780,27 @@
     });
   }
 
+  private static AsyncCallback<Preferences> createMyMenuBarCallback() {
+    return new GerritCallback<Preferences>() {
+      @Override
+      public void onSuccess(Preferences prefs) {
+        LinkMenuBar myBar = menuBars.get(GerritTopMenu.MY.menuName);
+        myBar.clear();
+        List<TopMenuItem> myMenuItems = Natives.asList(prefs.my());
+        String url = null;
+        if (!myMenuItems.isEmpty()) {
+          if (myMenuItems.get(0).getUrl().startsWith("#")) {
+            url = myMenuItems.get(0).getUrl().substring(1);
+          }
+          for (TopMenuItem item : myMenuItems) {
+            addExtensionLink(myBar, item);
+          }
+        }
+        defaultScreenToken = url;
+      }
+    };
+  }
+
   public static void applyUserPreferences() {
     if (myAccount != null) {
       final AccountGeneralPreferences p = myAccount.getGeneralPreferences();
@@ -839,32 +892,6 @@
       });
   }
 
-  private static LinkMenuItem addProjectLink(final LinkMenuBar m, final String text,
-      final String panel) {
-    LinkMenuItem i = new LinkMenuItem(text, "") {
-        @Override
-        public void onScreenLoad(ScreenLoadEvent event) {
-          Screen screen = event.getScreen();
-          Project.NameKey projectKey;
-          if (screen instanceof ProjectScreen) {
-            projectKey = ((ProjectScreen)screen).getProjectKey();
-          } else {
-            projectKey = ProjectScreen.getSavedKey();
-          }
-
-          if (projectKey != null) {
-            setVisible(true);
-            setTargetHistoryToken(Dispatcher.toProjectAdmin(projectKey, panel));
-          } else {
-            setVisible(false);
-          }
-          super.onScreenLoad(event);
-        }
-      };
-    m.addItem(i);
-    return i;
-  }
-
   private static void addDiffLink(final LinkMenuBar m, final String text,
       final PatchScreen.Type type) {
     m.addItem(new LinkMenuItem(text, "") {
@@ -890,15 +917,26 @@
   }
 
   private static void addExtensionLink(LinkMenuBar m, TopMenuItem item) {
-    Anchor atag = anchor(item.getName(), isAbsolute(item.getUrl())
-        ? item.getUrl()
-        : selfRedirect(item.getUrl()));
-
-    atag.setTarget(item.getTarget());
-    if (item.getId() != null) {
-      atag.getElement().setAttribute("id", item.getId());
+    if (item.getUrl().startsWith("#")
+        && (item.getTarget() == null || item.getTarget().isEmpty())) {
+      LinkMenuItem a =
+          new LinkMenuItem(item.getName(), item.getUrl().substring(1));
+      if (item.getId() != null) {
+        a.getElement().setAttribute("id", item.getId());
+      }
+      m.addItem(a);
+    } else {
+      Anchor atag = anchor(item.getName(), isAbsolute(item.getUrl())
+          ? item.getUrl()
+          : selfRedirect(item.getUrl()));
+      if (item.getTarget() != null && !item.getTarget().isEmpty()) {
+        atag.setTarget(item.getTarget());
+      }
+      if (item.getId() != null) {
+        atag.getElement().setAttribute("id", item.getId());
+      }
+      m.add(atag);
     }
-    m.add(atag);
   }
 
   private static boolean isAbsolute(String url) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
index 73c270b..5ba520c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
@@ -117,4 +117,9 @@
   String projectAccessProposeForReviewHint();
 
   String userCannotVoteToolTip();
+
+  String stringListPanelAdd();
+  String stringListPanelDelete();
+  String stringListPanelUp();
+  String stringListPanelDown();
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
index 63b8fe1..58a6d08 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
@@ -100,3 +100,8 @@
 projectAccessProposeForReviewHint = You may propose these modifications to the project owners by clicking on 'Save for Review'.
 
 userCannotVoteToolTip = User cannot vote in this category
+
+stringListPanelAdd = Add
+stringListPanelDelete = Delete
+stringListPanelUp = Up
+stringListPanelDown = Down
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 98b9534..42a8d74 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
@@ -85,7 +85,9 @@
   String createGroupLink();
   String createProjectPanel();
   String dataCell();
+  String dataCellHidden();
   String dataHeader();
+  String dataHeaderHidden();
   String diffLinkCell();
   String diffText();
   String diffTextCONTEXT();
@@ -232,6 +234,7 @@
   String sshHostKeyPanelKnownHostEntry();
   String sshKeyPanelEncodedKey();
   String sshKeyPanelInvalid();
+  String stringListPanelButtons();
   String topMostCell();
   String topmenu();
   String topmenuMenuLeft();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java
index e50445d..2b78fa4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java
@@ -28,6 +28,12 @@
   @Source("arrowRight.gif")
   public ImageResource arrowRight();
 
+  @Source("arrowUp.png")
+  public ImageResource arrowUp();
+
+  @Source("arrowDown.png")
+  public ImageResource arrowDown();
+
   @Source("editText.png")
   public ImageResource edit();
 
@@ -70,6 +76,9 @@
   @Source("warning.png")
   public ImageResource warning();
 
+  @Source("listAdd.png")
+  public ImageResource listAdd();
+
   @Source("merge.png")
   public ImageResource merge();
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/MessageOfTheDayBar.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/MessageOfTheDayBar.java
index fa569b8..b6ea636 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/MessageOfTheDayBar.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/MessageOfTheDayBar.java
@@ -52,6 +52,8 @@
       for (HostPageData.Message m : motd) {
         b.openDiv();
         b.append(SafeHtml.asis(m.html));
+        b.openElement("hr");
+        b.closeSelf();
         b.closeDiv();
       }
     }
@@ -74,7 +76,7 @@
   }
 
   private static List<HostPageData.Message> filter(List<HostPageData.Message> in) {
-    List<HostPageData.Message> show = new ArrayList<HostPageData.Message>();
+    List<HostPageData.Message> show = new ArrayList<>();
     for (HostPageData.Message m : in) {
       if (Cookies.getCookie(cookieName(m)) == null) {
         show.add(m);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
index 0715aee..e92e613 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
@@ -81,6 +81,7 @@
     suggestions.add("comment:");
     suggestions.add("conflicts:");
     suggestions.add("project:");
+    suggestions.add("projects:");
     suggestions.add("parentproject:");
     suggestions.add("branch:");
     suggestions.add("topic:");
@@ -101,6 +102,7 @@
     suggestions.add("is:owner");
     suggestions.add("is:reviewer");
     suggestions.add("is:open");
+    suggestions.add("is:pending");
     suggestions.add("is:draft");
     suggestions.add("is:closed");
     suggestions.add("is:submitted");
@@ -110,12 +112,18 @@
 
     suggestions.add("status:");
     suggestions.add("status:open");
+    suggestions.add("status:pending");
     suggestions.add("status:reviewed");
     suggestions.add("status:submitted");
     suggestions.add("status:closed");
     suggestions.add("status:merged");
     suggestions.add("status:abandoned");
 
+    suggestions.add("added:");
+    suggestions.add("deleted:");
+    suggestions.add("delta:");
+    suggestions.add("size:");
+
     suggestions.add("AND");
     suggestions.add("OR");
     suggestions.add("NOT");
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/StringListPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/StringListPanel.java
new file mode 100644
index 0000000..bc36654
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/StringListPanel.java
@@ -0,0 +1,359 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client;
+
+import com.google.gerrit.client.ui.NavigationTable;
+import com.google.gerrit.client.ui.OnEditEnabler;
+import com.google.gerrit.client.ui.SmallHeading;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.event.logical.shared.ValueChangeHandler;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.CheckBox;
+import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.FocusWidget;
+import com.google.gwt.user.client.ui.HasEnabled;
+import com.google.gwt.user.client.ui.HorizontalPanel;
+import com.google.gwt.user.client.ui.Image;
+import com.google.gwt.user.client.ui.ImageResourceRenderer;
+import com.google.gwtexpui.globalkey.client.NpTextBox;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class StringListPanel extends FlowPanel implements HasEnabled {
+  private final StringListTable t;
+  private HorizontalPanel titlePanel;
+  protected final HorizontalPanel buttonPanel;
+  private final Button deleteButton;
+  private Image info;
+  protected FocusWidget widget;
+
+  public StringListPanel(String title, List<String> fieldNames, FocusWidget w,
+      boolean autoSort) {
+    widget = w;
+    if (title != null) {
+      titlePanel = new HorizontalPanel();
+      SmallHeading titleLabel = new SmallHeading(title);
+      titlePanel.add(titleLabel);
+      add(titlePanel);
+    }
+
+    t = new StringListTable(fieldNames, autoSort);
+    add(t);
+
+    buttonPanel = new HorizontalPanel();
+    buttonPanel.setStyleName(Gerrit.RESOURCES.css().stringListPanelButtons());
+    deleteButton = new Button(Gerrit.C.stringListPanelDelete());
+    deleteButton.setEnabled(false);
+    deleteButton.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(ClickEvent event) {
+        widget.setEnabled(true);
+        t.deleteChecked();
+      }
+    });
+    buttonPanel.add(deleteButton);
+    add(buttonPanel);
+  }
+
+  public void display(List<List<String>> values) {
+    t.display(values);
+  }
+
+  public void setInfo(String msg) {
+    if (info == null && titlePanel != null) {
+      info = new Image(Gerrit.RESOURCES.info());
+      titlePanel.add(info);
+    }
+    if (info != null) {
+      info.setTitle(msg);
+    }
+  }
+
+  public List<List<String>> getValues() {
+    return t.getValues();
+  }
+
+  public List<String> getValues(int i) {
+    List<List<String>> allValuesList = getValues();
+    List<String> singleValueList = new ArrayList<>(allValuesList.size());
+    for (List<String> values : allValuesList) {
+      singleValueList.add(values.get(i));
+    }
+    return singleValueList;
+  }
+
+  private class StringListTable extends NavigationTable<List<String>> {
+    private final Button addButton;
+    private final List<NpTextBox> inputs;
+    private final boolean autoSort;
+
+    StringListTable(List<String> names, boolean autoSort) {
+      this.autoSort = autoSort;
+
+      addButton =
+          new Button(new ImageResourceRenderer().render(Gerrit.RESOURCES.listAdd()));
+      addButton.setTitle(Gerrit.C.stringListPanelAdd());
+      OnEditEnabler e = new OnEditEnabler(addButton);
+      inputs = new ArrayList<>();
+
+      FlexCellFormatter fmt = table.getFlexCellFormatter();
+      fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().iconHeader());
+      fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().leftMostCell());
+      for (int i = 0; i < names.size(); i++) {
+        fmt.addStyleName(0, i + 1, Gerrit.RESOURCES.css().dataHeader());
+        table.setText(0, i + 1, names.get(i));
+
+        NpTextBox input = new NpTextBox();
+        input.setVisibleLength(35);
+        input.addKeyPressHandler(new KeyPressHandler() {
+          @Override
+          public void onKeyPress(KeyPressEvent event) {
+            if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
+              widget.setEnabled(true);
+              add();
+            }
+          }
+        });
+        inputs.add(input);
+        fmt.addStyleName(1, i + 1, Gerrit.RESOURCES.css().dataHeader());
+        table.setWidget(1, i + 1, input);
+        e.listenTo(input);
+      }
+      addButton.setEnabled(false);
+
+      addButton.addClickHandler(new ClickHandler() {
+        @Override
+        public void onClick(ClickEvent event) {
+          widget.setEnabled(true);
+          add();
+        }
+      });
+      fmt.addStyleName(1, 0, Gerrit.RESOURCES.css().iconHeader());
+      fmt.addStyleName(1, 0, Gerrit.RESOURCES.css().leftMostCell());
+      table.setWidget(1, 0, addButton);
+
+      if (!autoSort) {
+        fmt.addStyleName(0, names.size() + 1, Gerrit.RESOURCES.css().iconHeader());
+        fmt.addStyleName(0, names.size() + 2, Gerrit.RESOURCES.css().iconHeader());
+        fmt.addStyleName(1, names.size() + 1, Gerrit.RESOURCES.css().iconHeader());
+        fmt.addStyleName(1, names.size() + 2, Gerrit.RESOURCES.css().iconHeader());
+      }
+    }
+
+    void display(List<List<String>> values) {
+      for (int row = 2; row < table.getRowCount(); row++) {
+        table.removeRow(row--);
+      }
+      int row = 2;
+      for (List<String> v : values) {
+        populate(row, v);
+        row++;
+      }
+      updateNavigationLinks();
+    }
+
+    List<List<String>> getValues() {
+      List<List<String>> values = new ArrayList<>();
+      for (int row = 2; row < table.getRowCount(); row++) {
+        values.add(getRowItem(row));
+      }
+      return values;
+    }
+
+    @Override
+    protected List<String> getRowItem(int row) {
+      List<String> v = new ArrayList<>();
+      for (int i = 0; i < inputs.size(); i++) {
+        v.add(table.getText(row, i + 1));
+      }
+      return v;
+    }
+
+    private void populate(final int row, List<String> values) {
+      FlexCellFormatter fmt = table.getFlexCellFormatter();
+      fmt.addStyleName(row, 0, Gerrit.RESOURCES.css().iconCell());
+      fmt.addStyleName(row, 0, Gerrit.RESOURCES.css().leftMostCell());
+      CheckBox checkBox = new CheckBox();
+      checkBox.addValueChangeHandler(new ValueChangeHandler<Boolean>() {
+        @Override
+        public void onValueChange(ValueChangeEvent<Boolean> event) {
+          enableDelete();
+        }
+      });
+      table.setWidget(row, 0, checkBox);
+      for (int i = 0; i < values.size(); i++) {
+        fmt.addStyleName(row, i + 1, Gerrit.RESOURCES.css().dataCell());
+        table.setText(row, i + 1, values.get(i));
+      }
+      if (!autoSort) {
+        fmt.addStyleName(row, values.size() + 1, Gerrit.RESOURCES.css().iconCell());
+        fmt.addStyleName(row, values.size() + 2, Gerrit.RESOURCES.css().dataCell());
+
+        Image down = new Image(Gerrit.RESOURCES.arrowDown());
+        down.setTitle(Gerrit.C.stringListPanelDown());
+        down.addClickHandler(new ClickHandler() {
+          @Override
+          public void onClick(ClickEvent event) {
+            moveDown(row);
+          }
+        });
+        table.setWidget(row, values.size() + 1, down);
+
+        Image up = new Image(Gerrit.RESOURCES.arrowUp());
+        up.setTitle(Gerrit.C.stringListPanelUp());
+        up.addClickHandler(new ClickHandler() {
+          @Override
+          public void onClick(ClickEvent event) {
+            moveUp(row);
+          }
+        });
+        table.setWidget(row, values.size() + 2, up);
+      }
+    }
+
+    @Override
+    protected void onCellSingleClick(Event event, int row, int column) {
+      if (column == inputs.size() + 1 && row >= 2
+          && row < table.getRowCount() - 2) {
+        moveDown(row);
+      } else if (column == inputs.size() + 2 && row > 2) {
+        moveUp(row);
+      }
+    }
+
+    void moveDown(int row) {
+      if (row < table.getRowCount() - 1) {
+        swap(row, row + 1);
+      }
+    }
+
+    void moveUp(int row) {
+      if (row > 2) {
+        swap(row - 1, row);
+      }
+    }
+
+    void swap(int row1, int row2) {
+      List<String> value = getRowItem(row1);
+      List<String> nextValue = getRowItem(row2);
+      populate(row1, nextValue);
+      populate(row2, value);
+      updateNavigationLinks();
+      widget.setEnabled(true);
+    }
+
+    private void updateNavigationLinks() {
+      if (!autoSort) {
+        for (int row = 2; row < table.getRowCount(); row++) {
+          table.getWidget(row, inputs.size() + 1).setVisible(
+              row < table.getRowCount() - 1);
+          table.getWidget(row, inputs.size() + 2).setVisible(row > 2);
+        }
+      }
+    }
+
+    void add() {
+      List<String> values = new ArrayList<>();
+      for (NpTextBox input : inputs) {
+        values.add(input.getValue().trim());
+        input.setValue("");
+      }
+      insert(values);
+    }
+
+    void insert(List<String> v) {
+      int insertPos = table.getRowCount();
+      if (autoSort) {
+        for (int row = 1; row < table.getRowCount(); row++) {
+          int compareResult = v.get(0).compareTo(table.getText(row, 1));
+          if (compareResult < 0)  {
+            insertPos = row;
+            break;
+          } else if (compareResult == 0) {
+            return;
+          }
+        }
+      }
+      table.insertRow(insertPos);
+      populate(insertPos, v);
+      updateNavigationLinks();
+    }
+
+    void enableDelete() {
+      for (int row = 2; row < table.getRowCount(); row++) {
+        if (((CheckBox) table.getWidget(row, 0)).getValue()) {
+          deleteButton.setEnabled(true);
+          return;
+        }
+      }
+      deleteButton.setEnabled(false);
+    }
+
+    void deleteChecked() {
+      deleteButton.setEnabled(false);
+      for (int row = 2; row < table.getRowCount(); row++) {
+        if (((CheckBox) table.getWidget(row, 0)).getValue()) {
+          table.removeRow(row--);
+        }
+      }
+      updateNavigationLinks();
+    }
+
+    @Override
+    protected void onOpenRow(int row) {
+    }
+
+    @Override
+    protected Object getRowItemKey(List<String> item) {
+      return item.get(0);
+    }
+
+    void setEnabled(boolean enabled) {
+      addButton.setVisible(enabled);
+      for (NpTextBox input : inputs) {
+        input.setEnabled(enabled);
+      }
+      for (int row = 2; row < table.getRowCount(); row++) {
+        table.getWidget(row, 0).setVisible(enabled);
+        if (!autoSort) {
+          table.getWidget(row, inputs.size() + 1).setVisible(enabled);
+          table.getWidget(row, inputs.size() + 2).setVisible(enabled);
+        }
+      }
+      if (enabled) {
+        updateNavigationLinks();
+      }
+    }
+  }
+
+  @Override
+  public boolean isEnabled() {
+    return deleteButton.isVisible();
+  }
+
+  @Override
+  public void setEnabled(boolean enabled) {
+    t.setEnabled(enabled);
+    deleteButton.setVisible(enabled);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_58.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/WebLinkInfo.java
similarity index 61%
copy from gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_58.java
copy to gerrit-gwtui/src/main/java/com/google/gerrit/client/WebLinkInfo.java
index 72b0c8b..64b9cb8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_58.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/WebLinkInfo.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2011 The Android Open Source Project
+// Copyright (C) 2014 The Android Open 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,14 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.schema;
+package com.google.gerrit.client;
 
-import com.google.inject.Inject;
-import com.google.inject.Provider;
+import com.google.gwt.core.client.JavaScriptObject;
 
-public class Schema_58 extends SchemaVersion {
-  @Inject
-  Schema_58(Provider<Schema_57> prior) {
-    super(prior);
+public class WebLinkInfo extends JavaScriptObject {
+
+  public final native String name() /*-{ return this.name; }-*/;
+  public final native String url() /*-{ return this.url; }-*/;
+
+  protected WebLinkInfo() {
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
index b0a6160..f52eb31 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
@@ -35,10 +35,21 @@
   String useFlashClipboard();
   String copySelfOnEmails();
   String reversePatchSetOrder();
-  String showUsernameInReviewCategory();
+  String reviewCategoryLabel();
+  String messageShowInReviewCategoryNone();
+  String messageShowInReviewCategoryName();
+  String messageShowInReviewCategoryEmail();
+  String messageShowInReviewCategoryUsername();
+  String messageShowInReviewCategoryAbbrev();
   String buttonSaveChanges();
   String showRelativeDateInChangeTable();
   String showSizeBarInChangeTable();
+  String showLegacycidInChangeTable();
+  String myMenu();
+  String myMenuInfo();
+  String myMenuName();
+  String myMenuUrl();
+  String myMenuReset();
 
   String changeScreenOldUi();
   String changeScreenNewUi();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
index 63cb871..5d48bb8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
@@ -9,7 +9,14 @@
 useFlashClipboard = Use Flash Clipboard Widget
 copySelfOnEmails = CC Me On Comments I Write
 reversePatchSetOrder = Display Patch Sets In Reverse Order (deprecated: Old Change Screen)
-showUsernameInReviewCategory = Display Person Name In Review Category
+
+reviewCategoryLabel = Display In Review Category
+messageShowInReviewCategoryNone = None (default)
+messageShowInReviewCategoryName = Show Name
+messageShowInReviewCategoryEmail = Show Email
+messageShowInReviewCategoryUsername = Show Username
+messageShowInReviewCategoryAbbrev = Show Abbreviated Name
+
 maximumPageSizeFieldLabel = Maximum Page Size:
 commentVisibilityLabel = Comment Visibility (deprecated: Old Change Screen):
 changeScreenLabel = Change View:
@@ -19,6 +26,14 @@
 buttonSaveChanges = Save Changes
 showRelativeDateInChangeTable = Show Relative Dates In Changes Table
 showSizeBarInChangeTable = Show Change Sizes As Colored Bars In Changes Table
+showLegacycidInChangeTable = Show Change Number In Changes Table
+myMenu = My Menu
+myMenuInfo = \
+  Menu items for the 'My' top level menu. \
+  The first menu item defines the default screen.
+myMenuName = Name
+myMenuUrl = URL
+myMenuReset = Reset
 
 changeScreenOldUi = Old Screen
 changeScreenNewUi = New Screen
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountInfo.java
index 26d551f..3ac626c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountInfo.java
@@ -21,6 +21,7 @@
   public final native int _account_id() /*-{ return this._account_id || 0; }-*/;
   public final native String name() /*-{ return this.name; }-*/;
   public final native String email() /*-{ return this.email; }-*/;
+  public final native String username() /*-{ return this.username; }-*/;
 
   /**
    * @return true if the server supplied avatar information about this account.
@@ -46,8 +47,9 @@
   /*-{ return this.avatars }-*/;
 
   public static native AccountInfo create(int id, String name,
-      String email) /*-{
-    return {'_account_id': id, 'name': name, 'email': email};
+      String email, String username) /*-{
+    return {'_account_id': id, 'name': name, 'email': email,
+        'username': username};
   }-*/;
 
   protected AccountInfo() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/DiffPreferences.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/DiffPreferences.java
index 5c66f3c..8a4666b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/DiffPreferences.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/DiffPreferences.java
@@ -40,6 +40,7 @@
     p.manualReview(in.isManualReview());
     p.renderEntireFile(in.isRenderEntireFile());
     p.theme(in.getTheme());
+    p.hideEmptyPane(in.isHideEmptyPane());
     return p;
   }
 
@@ -59,6 +60,7 @@
     p.setManualReview(manualReview());
     p.setRenderEntireFile(renderEntireFile());
     p.setTheme(theme());
+    p.setHideEmptyPane(hideEmptyPane());
   }
 
   public final void ignoreWhitespace(Whitespace i) {
@@ -84,6 +86,7 @@
   public final native void expandAllComments(boolean e) /*-{ this.expand_all_comments = e }-*/;
   public final native void manualReview(boolean r) /*-{ this.manual_review = r }-*/;
   public final native void renderEntireFile(boolean r) /*-{ this.render_entire_file = r }-*/;
+  public final native void hideEmptyPane(boolean s) /*-{ this.hide_empty_pane = s }-*/;
   public final void showLineNumbers(boolean s) { hideLineNumbers(!s); }
 
   public final Whitespace ignoreWhitespace() {
@@ -111,6 +114,7 @@
   public final native boolean expandAllComments() /*-{ return this.expand_all_comments || false }-*/;
   public final native boolean manualReview() /*-{ return this.manual_review || false }-*/;
   public final native boolean renderEntireFile() /*-{ return this.render_entire_file || false }-*/;
+  public final native boolean hideEmptyPane() /*-{ return this.hide_empty_pane || false }-*/;
   public final boolean showLineNumbers() { return !hideLineNumbers(); }
   public final boolean autoReview() { return !manualReview(); }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyIdentitiesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyIdentitiesScreen.java
index 4703ef7..1f4d7ed 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyIdentitiesScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyIdentitiesScreen.java
@@ -59,7 +59,8 @@
     });
     add(deleteIdentity);
 
-    if (Gerrit.getConfig().getAuthType() == AuthType.OPENID) {
+    if (Gerrit.getConfig().getAuthType() == AuthType.OPENID
+        || Gerrit.getConfig().getAuthType() == AuthType.OAUTH) {
       Button linkIdentity = new Button(Util.C.buttonLinkIdentity());
       linkIdentity.addClickHandler(new ClickHandler() {
         @Override
@@ -243,15 +244,15 @@
         //
         return "";
 
-      } else if (k.isScheme(OpenIdUrls.URL_GOOGLE)) {
+      } else if (k.isScheme("https://www.google.com/accounts/o8/id")) {
         return OpenIdUtil.C.nameGoogle();
 
+      } else if (k.isScheme(OpenIdUrls.URL_LAUNCHPAD)) {
+        return OpenIdUtil.C.nameLaunchpad();
+
       } else if (k.isScheme(OpenIdUrls.URL_YAHOO)) {
         return OpenIdUtil.C.nameYahoo();
 
-      } else if (k.isScheme(AccountExternalId.LEGACY_GAE)) {
-        return OpenIdUtil.C.nameGoogle() + " (Imported from Google AppEngine)";
-
       } else {
         return k.getExternalId();
       }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
index d8a0427..db1acbf 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
@@ -19,12 +19,17 @@
 
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.StringListPanel;
+import com.google.gerrit.client.config.ConfigServerApi;
+import com.google.gerrit.client.extensions.TopMenuItem;
 import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.client.ui.OnEditEnabler;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.CommentVisibilityStrategy;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.ReviewCategoryStrategy;
+import com.google.gwt.core.client.JsArray;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.i18n.client.DateTimeFormat;
@@ -34,24 +39,28 @@
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Grid;
 import com.google.gwt.user.client.ui.ListBox;
-import com.google.gwtjsonrpc.common.VoidResult;
 
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Date;
+import java.util.List;
 
 public class MyPreferencesScreen extends SettingsScreen {
   private CheckBox showSiteHeader;
   private CheckBox useFlashClipboard;
   private CheckBox copySelfOnEmails;
   private CheckBox reversePatchSetOrder;
-  private CheckBox showUsernameInReviewCategory;
   private CheckBox relativeDateInChangeTable;
   private CheckBox sizeBarInChangeTable;
+  private CheckBox legacycidInChangeTable;
   private ListBox maximumPageSize;
   private ListBox dateFormat;
   private ListBox timeFormat;
+  private ListBox reviewCategoryStrategy;
   private ListBox commentVisibilityStrategy;
   private ListBox changeScreen;
   private ListBox diffView;
+  private StringListPanel myMenus;
   private Button save;
 
   @Override
@@ -62,12 +71,28 @@
     useFlashClipboard = new CheckBox(Util.C.useFlashClipboard());
     copySelfOnEmails = new CheckBox(Util.C.copySelfOnEmails());
     reversePatchSetOrder = new CheckBox(Util.C.reversePatchSetOrder());
-    showUsernameInReviewCategory = new CheckBox(Util.C.showUsernameInReviewCategory());
     maximumPageSize = new ListBox();
     for (final short v : PAGESIZE_CHOICES) {
       maximumPageSize.addItem(Util.M.rowsPerPage(v), String.valueOf(v));
     }
 
+    reviewCategoryStrategy = new ListBox();
+    reviewCategoryStrategy.addItem(
+        Util.C.messageShowInReviewCategoryNone(),
+        AccountGeneralPreferences.ReviewCategoryStrategy.NONE.name());
+    reviewCategoryStrategy.addItem(
+        Util.C.messageShowInReviewCategoryName(),
+        AccountGeneralPreferences.ReviewCategoryStrategy.NAME.name());
+    reviewCategoryStrategy.addItem(
+        Util.C.messageShowInReviewCategoryEmail(),
+        AccountGeneralPreferences.ReviewCategoryStrategy.EMAIL.name());
+    reviewCategoryStrategy.addItem(
+        Util.C.messageShowInReviewCategoryUsername(),
+        AccountGeneralPreferences.ReviewCategoryStrategy.USERNAME.name());
+    reviewCategoryStrategy.addItem(
+        Util.C.messageShowInReviewCategoryAbbrev(),
+        AccountGeneralPreferences.ReviewCategoryStrategy.ABBREV.name());
+
     commentVisibilityStrategy = new ListBox();
     commentVisibilityStrategy.addItem(
         com.google.gerrit.client.changes.Util.C.messageCollapseAll(),
@@ -138,8 +163,9 @@
 
     relativeDateInChangeTable = new CheckBox(Util.C.showRelativeDateInChangeTable());
     sizeBarInChangeTable = new CheckBox(Util.C.showSizeBarInChangeTable());
+    legacycidInChangeTable = new CheckBox(Util.C.showLegacycidInChangeTable());
 
-    final Grid formGrid = new Grid(12, 2);
+    final Grid formGrid = new Grid(13, 2);
 
     int row = 0;
     formGrid.setText(row, labelIdx, "");
@@ -158,8 +184,8 @@
     formGrid.setWidget(row, fieldIdx, reversePatchSetOrder);
     row++;
 
-    formGrid.setText(row, labelIdx, "");
-    formGrid.setWidget(row, fieldIdx, showUsernameInReviewCategory);
+    formGrid.setText(row, labelIdx, Util.C.reviewCategoryLabel());
+    formGrid.setWidget(row, fieldIdx, reviewCategoryStrategy);
     row++;
 
     formGrid.setText(row, labelIdx, Util.C.maximumPageSizeFieldLabel());
@@ -178,6 +204,10 @@
       formGrid.setText(row, labelIdx, "");
       formGrid.setWidget(row, fieldIdx, sizeBarInChangeTable);
       row++;
+
+      formGrid.setText(row, labelIdx, "");
+      formGrid.setWidget(row, fieldIdx, legacycidInChangeTable);
+      row++;
     }
 
     formGrid.setText(row, labelIdx, Util.C.commentVisibilityLabel());
@@ -202,6 +232,10 @@
         doSave();
       }
     });
+
+    myMenus = new MyMenuPanel(save);
+    add(myMenus);
+
     add(save);
 
     final OnEditEnabler e = new OnEditEnabler(save);
@@ -209,12 +243,13 @@
     e.listenTo(useFlashClipboard);
     e.listenTo(copySelfOnEmails);
     e.listenTo(reversePatchSetOrder);
-    e.listenTo(showUsernameInReviewCategory);
     e.listenTo(maximumPageSize);
     e.listenTo(dateFormat);
     e.listenTo(timeFormat);
     e.listenTo(relativeDateInChangeTable);
     e.listenTo(sizeBarInChangeTable);
+    e.listenTo(legacycidInChangeTable);
+    e.listenTo(reviewCategoryStrategy);
     e.listenTo(commentVisibilityStrategy);
     e.listenTo(changeScreen);
     e.listenTo(diffView);
@@ -223,9 +258,11 @@
   @Override
   protected void onLoad() {
     super.onLoad();
-    Util.ACCOUNT_SVC.myAccount(new ScreenLoadCallback<Account>(this) {
-      public void preDisplay(final Account result) {
-        display(result.getGeneralPreferences());
+    AccountApi.self().view("preferences")
+        .get(new ScreenLoadCallback<Preferences>(this) {
+      @Override
+      public void preDisplay(Preferences prefs) {
+        display(prefs);
       }
     });
   }
@@ -235,39 +272,52 @@
     useFlashClipboard.setEnabled(on);
     copySelfOnEmails.setEnabled(on);
     reversePatchSetOrder.setEnabled(on);
-    showUsernameInReviewCategory.setEnabled(on);
     maximumPageSize.setEnabled(on);
     dateFormat.setEnabled(on);
     timeFormat.setEnabled(on);
     relativeDateInChangeTable.setEnabled(on);
     sizeBarInChangeTable.setEnabled(on);
+    legacycidInChangeTable.setEnabled(on);
+    reviewCategoryStrategy.setEnabled(on);
     commentVisibilityStrategy.setEnabled(on);
     changeScreen.setEnabled(on);
     diffView.setEnabled(on);
   }
 
-  private void display(final AccountGeneralPreferences p) {
-    showSiteHeader.setValue(p.isShowSiteHeader());
-    useFlashClipboard.setValue(p.isUseFlashClipboard());
-    copySelfOnEmails.setValue(p.isCopySelfOnEmails());
-    reversePatchSetOrder.setValue(p.isReversePatchSetOrder());
-    showUsernameInReviewCategory.setValue(p.isShowUsernameInReviewCategory());
-    setListBox(maximumPageSize, DEFAULT_PAGESIZE, p.getMaximumPageSize());
+  private void display(Preferences p) {
+    showSiteHeader.setValue(p.showSiteHeader());
+    useFlashClipboard.setValue(p.useFlashClipboard());
+    copySelfOnEmails.setValue(p.copySelfOnEmail());
+    reversePatchSetOrder.setValue(p.reversePatchSetOrder());
+    setListBox(maximumPageSize, DEFAULT_PAGESIZE, p.changesPerPage());
     setListBox(dateFormat, AccountGeneralPreferences.DateFormat.STD, //
-        p.getDateFormat());
+        p.dateFormat());
     setListBox(timeFormat, AccountGeneralPreferences.TimeFormat.HHMM_12, //
-        p.getTimeFormat());
-    relativeDateInChangeTable.setValue(p.isRelativeDateInChangeTable());
-    sizeBarInChangeTable.setValue(p.isSizeBarInChangeTable());
+        p.timeFormat());
+    relativeDateInChangeTable.setValue(p.relativeDateInChangeTable());
+    sizeBarInChangeTable.setValue(p.sizeBarInChangeTable());
+    legacycidInChangeTable.setValue(p.legacycidInChangeTable());
+    setListBox(reviewCategoryStrategy,
+        AccountGeneralPreferences.ReviewCategoryStrategy.NONE,
+        p.reviewCategoryStrategy());
     setListBox(commentVisibilityStrategy,
         AccountGeneralPreferences.CommentVisibilityStrategy.EXPAND_RECENT,
-        p.getCommentVisibilityStrategy());
+        p.commentVisibilityStrategy());
     setListBox(changeScreen,
         null,
-        p.getChangeScreen());
+        p.changeScreen());
     setListBox(diffView,
         AccountGeneralPreferences.DiffView.SIDE_BY_SIDE,
-        p.getDiffView());
+        p.diffView());
+    display(p.my());
+  }
+
+  private void display(JsArray<TopMenuItem> items) {
+    List<List<String>> values = new ArrayList<>();
+    for (TopMenuItem item : Natives.asList(items)) {
+      values.add(Arrays.asList(item.getName(), item.getUrl()));
+    }
+    myMenus.display(values);
   }
 
   private void setListBox(final ListBox f, final short defaultValue,
@@ -327,7 +377,6 @@
     p.setUseFlashClipboard(useFlashClipboard.getValue());
     p.setCopySelfOnEmails(copySelfOnEmails.getValue());
     p.setReversePatchSetOrder(reversePatchSetOrder.getValue());
-    p.setShowUsernameInReviewCategory(showUsernameInReviewCategory.getValue());
     p.setMaximumPageSize(getListBox(maximumPageSize, DEFAULT_PAGESIZE));
     p.setDateFormat(getListBox(dateFormat,
         AccountGeneralPreferences.DateFormat.STD,
@@ -337,6 +386,10 @@
         AccountGeneralPreferences.TimeFormat.values()));
     p.setRelativeDateInChangeTable(relativeDateInChangeTable.getValue());
     p.setSizeBarInChangeTable(sizeBarInChangeTable.getValue());
+    p.setLegacycidInChangeTable(legacycidInChangeTable.getValue());
+    p.setReviewCategoryStrategy(getListBox(reviewCategoryStrategy,
+        ReviewCategoryStrategy.NONE,
+        ReviewCategoryStrategy.values()));
     p.setCommentVisibilityStrategy(getListBox(commentVisibilityStrategy,
         CommentVisibilityStrategy.EXPAND_RECENT,
         CommentVisibilityStrategy.values()));
@@ -350,22 +403,30 @@
     enable(false);
     save.setEnabled(false);
 
-    Util.ACCOUNT_SVC.changePreferences(p, new GerritCallback<VoidResult>() {
-      @Override
-      public void onSuccess(final VoidResult result) {
-        Gerrit.getUserAccount().setGeneralPreferences(p);
-        Gerrit.applyUserPreferences();
-        Dispatcher.changeScreen2 = false;
-        enable(true);
-      }
+    List<TopMenuItem> items = new ArrayList<>();
+    for (List<String> v : myMenus.getValues()) {
+      items.add(TopMenuItem.create(v.get(0), v.get(1)));
+    }
 
-      @Override
-      public void onFailure(final Throwable caught) {
-        enable(true);
-        save.setEnabled(true);
-        super.onFailure(caught);
-      }
-    });
+    AccountApi.self().view("preferences")
+        .post(Preferences.create(p, items), new GerritCallback<Preferences>() {
+          @Override
+          public void onSuccess(Preferences prefs) {
+            Gerrit.getUserAccount().setGeneralPreferences(p);
+            Gerrit.applyUserPreferences();
+            Dispatcher.changeScreen2 = false;
+            enable(true);
+            display(prefs);
+            Gerrit.refreshMenuBar();
+          }
+
+          @Override
+          public void onFailure(Throwable caught) {
+            enable(true);
+            save.setEnabled(true);
+            super.onFailure(caught);
+          }
+        });
   }
 
   private static String getLabel(AccountGeneralPreferences.ChangeScreen ui) {
@@ -381,4 +442,28 @@
         return ui.name();
     }
   }
+
+  private class MyMenuPanel extends StringListPanel {
+    MyMenuPanel(Button save) {
+      super(Util.C.myMenu(), Arrays.asList(Util.C.myMenuName(),
+          Util.C.myMenuUrl()), save, false);
+
+      setInfo(Util.C.myMenuInfo());
+
+      Button resetButton = new Button(Util.C.myMenuReset());
+      resetButton.addClickHandler(new ClickHandler() {
+        @Override
+        public void onClick(ClickEvent event) {
+          ConfigServerApi.defaultPreferences(new GerritCallback<Preferences>() {
+            @Override
+            public void onSuccess(Preferences p) {
+              MyPreferencesScreen.this.display(p.my());
+              widget.setEnabled(true);
+            }
+          });
+        }
+      });
+      buttonPanel.add(resetButton);
+    }
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/Preferences.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/Preferences.java
new file mode 100644
index 0000000..25036d3
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/Preferences.java
@@ -0,0 +1,227 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.account;
+
+import com.google.gerrit.client.extensions.TopMenuItem;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.ChangeScreen;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.CommentVisibilityStrategy;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.ReviewCategoryStrategy;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DateFormat;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DiffView;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.TimeFormat;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+
+import java.util.List;
+
+public class Preferences extends JavaScriptObject {
+  public static Preferences create(AccountGeneralPreferences in, List<TopMenuItem> myMenus) {
+    Preferences p = createObject().cast();
+    if (in == null) {
+      in = AccountGeneralPreferences.createDefault();
+    }
+    p.changesPerPage(in.getMaximumPageSize());
+    p.showSiteHeader(in.isShowSiteHeader());
+    p.useFlashClipboard(in.isUseFlashClipboard());
+    p.downloadScheme(in.getDownloadUrl());
+    p.downloadCommand(in.getDownloadCommand());
+    p.copySelfOnEmail(in.isCopySelfOnEmails());
+    p.dateFormat(in.getDateFormat());
+    p.timeFormat(in.getTimeFormat());
+    p.reversePatchSetOrder(in.isReversePatchSetOrder());
+    p.relativeDateInChangeTable(in.isRelativeDateInChangeTable());
+    p.sizeBarInChangeTable(in.isSizeBarInChangeTable());
+    p.legacycidInChangeTable(in.isLegacycidInChangeTable());
+    p.commentVisibilityStrategy(in.getCommentVisibilityStrategy());
+    p.reviewCategoryStrategy(in.getReviewCategoryStrategy());
+    p.diffView(in.getDiffView());
+    p.changeScreen(in.getChangeScreen());
+    p.setMyMenus(myMenus);
+    return p;
+  }
+
+  public final short changesPerPage() {
+    return get("changes_per_page", AccountGeneralPreferences.DEFAULT_PAGESIZE);
+  }
+  private final native short get(String n, int d)
+  /*-{ return this.hasOwnProperty(n) ? this[n] : d }-*/;
+
+  public final native boolean showSiteHeader()
+  /*-{ return this.show_site_header || false }-*/;
+
+  public final native boolean useFlashClipboard()
+  /*-{ return this.use_flash_clipboard || false }-*/;
+
+  public final DownloadScheme downloadScheme() {
+    String s = downloadSchemeRaw();
+    return s != null ? DownloadScheme.valueOf(s) : null;
+  }
+  private final native String downloadSchemeRaw()
+  /*-{ return this.download_scheme }-*/;
+
+  public final DownloadCommand downloadCommand() {
+    String s = downloadCommandRaw();
+    return s != null ? DownloadCommand.valueOf(s) : null;
+  }
+  private final native String downloadCommandRaw()
+  /*-{ return this.download_command }-*/;
+
+  public final native boolean copySelfOnEmail()
+  /*-{ return this.copy_self_on_email || false }-*/;
+
+  public final DateFormat dateFormat() {
+    String s = dateFormatRaw();
+    return s != null ? DateFormat.valueOf(s) : null;
+  }
+  private final native String dateFormatRaw()
+  /*-{ return this.date_format }-*/;
+
+  public final TimeFormat timeFormat() {
+    String s = timeFormatRaw();
+    return s != null ? TimeFormat.valueOf(s) : null;
+  }
+  private final native String timeFormatRaw()
+  /*-{ return this.time_format }-*/;
+
+  public final native boolean reversePatchSetOrder()
+  /*-{ return this.reverse_patch_set_order || false }-*/;
+
+  public final native boolean relativeDateInChangeTable()
+  /*-{ return this.relative_date_in_change_table || false }-*/;
+
+  public final native boolean sizeBarInChangeTable()
+  /*-{ return this.size_bar_in_change_table || false }-*/;
+
+  public final native boolean legacycidInChangeTable()
+  /*-{ return this.legacycid_in_change_table || false }-*/;
+
+  public final ReviewCategoryStrategy reviewCategoryStrategy() {
+    String s = reviewCategeoryStrategyRaw();
+    return s != null ? ReviewCategoryStrategy.valueOf(s) : ReviewCategoryStrategy.NONE;
+  }
+  private final native String reviewCategeoryStrategyRaw()
+  /*-{ return this.review_category_strategy }-*/;
+
+  public final CommentVisibilityStrategy commentVisibilityStrategy() {
+    String s = commentVisibilityStrategyRaw();
+    return s != null ? CommentVisibilityStrategy.valueOf(s) : null;
+  }
+  private final native String commentVisibilityStrategyRaw()
+  /*-{ return this.comment_visibility_strategy }-*/;
+
+  public final DiffView diffView() {
+    String s = diffViewRaw();
+    return s != null ? DiffView.valueOf(s) : null;
+  }
+  private final native String diffViewRaw()
+  /*-{ return this.diff_view }-*/;
+
+  public final ChangeScreen changeScreen() {
+    String s = changeScreenRaw();
+    return s != null ? ChangeScreen.valueOf(s) : null;
+  }
+  private final native String changeScreenRaw()
+  /*-{ return this.change_screen }-*/;
+
+  public final native JsArray<TopMenuItem> my()
+  /*-{ return this.my; }-*/;
+
+  public final native void changesPerPage(short n)
+  /*-{ this.changes_per_page = n }-*/;
+
+  public final native void showSiteHeader(boolean s)
+  /*-{ this.show_site_header = s }-*/;
+
+  public final native void useFlashClipboard(boolean u)
+  /*-{ this.use_flash_clipboard = u }-*/;
+
+  public final void downloadScheme(DownloadScheme d) {
+    downloadSchemeRaw(d != null ? d.toString() : null);
+  }
+  private final native void downloadSchemeRaw(String d)
+  /*-{ this.download_scheme = d }-*/;
+
+  public final void downloadCommand(DownloadCommand d) {
+    downloadCommandRaw(d != null ? d.toString() : null);
+  }
+  public final native void downloadCommandRaw(String d)
+  /*-{ this.download_command = d }-*/;
+
+  public final native void copySelfOnEmail(boolean c)
+  /*-{ this.copy_self_on_email = c }-*/;
+
+  public final void dateFormat(DateFormat f) {
+    dateFormatRaw(f != null ? f.toString() : null);
+  }
+  private final native void dateFormatRaw(String f)
+  /*-{ this.date_format = f }-*/;
+
+  public final void timeFormat(TimeFormat f) {
+    timeFormatRaw(f != null ? f.toString() : null);
+  }
+  private final native void timeFormatRaw(String f)
+  /*-{ this.time_format = f }-*/;
+
+  public final native void reversePatchSetOrder(boolean r)
+  /*-{ this.reverse_patch_set_order = r }-*/;
+
+  public final native void relativeDateInChangeTable(boolean d)
+  /*-{ this.relative_date_in_change_table = d }-*/;
+
+  public final native void sizeBarInChangeTable(boolean s)
+  /*-{ this.size_bar_in_change_table = s }-*/;
+
+  public final native void legacycidInChangeTable(boolean s)
+  /*-{ this.legacycid_in_change_table = s }-*/;
+
+  public final void reviewCategoryStrategy(ReviewCategoryStrategy s) {
+    reviewCategoryStrategyRaw(s != null ? s.toString() : null);
+  }
+  private final native void reviewCategoryStrategyRaw(String s)
+  /*-{ this.review_category_strategy = s }-*/;
+
+  public final void commentVisibilityStrategy(CommentVisibilityStrategy s) {
+    commentVisibilityStrategyRaw(s != null ? s.toString() : null);
+  }
+  private final native void commentVisibilityStrategyRaw(String s)
+  /*-{ this.comment_visibility_strategy = s }-*/;
+
+  public final void diffView(DiffView d) {
+    diffViewRaw(d != null ? d.toString() : null);
+  }
+  private final native void diffViewRaw(String d)
+  /*-{ this.diff_view = d }-*/;
+
+  public final void changeScreen(ChangeScreen s) {
+    changeScreenRaw(s != null ? s.toString() : null);
+  }
+  private final native void changeScreenRaw(String s)
+  /*-{ this.change_screen = s }-*/;
+
+  final void setMyMenus(List<TopMenuItem> myMenus) {
+    initMy();
+    for (TopMenuItem n : myMenus) {
+      addMy(n);
+    }
+  }
+  final native void initMy() /*-{ this.my = []; }-*/;
+  final native void addMy(TopMenuItem m) /*-{ this.my.push(m); }-*/;
+
+  protected Preferences() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java
index 2c29ab2..c689b49 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java
@@ -29,7 +29,7 @@
     if (Gerrit.getConfig().getSshdAddress() != null) {
       link(Util.C.tabSshKeys(), PageLinks.SETTINGS_SSHKEYS);
     }
-    if (!Gerrit.getConfig().isGitBasicAuth()) {
+    if (Gerrit.getConfig().isHttpPasswordSettingsEnabled()) {
       link(Util.C.tabHttpAccess(), PageLinks.SETTINGS_HTTP_PASSWORD);
     }
     link(Util.C.tabWebIdentities(), PageLinks.SETTINGS_WEBIDENT);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/actions/ActionButton.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/actions/ActionButton.java
index 7f13075..ab94a75c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/actions/ActionButton.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/actions/ActionButton.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.client.api.RevisionGlue;
 import com.google.gerrit.client.changes.ChangeInfo;
 import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.projects.BranchInfo;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
@@ -28,13 +29,19 @@
 
 public class ActionButton extends Button implements ClickHandler {
   private final Project.NameKey project;
+  private final BranchInfo branch;
   private final ChangeInfo change;
   private final RevisionInfo revision;
   private final ActionInfo action;
   private ActionContext ctx;
 
   public ActionButton(Project.NameKey project, ActionInfo action) {
-    this(project, null, null, action);
+    this(project, null, null, null, action);
+  }
+
+  public ActionButton(Project.NameKey project, BranchInfo branch,
+      ActionInfo action) {
+    this(project, branch, null, null, action);
   }
 
   public ActionButton(ChangeInfo change, ActionInfo action) {
@@ -43,11 +50,11 @@
 
   public ActionButton(ChangeInfo change, RevisionInfo revision,
       ActionInfo action) {
-    this(null, change, revision, action);
+    this(null, null, change, revision, action);
   }
 
-  private ActionButton(Project.NameKey project, ChangeInfo change,
-      RevisionInfo revision, ActionInfo action) {
+  private ActionButton(Project.NameKey project, BranchInfo branch,
+      ChangeInfo change, RevisionInfo revision, ActionInfo action) {
     super(new SafeHtmlBuilder()
       .openDiv()
       .append(action.label())
@@ -58,6 +65,7 @@
     addClickHandler(this);
 
     this.project = project;
+    this.branch = branch;
     this.change = change;
     this.revision = revision;
     this.action = action;
@@ -75,6 +83,8 @@
       RevisionGlue.onAction(change, revision, action, this);
     } else if (change != null) {
       ChangeGlue.onAction(change, action, this);
+    } else if (branch != null) {
+      ProjectGlue.onAction(project, branch, action, this);
     } else if (project != null) {
       ProjectGlue.onAction(project, action, this);
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/MyGroupsListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/MyGroupsListScreen.java
new file mode 100644
index 0000000..cabe2f5
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/MyGroupsListScreen.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.admin;
+
+import com.google.gerrit.client.groups.GroupList;
+import com.google.gerrit.client.rpc.ScreenLoadCallback;
+import com.google.gerrit.client.ui.AccountScreen;
+
+public class MyGroupsListScreen extends AccountScreen {
+  private GroupTable groups;
+
+  @Override
+  protected void onInitUI() {
+    super.onInitUI();
+    groups = new GroupTable();
+    add(groups);
+  }
+
+  @Override
+  protected void onLoad() {
+    super.onLoad();
+    GroupList.my(new ScreenLoadCallback<GroupList>(this) {
+      @Override
+      protected void preDisplay(GroupList result) {
+        groups.display(result);
+        groups.finishDisplay();
+      }});
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java
index efb2fb4..b3d8564 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java
@@ -124,8 +124,8 @@
 
     PermissionNameRenderer nameRenderer =
         new PermissionNameRenderer(projectAccess.getCapabilities());
-    normalName = new ValueLabel<String>(nameRenderer);
-    deletedName = new ValueLabel<String>(nameRenderer);
+    normalName = new ValueLabel<>(nameRenderer);
+    deletedName = new ValueLabel<>(nameRenderer);
 
     initWidget(uiBinder.createAndBindUi(this));
     groupToAdd.setProject(projectName);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.java
index 07862f4..9e719ea 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.java
@@ -101,7 +101,7 @@
       Permission permission,
       PermissionRange.WithDefaults validRange) {
     this.groupInfo = groupInfo;
-    action = new ValueListBox<PermissionRule.Action>(actionRenderer);
+    action = new ValueListBox<>(actionRenderer);
 
     if (validRange != null && 10 < validRange.getRangeSize()) {
         min = new RangeBox.Box();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
index 26537bd..577bf1d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
@@ -22,6 +22,8 @@
 import com.google.gerrit.client.VoidResult;
 import com.google.gerrit.client.access.AccessMap;
 import com.google.gerrit.client.access.ProjectAccessInfo;
+import com.google.gerrit.client.actions.ActionButton;
+import com.google.gerrit.client.actions.ActionInfo;
 import com.google.gerrit.client.projects.BranchInfo;
 import com.google.gerrit.client.projects.ProjectApi;
 import com.google.gerrit.client.rpc.GerritCallback;
@@ -389,10 +391,18 @@
         table.setText(row, 3, "");
       }
 
+      FlowPanel actionsPanel = new FlowPanel();
       if (c != null) {
-        table.setWidget(row, 4, new Anchor(c.getLinkName(), false,
+        actionsPanel.add(new Anchor(c.getLinkName(), false,
             c.toBranch(new Branch.NameKey(getProjectKey(), k.ref()))));
       }
+      if (k.actions() != null) {
+        k.actions().copyKeysIntoChildren("id");
+        for (ActionInfo a : Natives.asList(k.actions().values())) {
+          actionsPanel.add(new ActionButton(getProjectKey(), k, a));
+        }
+      }
+      table.setWidget(row, 4, actionsPanel);
 
       final FlexCellFormatter fmt = table.getFlexCellFormatter();
       String iconCellStyle = Gerrit.RESOURCES.css().iconCell();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
index 2b50cac..24ee27d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.client.admin;
 
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.StringListPanel;
 import com.google.gerrit.client.access.AccessMap;
 import com.google.gerrit.client.access.ProjectAccessInfo;
 import com.google.gerrit.client.actions.ActionButton;
@@ -34,10 +35,11 @@
 import com.google.gerrit.client.ui.NpIntTextBox;
 import com.google.gerrit.client.ui.OnEditEnabler;
 import com.google.gerrit.client.ui.SmallHeading;
+import com.google.gerrit.extensions.api.projects.ProjectState;
+import com.google.gerrit.extensions.common.InheritableBoolean;
+import com.google.gerrit.extensions.common.SubmitType;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
-import com.google.gerrit.reviewdb.client.Project.SubmitType;
 import com.google.gwt.event.dom.client.ChangeEvent;
 import com.google.gwt.event.dom.client.ChangeHandler;
 import com.google.gwt.event.dom.client.ClickEvent;
@@ -46,7 +48,7 @@
 import com.google.gwt.user.client.ui.CheckBox;
 import com.google.gwt.user.client.ui.FlexTable;
 import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.FocusWidget;
+import com.google.gwt.user.client.ui.HasEnabled;
 import com.google.gwt.user.client.ui.HorizontalPanel;
 import com.google.gwt.user.client.ui.Image;
 import com.google.gwt.user.client.ui.Label;
@@ -58,7 +60,10 @@
 import com.google.gwtexpui.globalkey.client.NpTextArea;
 import com.google.gwtexpui.globalkey.client.NpTextBox;
 
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 
@@ -76,7 +81,7 @@
   private ListBox contentMerge;
   private NpTextBox maxObjectSizeLimit;
   private Label effectiveMaxObjectSizeLimit;
-  private Map<String, Map<String, FocusWidget>> pluginConfigWidgets;
+  private Map<String, Map<String, HasEnabled>> pluginConfigWidgets;
 
   // Section: Contributor Agreements
   private ListBox contributorAgreements;
@@ -149,9 +154,9 @@
   }
 
   private void enableForm(boolean isOwner) {
-    submitType.setEnabled(isOwner);
     state.setEnabled(isOwner);
-    contentMerge.setEnabled(isOwner);
+    submitType.setEnabled(isOwner);
+    setEnabledForUseContentMerge();
     descTxt.setEnabled(isOwner);
     contributorAgreements.setEnabled(isOwner);
     signedOffBy.setEnabled(isOwner);
@@ -159,8 +164,8 @@
     maxObjectSizeLimit.setEnabled(isOwner);
 
     if (pluginConfigWidgets != null) {
-      for (Map<String, FocusWidget> widgetMap : pluginConfigWidgets.values()) {
-        for (FocusWidget widget : widgetMap.values()) {
+      for (Map<String, HasEnabled> widgetMap : pluginConfigWidgets.values()) {
+        for (HasEnabled widget : widgetMap.values()) {
           widget.setEnabled(isOwner);
         }
       }
@@ -185,14 +190,14 @@
     grid.addHeader(new SmallHeading(Util.C.headingProjectOptions()));
 
     state = new ListBox();
-    for (final Project.State stateValue : Project.State.values()) {
+    for (ProjectState stateValue : ProjectState.values()) {
       state.addItem(Util.toLongString(stateValue), stateValue.name());
     }
     saveEnabler.listenTo(state);
     grid.add(Util.C.headingProjectState(), state);
 
     submitType = new ListBox();
-    for (final Project.SubmitType type : Project.SubmitType.values()) {
+    for (final SubmitType type : SubmitType.values()) {
       submitType.addItem(Util.toLongString(type), type.name());
     }
     submitType.addChangeHandler(new ChangeHandler() {
@@ -238,7 +243,7 @@
    * content merge the useContentMerge checkbox gets disabled.
    */
   private void setEnabledForUseContentMerge() {
-    if (SubmitType.FAST_FORWARD_ONLY.equals(Project.SubmitType
+    if (SubmitType.FAST_FORWARD_ONLY.equals(SubmitType
         .valueOf(submitType.getValue(submitType.getSelectedIndex())))) {
       contentMerge.setEnabled(false);
       InheritedBooleanInfo b = InheritedBooleanInfo.create();
@@ -263,7 +268,7 @@
     grid.addHtml(Util.C.useSignedOffBy(), signedOffBy);
   }
 
-  private void setSubmitType(final Project.SubmitType newSubmitType) {
+  private void setSubmitType(final SubmitType newSubmitType) {
     int index = -1;
     if (submitType != null) {
       for (int i = 0; i < submitType.getItemCount(); i++) {
@@ -277,7 +282,7 @@
     }
   }
 
-  private void setState(final Project.State newState) {
+  private void setState(final ProjectState newState) {
     if (state != null) {
       for (int i = 0; i < state.getItemCount(); i++) {
         if (newState.name().equals(state.getValue(i))) {
@@ -357,7 +362,7 @@
     pluginConfigWidgets = new HashMap<>();
 
     for (String pluginName : info.pluginConfig().keySet()) {
-      Map<String, FocusWidget> widgetMap = new HashMap<>();
+      Map<String, HasEnabled> widgetMap = new HashMap<>();
       pluginConfigWidgets.put(pluginName, widgetMap);
       LabeledWidgetsGrid g = new LabeledWidgetsGrid();
       g.addHeader(new SmallHeading(Util.M.pluginProjectOptionsTitle(pluginName)));
@@ -366,7 +371,7 @@
           info.pluginConfig(pluginName);
       pluginConfig.copyKeysIntoChildren("name");
       for (ConfigParameterInfo param : Natives.asList(pluginConfig.values())) {
-        FocusWidget w;
+        HasEnabled w;
         switch (param.type()) {
           case "STRING":
           case "INT":
@@ -380,7 +385,7 @@
             w = renderListBox(g, param);
             break;
           case "ARRAY":
-            w = renderTextArea(g, param);
+            w = renderStringListPanel(g, param);
             break;
           default:
             throw new UnsupportedOperationException("unsupported widget type");
@@ -497,24 +502,21 @@
     return listBox;
   }
 
-  private NpTextArea renderTextArea(LabeledWidgetsGrid g,
+  private StringListPanel renderStringListPanel(LabeledWidgetsGrid g,
       ConfigParameterInfo param) {
-    NpTextArea txtArea = new NpTextArea();
-    txtArea.setVisibleLines(4);
-    txtArea.setCharacterWidth(40);
-    StringBuilder sb = new StringBuilder();
-    for (int i = 0; i < param.values().length(); i++) {
-      String v = param.values().get(i);
-      sb.append(v).append("\n");
+    StringListPanel p =
+        new StringListPanel(null, Arrays.asList(getDisplayName(param)),
+            saveProject, false);
+    List<List<String>> values = new ArrayList<>();
+    for (String v : Natives.asList(param.values())) {
+      values.add(Arrays.asList(v));
     }
-    txtArea.setText(sb.toString());
-    if (param.editable()) {
-      saveEnabler.listenTo(txtArea);
-    } else {
-      txtArea.setEnabled(false);
+    p.display(values);
+    if (!param.editable()) {
+      p.setEnabled(false);
     }
-    addWidget(g, txtArea, param);
-    return txtArea;
+    addWidget(g, p, param);
+    return p;
   }
 
   private void addWidget(LabeledWidgetsGrid g, Widget w, ConfigParameterInfo param) {
@@ -569,8 +571,8 @@
         getBool(contributorAgreements), getBool(contentMerge),
         getBool(signedOffBy), getBool(requireChangeID),
         maxObjectSizeLimit.getText().trim(),
-        Project.SubmitType.valueOf(submitType.getValue(submitType.getSelectedIndex())),
-        Project.State.valueOf(state.getValue(state.getSelectedIndex())),
+        SubmitType.valueOf(submitType.getValue(submitType.getSelectedIndex())),
+        ProjectState.valueOf(state.getValue(state.getSelectedIndex())),
         getPluginConfigValues(), new GerritCallback<ConfigInfo>() {
           @Override
           public void onSuccess(ConfigInfo result) {
@@ -589,11 +591,11 @@
   private Map<String, Map<String, ConfigParameterValue>> getPluginConfigValues() {
     Map<String, Map<String, ConfigParameterValue>> pluginConfigValues =
         new HashMap<>(pluginConfigWidgets.size());
-    for (Entry<String, Map<String, FocusWidget>> e : pluginConfigWidgets.entrySet()) {
+    for (Entry<String, Map<String, HasEnabled>> e : pluginConfigWidgets.entrySet()) {
       Map<String, ConfigParameterValue> values = new HashMap<>(e.getValue().size());
       pluginConfigValues.put(e.getKey(), values);
-      for (Entry<String, FocusWidget> e2 : e.getValue().entrySet()) {
-        FocusWidget widget = e2.getValue();
+      for (Entry<String, HasEnabled> e2 : e.getValue().entrySet()) {
+        HasEnabled widget = e2.getValue();
         if (widget instanceof TextBox) {
           values.put(e2.getKey(), ConfigParameterValue.create()
               .value(((TextBox) widget).getValue().trim()));
@@ -608,10 +610,11 @@
               ? listBox.getValue(listBox.getSelectedIndex()) : null;
           values.put(e2.getKey(), ConfigParameterValue.create()
               .value(value));
-        } else if (widget instanceof NpTextArea) {
-          String text = ((NpTextArea) widget).getText().trim();
-          values.put(e2.getKey(), ConfigParameterValue.create()
-              .values(text.split("\n")));
+        } else if (widget instanceof StringListPanel) {
+          values.put(e2.getKey(),
+              ConfigParameterValue.create().values(
+                  ((StringListPanel) widget).getValues(0)
+                      .toArray(new String[] {})));
         } else {
           throw new UnsupportedOperationException("unsupported widget type");
         }
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 3a6cb7d..3bd05e0 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
@@ -19,9 +19,11 @@
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.GitwebLink;
+import com.google.gerrit.client.WebLinkInfo;
 import com.google.gerrit.client.projects.ProjectInfo;
 import com.google.gerrit.client.projects.ProjectMap;
 import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.FilteredUserInterface;
 import com.google.gerrit.client.ui.HighlightingInlineHyperlink;
 import com.google.gerrit.client.ui.Hyperlink;
@@ -43,6 +45,8 @@
 import com.google.gwt.user.client.ui.Label;
 import com.google.gwtexpui.globalkey.client.NpTextBox;
 
+import java.util.List;
+
 public class ProjectListScreen extends Screen implements FilteredUserInterface {
   private Hyperlink prev;
   private Hyperlink next;
@@ -167,13 +171,11 @@
       @Override
       protected void initColumnHeaders() {
         super.initColumnHeaders();
-        if (Gerrit.getGitwebLink() != null) {
-          table.setText(0, ProjectsTable.C_REPO_BROWSER,
-              Util.C.projectRepoBrowser());
-          table.getFlexCellFormatter().
-            addStyleName(0, ProjectsTable.C_REPO_BROWSER,
-                Gerrit.RESOURCES.css().dataHeader());
-        }
+        table.setText(0, ProjectsTable.C_REPO_BROWSER,
+            Util.C.projectRepoBrowser());
+        table.getFlexCellFormatter().
+          addStyleName(0, ProjectsTable.C_REPO_BROWSER,
+              Gerrit.RESOURCES.css().dataHeader());
       }
 
       @Override
@@ -188,11 +190,8 @@
       @Override
       protected void insert(int row, ProjectInfo k) {
         super.insert(row, k);
-        if (Gerrit.getGitwebLink() != null) {
-          table.getFlexCellFormatter().
-            addStyleName(row, ProjectsTable.C_REPO_BROWSER,
-                Gerrit.RESOURCES.css().dataCell());
-        }
+        table.getFlexCellFormatter().addStyleName(row,
+            ProjectsTable.C_REPO_BROWSER, Gerrit.RESOURCES.css().dataCell());
       }
 
       @Override
@@ -219,15 +218,33 @@
         fp.add(new HighlightingInlineHyperlink(k.name(), link(k), subname));
         table.setWidget(row, ProjectsTable.C_NAME, fp);
         table.setText(row, ProjectsTable.C_DESCRIPTION, k.description());
-        GitwebLink l = Gerrit.getGitwebLink();
-        if (l != null) {
-          table.setWidget(row, ProjectsTable.C_REPO_BROWSER,
-              new Anchor(l.getLinkName(), false, l.toProject(k
-              .name_key())));
-        }
+        addWebLinks(row, k);
 
         setRowItem(row, k);
       }
+
+      private void addWebLinks(int row, ProjectInfo k) {
+        GitwebLink gitWebLink = Gerrit.getGitwebLink();
+        List<WebLinkInfo> webLinks = Natives.asList(k.web_links());
+        if (gitWebLink != null || (webLinks != null && !webLinks.isEmpty())) {
+          FlowPanel p = new FlowPanel();
+          table.setWidget(row, ProjectsTable.C_REPO_BROWSER, p);
+
+          if (gitWebLink != null) {
+            Anchor a = new Anchor();
+            a.setText(gitWebLink.getLinkName());
+            a.setHref(gitWebLink.toProject(k.name_key()));
+            p.add(a);
+          }
+
+          for (WebLinkInfo weblink : webLinks) {
+            Anchor a = new Anchor();
+            a.setText("(" + weblink.name() + ")");
+            a.setHref(weblink.url());
+            p.add(a);
+          }
+        }
+      }
     };
     projects.setSavePointerId(PageLinks.ADMIN_PROJECTS);
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/RangeBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/RangeBox.java
index bf04b07..088899e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/RangeBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/RangeBox.java
@@ -49,7 +49,7 @@
     final ValueListBox<Integer> list;
 
     List() {
-      list = new ValueListBox<Integer>(rangeRenderer);
+      list = new ValueListBox<>(rangeRenderer);
       initWidget(list);
     }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/Util.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/Util.java
index 1696c43..5b8fe38 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/Util.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/Util.java
@@ -15,7 +15,8 @@
 package com.google.gerrit.client.admin;
 
 import com.google.gerrit.common.data.ProjectAdminService;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.extensions.api.projects.ProjectState;
+import com.google.gerrit.extensions.common.SubmitType;
 import com.google.gwt.core.client.GWT;
 import com.google.gwtjsonrpc.client.JsonUtil;
 
@@ -31,7 +32,7 @@
     AdminResources.I.css().ensureInjected();
   }
 
-  public static String toLongString(final Project.SubmitType type) {
+  public static String toLongString(final SubmitType type) {
     if (type == null) {
       return "";
     }
@@ -51,7 +52,7 @@
     }
   }
 
-  public static String toLongString(final Project.State type) {
+  public static String toLongString(final ProjectState type) {
     if (type == null) {
       return "";
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ActionContext.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ActionContext.java
index 453a3f2..7490d82 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ActionContext.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ActionContext.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.client.actions.ActionInfo;
 import com.google.gerrit.client.changes.ChangeInfo;
 import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.projects.BranchInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.NativeString;
 import com.google.gerrit.client.rpc.RestApi;
@@ -137,6 +138,7 @@
   final native void set(ActionInfo a) /*-{ this.action=a; }-*/;
   final native void set(ChangeInfo c) /*-{ this.change=c; }-*/;
   final native void set(Project.NameKey p) /*-{ this.project=p; }-*/;
+  final native void set(BranchInfo b) /*-{ this.branch=b }-*/;
   final native void set(RevisionInfo r) /*-{ this.revision=r; }-*/;
 
   final native void button(ActionButton b) /*-{ this._b=b; }-*/;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java
index 976dc0c..8da896e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java
@@ -43,6 +43,7 @@
       change_actions: {},
       revision_actions: {},
       project_actions: {},
+      branch_actions: {},
 
       getPluginName: @com.google.gerrit.client.api.ApiGlue::getPluginName(),
       injectCss: @com.google.gwt.dom.client.StyleInjector::inject(Ljava/lang/String;),
@@ -72,6 +73,7 @@
         if ('change' == t) this.change_actions[i]=c;
         else if ('revision' == t) this.revision_actions[i]=c;
         else if ('project' == t) this.project_actions[i]=c;
+        else if ('branch' == t) this.branch_actions[i]=c;
         else if ('screen' == t) _screen(p,t,c);
       },
       screen: function(r,c){this._screen(this.getPluginName(),r,c)},
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/DefaultActions.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/DefaultActions.java
index 53a3784..0e4048d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/DefaultActions.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/DefaultActions.java
@@ -21,50 +21,54 @@
 import com.google.gerrit.client.rpc.NativeString;
 import com.google.gerrit.client.rpc.RestApi;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.Window.Location;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 
 class DefaultActions {
   static void invoke(ChangeInfo change, ActionInfo action, RestApi api) {
-    final Change.Id id = change.legacy_id();
-    AsyncCallback<JavaScriptObject> cb = new GerritCallback<JavaScriptObject>() {
-      @Override
-      public void onSuccess(JavaScriptObject msg) {
-        if (NativeString.is(msg)) {
-          NativeString str = (NativeString) msg;
-          if (!str.asString().isEmpty()) {
-            Window.alert(str.asString());
-          }
-        }
-        Gerrit.display(PageLinks.toChange(id));
-      }
-    };
-    invoke(action, api, cb);
+    invoke(action, api, callback(PageLinks.toChange(change.legacy_id())));
   }
 
-  static void invokeProjectAction(final Project.NameKey project,
-      ActionInfo action, RestApi api) {
-    AsyncCallback<JavaScriptObject> cb = new GerritCallback<JavaScriptObject>() {
+  static void invoke(Project.NameKey project, ActionInfo action, RestApi api) {
+    invoke(action, api, callback(PageLinks.toProject(project)));
+  }
+
+  private static AsyncCallback<JavaScriptObject> callback(final String target) {
+    return new GerritCallback<JavaScriptObject>() {
       @Override
-      public void onSuccess(JavaScriptObject msg) {
-        if (NativeString.is(msg)) {
-          NativeString str = (NativeString) msg;
-          if (!str.asString().isEmpty()) {
-            Window.alert(str.asString());
-          }
+      public void onSuccess(JavaScriptObject in) {
+        UiResult result = asUiResult(in);
+        if (result.alert() != null) {
+          Window.alert(result.alert());
         }
-        Gerrit.display(PageLinks.toProject(project));
+
+        if (result.redirectUrl() != null && result.openWindow()) {
+          Window.open(result.redirectUrl(), "_blank", null);
+        } else if (result.redirectUrl() != null) {
+          Location.assign(result.redirectUrl());
+        } else {
+          Gerrit.display(target);
+        }
+      }
+
+      private UiResult asUiResult(JavaScriptObject in) {
+        if (NativeString.is(in)) {
+          String str = ((NativeString) in).asString();
+          return str.isEmpty() ? UiResult.none() : UiResult.alert(str);
+        }
+        return in.cast();
       }
     };
-    invoke(action, api, cb);
   }
 
   private static void invoke(ActionInfo action, RestApi api,
       AsyncCallback<JavaScriptObject> cb) {
-    if ("PUT".equalsIgnoreCase(action.method())) {
+    if ("GET".equalsIgnoreCase(action.method())) {
+      api.get(cb);
+    } else if ("PUT".equalsIgnoreCase(action.method())) {
       api.put(JavaScriptObject.createObject(), cb);
     } else if ("DELETE".equalsIgnoreCase(action.method())) {
       api.delete(cb);
@@ -75,4 +79,16 @@
 
   private DefaultActions() {
   }
+
+  private static class UiResult extends JavaScriptObject {
+    static native UiResult alert(String m) /*-{ return {'alert':m} }-*/;
+    static native UiResult none() /*-{ return {} }-*/;
+
+    final native String alert() /*-{ return this.alert }-*/;
+    final native String redirectUrl() /*-{ return this.url }-*/;
+    final native boolean openWindow() /*-{ return this.open_window || false }-*/;
+
+    protected UiResult() {
+    }
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginLoader.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginLoader.java
index 8b5c93e..f0fb436 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginLoader.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginLoader.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.client.ErrorDialog;
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.VoidResult;
 import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gwt.core.client.Callback;
@@ -23,6 +24,7 @@
 import com.google.gwt.core.client.ScriptInjector;
 import com.google.gwt.user.client.Timer;
 import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwt.user.client.ui.DialogBox;
 import com.google.gwtexpui.progress.client.ProgressBar;
 
@@ -33,11 +35,12 @@
   private static final int MAX_LOAD_TIME_MILLIS = 5000;
   private static PluginLoader self;
 
-  public static void load(List<String> plugins, final String token) {
+  public static void load(List<String> plugins,
+      AsyncCallback<VoidResult> callback) {
     if (plugins == null || plugins.isEmpty()) {
-      Gerrit.display(token);
+      callback.onSuccess(VoidResult.create());
     } else {
-      self = new PluginLoader(token);
+      self = new PluginLoader(callback);
       self.load(plugins);
       self.startTimers();
       self.center();
@@ -48,16 +51,16 @@
     self.loadedOne();
   }
 
-  private final String token;
+  private final AsyncCallback<VoidResult> callback;
   private ProgressBar progress;
   private Timer show;
   private Timer update;
   private Timer timeout;
   private boolean visible;
 
-  private PluginLoader(String tokenToDisplay) {
+  private PluginLoader(AsyncCallback<VoidResult> cb) {
     super(/* auto hide */false, /* modal */true);
-    token = tokenToDisplay;
+    callback = cb;
     progress = new ProgressBar(Gerrit.C.loadingPlugins());
 
     setStyleName(Gerrit.RESOURCES.css().errorDialog());
@@ -139,7 +142,7 @@
       }
     }
 
-    Gerrit.display(token);
+    callback.onSuccess(VoidResult.create());
   }
 
   private boolean hadFailures() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginName.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginName.java
index 2c1f2c7..bd90cd3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginName.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginName.java
@@ -17,7 +17,6 @@
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.core.client.JavaScriptException;
 import com.google.gwt.core.client.JsArrayString;
-import com.google.gwt.core.client.impl.StackTraceCreator;
 
 /**
  * Determines the name a plugin has been installed under.
@@ -68,7 +67,9 @@
   }
 
   private static StackTraceElement[] getTrace(JavaScriptException err) {
-    StackTraceCreator.fillInStackTrace(err);
+    if (err.getStackTrace().length == 0) {
+      err.fillInStackTrace();
+    }
     return err.getStackTrace();
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ProjectGlue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ProjectGlue.java
index bce691c..3f2e794 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ProjectGlue.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ProjectGlue.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.client.actions.ActionButton;
 import com.google.gerrit.client.actions.ActionInfo;
+import com.google.gerrit.client.projects.BranchInfo;
 import com.google.gerrit.client.projects.ProjectApi;
 import com.google.gerrit.client.rpc.RestApi;
 import com.google.gerrit.reviewdb.client.Project;
@@ -24,10 +25,31 @@
 public class ProjectGlue {
   public static void onAction(
       Project.NameKey project,
+      BranchInfo branch,
+      ActionInfo action,
+      ActionButton button) {
+    RestApi api = ProjectApi.project(project)
+        .view("branches").id(branch.ref())
+        .view(action.id());
+    JavaScriptObject f = branchAction(action.id());
+    if (f != null) {
+      ActionContext c = ActionContext.create(api);
+      c.set(action);
+      c.set(project);
+      c.set(branch);
+      c.button(button);
+      ApiGlue.invoke(f, c);
+    } else {
+      DefaultActions.invoke(project, action, api);
+    }
+  }
+
+  public static void onAction(
+      Project.NameKey project,
       ActionInfo action,
       ActionButton button) {
     RestApi api = ProjectApi.project(project).view(action.id());
-    JavaScriptObject f = get(action.id());
+    JavaScriptObject f = projectAction(action.id());
     if (f != null) {
       ActionContext c = ActionContext.create(api);
       c.set(action);
@@ -35,14 +57,18 @@
       c.button(button);
       ApiGlue.invoke(f, c);
     } else {
-      DefaultActions.invokeProjectAction(project, action, api);
+      DefaultActions.invoke(project, action, api);
     }
   }
 
-  private static final native JavaScriptObject get(String id) /*-{
+  private static final native JavaScriptObject projectAction(String id) /*-{
     return $wnd.Gerrit.project_actions[id];
   }-*/;
 
+  private static final native JavaScriptObject branchAction(String id) /*-{
+    return $wnd.Gerrit.branch_actions[id];
+  }-*/;
+
   private ProjectGlue() {
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/arrowDown.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/arrowDown.png
new file mode 100644
index 0000000..ba67de7
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/arrowDown.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/arrowUp.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/arrowUp.png
new file mode 100644
index 0000000..5674d6c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/arrowUp.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdConstants.java
index 5d756a1..3bfb297 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdConstants.java
@@ -18,5 +18,6 @@
 
 public interface OpenIdConstants extends Constants {
   String nameGoogle();
+  String nameLaunchpad();
   String nameYahoo();
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdConstants.properties
index 1a8acc1..08ddf38 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdConstants.properties
@@ -1,2 +1,3 @@
 nameGoogle = Google Account
+nameLaunchpad = Launchpad ID
 nameYahoo = Yahoo! ID
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java
index 92444d0..906efca 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java
@@ -30,6 +30,7 @@
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 
 import java.util.TreeSet;
 
@@ -113,7 +114,12 @@
     if (hasUser) {
       canSubmit = actions.containsKey("submit");
       if (canSubmit) {
-        submit.setTitle(actions.get("submit").title());
+        ActionInfo action = actions.get("submit");
+        submit.setTitle(action.title());
+        submit.setHTML(new SafeHtmlBuilder()
+            .openDiv()
+            .append(action.label())
+            .closeDiv());
       }
       a2b(actions, "/", deleteRevision);
       a2b(actions, "cherrypick", cherrypick);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.ui.xml
index 72e6a61..cb2b37f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.ui.xml
@@ -100,8 +100,6 @@
       <div><ui:msg>Restore</ui:msg></div>
     </g:Button>
 
-    <g:Button ui:field='submit' styleName='{style.submit}' visible='false'>
-      <div><ui:msg>Submit</ui:msg></div>
-    </g:Button>
+    <g:Button ui:field='submit' styleName='{style.submit}' visible='false'/>
   </g:FlowPanel>
 </ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Constants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.java
similarity index 93%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Constants.java
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.java
index 4d457ad..11a1824 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Constants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.java
@@ -14,7 +14,9 @@
 
 package com.google.gerrit.client.change;
 
-interface Constants extends com.google.gwt.i18n.client.Constants {
+import com.google.gwt.i18n.client.Constants;
+
+interface ChangeConstants extends Constants {
   String previousChange();
   String nextChange();
   String openChange();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Constants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.properties
similarity index 100%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Constants.properties
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.properties
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Messages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.java
similarity index 91%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Messages.java
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.java
index 1d2ea4b..595a6c9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Messages.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.java
@@ -14,7 +14,9 @@
 
 package com.google.gerrit.client.change;
 
-public interface Messages extends com.google.gwt.i18n.client.Messages {
+import com.google.gwt.i18n.client.Messages;
+
+public interface ChangeMessages extends Messages {
   String patchSets(int currentlyViewedPatchSet, int currentPatchSet);
   String changeWithNoRevisions(int changeId);
   String relatedChanges(int count);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Messages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.properties
similarity index 100%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Messages.properties
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.properties
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.java
index e0c1105..69cd3fc 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.java
@@ -14,9 +14,11 @@
 
 package com.google.gerrit.client.change;
 
+import com.google.gerrit.client.AvatarImage;
 import com.google.gerrit.client.ErrorDialog;
 import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.account.AccountInfo.AvatarInfo;
 import com.google.gerrit.client.actions.ActionInfo;
 import com.google.gerrit.client.api.ChangeGlue;
 import com.google.gerrit.client.changes.ChangeApi;
@@ -49,11 +51,10 @@
 import com.google.gerrit.client.ui.UserActivityMonitor;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.extensions.common.ListChangesOption;
+import com.google.gerrit.extensions.common.SubmitType;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Project.SubmitType;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.core.client.JsArrayString;
@@ -77,6 +78,7 @@
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.HTMLPanel;
 import com.google.gwt.user.client.ui.Image;
 import com.google.gwt.user.client.ui.ListBox;
@@ -140,6 +142,7 @@
 
   @UiField Element ccText;
   @UiField Reviewers reviewers;
+  @UiField FlowPanel ownerPanel;
   @UiField InlineHyperlink ownerLink;
   @UiField Element statusText;
   @UiField Image projectSettings;
@@ -206,7 +209,8 @@
     RestApi call = ChangeApi.detail(changeId.get());
     ChangeList.addOptions(call, EnumSet.of(
       ListChangesOption.CURRENT_ACTIONS,
-      ListChangesOption.ALL_REVISIONS));
+      ListChangesOption.ALL_REVISIONS,
+      ListChangesOption.WEB_LINKS));
     if (!fg) {
       call.background();
     }
@@ -800,7 +804,7 @@
     if (Gerrit.isSignedIn()) {
       initEditMessageAction(info, revision);
       replyAction = new ReplyAction(info, revision,
-          style, commentLinkProcessor, reply);
+          style, commentLinkProcessor, reply, quickApprove);
       if (topic.canEdit()) {
         keysAction.add(new KeyCommand(0, 't', Util.C.keyEditTopic()) {
           @Override
@@ -835,6 +839,9 @@
         ? info.owner().name()
         : Gerrit.getConfig().getAnonymousCowardName();
 
+    if (info.owner().avatar(AvatarInfo.DEFAULT_SIZE) != null) {
+      ownerPanel.insert(new AvatarImage(info.owner()), 0);
+    }
     ownerLink.setText(name);
     ownerLink.setTitle(info.owner().email() != null
         ? info.owner().email()
@@ -849,7 +856,7 @@
 
   private void renderSubmitType(String action) {
     try {
-      SubmitType type = Project.SubmitType.valueOf(action);
+      SubmitType type = SubmitType.valueOf(action);
       submitActionText.setInnerText(
           com.google.gerrit.client.admin.Util.toLongString(type));
     } catch (IllegalArgumentException e) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.ui.xml
index e0e6053..2b09ef9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.ui.xml
@@ -302,6 +302,13 @@
     .replyBox {
       background-color: trimColor;
     }
+
+    .ownerPanel img {
+      margin: 0 2px 0 0;
+      width: 16px;
+      height: 16px !important;
+      vertical-align: bottom;
+    }
   </ui:style>
 
   <g:HTMLPanel styleName='{style.cs2}'>
@@ -368,7 +375,11 @@
           <table id='change_infoTable'>
             <tr>
               <th><ui:msg>Owner</ui:msg></th>
-              <td><x:InlineHyperlink ui:field='ownerLink'/></td>
+              <td>
+                <g:FlowPanel ui:field='ownerPanel' styleName='{style.ownerPanel}'>
+                  <x:InlineHyperlink ui:field='ownerLink'/>
+                </g:FlowPanel>
+              </td>
             </tr>
             <tr>
               <th><ui:msg>Reviewers</ui:msg></th>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CherryPickAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CherryPickAction.java
index 10d7297..9f64fa0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CherryPickAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CherryPickAction.java
@@ -23,10 +23,13 @@
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.event.logical.shared.CloseEvent;
 import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.PopupPanel;
+
 
 class CherryPickAction {
-  static void call(Button b, final ChangeInfo info, final String revision,
+  static void call(final Button b, final ChangeInfo info, final String revision,
       String project, final String commitMessage) {
     // TODO Replace CherryPickDialog with a nicer looking display.
     b.setEnabled(false);
@@ -62,6 +65,12 @@
               }
             });
       }
+
+      @Override
+      public void onClose(CloseEvent<PopupPanel> event) {
+        super.onClose(event);
+        b.setEnabled(true);
+      }
     }.center();
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java
index 443b31b..4572cf8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java
@@ -14,9 +14,12 @@
 
 package com.google.gerrit.client.change;
 
+import com.google.gerrit.client.AvatarImage;
 import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.GitwebLink;
+import com.google.gerrit.client.WebLinkInfo;
+import com.google.gerrit.client.account.AccountInfo;
 import com.google.gerrit.client.changes.ChangeInfo;
 import com.google.gerrit.client.changes.ChangeInfo.CommitInfo;
 import com.google.gerrit.client.changes.ChangeInfo.GitPerson;
@@ -25,16 +28,17 @@
 import com.google.gerrit.client.ui.CommentLinkProcessor;
 import com.google.gerrit.client.ui.InlineHyperlink;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.dom.client.AnchorElement;
 import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.TableCellElement;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.resources.client.CssResource;
 import com.google.gwt.uibinder.client.UiBinder;
 import com.google.gwt.uibinder.client.UiField;
 import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.Composite;
@@ -43,7 +47,6 @@
 import com.google.gwt.user.client.ui.HTMLPanel;
 import com.google.gwt.user.client.ui.Image;
 import com.google.gwt.user.client.ui.ScrollPanel;
-import com.google.gwt.user.client.ui.UIObject;
 import com.google.gwtexpui.clippy.client.CopyableLabel;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 
@@ -59,9 +62,11 @@
   }
 
   @UiField Style style;
+  @UiField FlowPanel authorPanel;
+  @UiField FlowPanel committerPanel;
   @UiField Image mergeCommit;
   @UiField CopyableLabel commitName;
-  @UiField AnchorElement browserLink;
+  @UiField TableCellElement webLinkCell;
   @UiField Element parents;
   @UiField FlowPanel parentCommits;
   @UiField FlowPanel parentWebLinks;
@@ -105,20 +110,14 @@
     commitName.setText(revision);
     idText.setText("Change-Id: " + change.change_id());
     idText.setPreviewText(change.change_id());
-    formatLink(commit.author(), authorNameEmail,
-        authorDate, change.status());
-    formatLink(commit.committer(), committerNameEmail,
-        committerDate, change.status());
+
+    formatLink(commit.author(), authorPanel, authorNameEmail, authorDate,
+        change);
+    formatLink(commit.committer(), committerPanel, committerNameEmail,
+        committerDate, change);
     text.setHTML(commentLinkProcessor.apply(
         new SafeHtmlBuilder().append(commit.message()).linkify()));
-
-    GitwebLink gw = Gerrit.getGitwebLink();
-    if (gw != null && gw.canLink(revInfo)) {
-      browserLink.setInnerText(gw.getLinkName());
-      browserLink.setHref(gw.toRevision(change.project(), revision));
-    } else {
-      UIObject.setVisible(browserLink, false);
-    }
+    setWebLinks(change, revision, revInfo);
 
     if (revInfo.commit().parents().length() > 1) {
       mergeCommit.setVisible(true);
@@ -126,6 +125,28 @@
     setParents(change.project(), revInfo.commit().parents());
   }
 
+  private void setWebLinks(ChangeInfo change, String revision,
+      RevisionInfo revInfo) {
+    GitwebLink gw = Gerrit.getGitwebLink();
+    if (gw != null && gw.canLink(revInfo)) {
+      addWebLink(gw.toRevision(change.project(), revision), gw.getLinkName());
+    }
+
+    JsArray<WebLinkInfo> links = revInfo.web_links();
+    if (links != null) {
+      for (WebLinkInfo link : Natives.asList(links)) {
+        addWebLink(link.url(), parenthesize(link.name()));
+      }
+    }
+  }
+
+  private void addWebLink(String href, String name) {
+    AnchorElement a = DOM.createAnchor().cast();
+    a.setHref(href);
+    a.setInnerText(name);
+    webLinkCell.appendChild(a);
+  }
+
   private void setParents(String project, JsArray<CommitInfo> commits) {
     setVisible(parents, true);
     for (CommitInfo c : Natives.asList(commits)) {
@@ -143,14 +164,36 @@
     }
   }
 
-  private static void formatLink(GitPerson person, InlineHyperlink name,
-      Element date, Status status) {
+  private static void formatLink(GitPerson person, FlowPanel p,
+      InlineHyperlink name, Element date, ChangeInfo change) {
+    // only try to fetch the avatar image for author and committer if an avatar
+    // plugin is installed, if the change owner has no avatar info assume that
+    // no avatar plugin is installed
+    if (change.owner().has_avatar_info()) {
+      AvatarImage avatar;
+      if (change.owner().email().equals(person.email())) {
+        avatar = new AvatarImage(change.owner());
+      } else {
+        avatar = new AvatarImage(
+            AccountInfo.create(0, person.name(), person.email(), null));
+      }
+      p.insert(avatar, 0);
+    }
+
     name.setText(renderName(person));
     name.setTargetHistoryToken(PageLinks
-        .toAccountQuery(owner(person), status));
+        .toAccountQuery(owner(person), change.status()));
     date.setInnerText(FormatUtil.mediumFormat(person.date()));
   }
 
+  private static String parenthesize(String str) {
+    return new StringBuilder()
+        .append("(")
+        .append(str)
+        .append(")")
+        .toString();
+  }
+
   private static String renderName(GitPerson person) {
     return person.name() + " <" + person.email() + ">";
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.ui.xml
index b0897cb..7d0bb00 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.ui.xml
@@ -81,6 +81,9 @@
       right: -16px;
     }
     <!-- To make room for the copyableLabel from the adjacent column -->
+    .webLinkCell a:first-child {
+      margin-left:16px;
+    }
     .parentWebLink {
       margin-left:16px;
       display: block;
@@ -90,6 +93,13 @@
       margin-right: 3px;
       float: left;
     }
+
+    .userPanel img {
+      margin: 0 2px 0 0;
+      width: 16px;
+      height: 16px !important;
+      vertical-align: bottom;
+    }
   </ui:style>
   <g:HTMLPanel>
     <g:ScrollPanel styleName='{style.scroll}' ui:field='scroll'>
@@ -106,19 +116,25 @@
     <table class='{style.header}'>
       <tr>
         <th><ui:msg>Author</ui:msg></th>
-        <td><x:InlineHyperlink ui:field='authorNameEmail'
+        <td>
+          <g:FlowPanel ui:field='authorPanel' styleName='{style.userPanel}'>
+            <x:InlineHyperlink ui:field='authorNameEmail'
               title='Search for changes by this user'>
               <ui:attribute name='title'/>
             </x:InlineHyperlink>
+          </g:FlowPanel>
         </td>
         <td ui:field='authorDate' class='{style.date}' colspan="2"/>
       </tr>
       <tr>
         <th><ui:msg>Committer</ui:msg></th>
-        <td><x:InlineHyperlink ui:field='committerNameEmail'
+        <td>
+          <g:FlowPanel ui:field='committerPanel' styleName='{style.userPanel}'>
+            <x:InlineHyperlink ui:field='committerNameEmail'
               title='Search for changes by this user'>
               <ui:attribute name='title'/>
             </x:InlineHyperlink>
+          </g:FlowPanel>
         </td>
         <td ui:field='committerDate' class='{style.date}' colspan="2"/>
       </tr>
@@ -136,7 +152,7 @@
           </g:Image>
         </th>
         <td><clippy:CopyableLabel styleName='{style.clippy}' ui:field='commitName'/></td>
-        <td><a style="margin-left:16px;" ui:field='browserLink' href=""/></td>
+        <td ui:field='webLinkCell' class='{style.webLinkCell}'></td>
       </tr>
       <tr ui:field='parents' style='display: none'>
         <th><ui:msg>Parent(s)</ui:msg></th>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadBox.java
index 929bf90..09335c1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadBox.java
@@ -46,7 +46,6 @@
 import java.util.List;
 
 class DownloadBox extends VerticalPanel {
-  private final static String ARCHIVE[] = {"tar", "tbz2", "tgz", "txz"};
   private final ChangeInfo change;
   private final String revision;
   private final PatchSet.Id psId;
@@ -149,8 +148,13 @@
   }
 
   private void insertArchive() {
-    List<Anchor> formats = new ArrayList<>(ARCHIVE.length);
-    for (String f : ARCHIVE) {
+    List<String> activated = Gerrit.getConfig().getArchiveFormats();
+    if (activated.isEmpty()) {
+      return;
+    }
+
+    List<Anchor> anchors = new ArrayList<>(activated.size());
+    for (String f : activated) {
       Anchor archive = new Anchor(f);
       archive.setHref(new RestApi("/changes/")
           .id(psId.getParentKey().get())
@@ -159,11 +163,11 @@
           .view("archive")
           .addParameter("format", f)
           .url());
-      formats.add(archive);
+      anchors.add(archive);
     }
 
     HorizontalPanel p = new HorizontalPanel();
-    Iterator<Anchor> it = formats.iterator();
+    Iterator<Anchor> it = anchors.iterator();
     while (it.hasNext()) {
       Anchor a = it.next();
       p.add(a);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageBox.java
index 700638a..0fe91ca 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageBox.java
@@ -55,6 +55,7 @@
     this.revision = revision;
     this.originalMessage = msg.trim();
     initWidget(uiBinder.createAndBindUi(this));
+    message.getElement().setAttribute("wrap", "off");
     message.setText("");
     new TextBoxChangeListener(message) {
       public void onTextChanged(String newText) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.ui.xml
index a3a706d..fe0c97b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.ui.xml
@@ -56,6 +56,7 @@
     .contents blockquote {
       -webkit-margin-before: 0;
       -webkit-margin-after: 0.3em;
+       white-space: pre-wrap;
     }
 
     .name {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/QuickApprove.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/QuickApprove.java
index 98d495a..6e86730 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/QuickApprove.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/QuickApprove.java
@@ -109,16 +109,17 @@
 
   @Override
   public void onClick(ClickEvent event) {
-    if (replyAction != null) {
-      input.message(replyAction.getMessage());
+    if (replyAction != null && replyAction.isVisible()) {
+      replyAction.quickApprove(input);
+    } else {
+      ChangeApi.revision(changeId.get(), revision)
+        .view("review")
+        .post(input, new GerritCallback<ReviewInput>() {
+          @Override
+          public void onSuccess(ReviewInput result) {
+            Gerrit.display(PageLinks.toChange(changeId));
+          }
+        });
     }
-    ChangeApi.revision(changeId.get(), revision)
-      .view("review")
-      .post(input, new GerritCallback<ReviewInput>() {
-        @Override
-        public void onSuccess(ReviewInput result) {
-          Gerrit.display(PageLinks.toChange(changeId));
-        }
-      });
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyAction.java
index aaa301c..b234fe3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyAction.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.client.changes.ChangeInfo;
 import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
 import com.google.gerrit.client.changes.ChangeInfo.MessageInfo;
+import com.google.gerrit.client.changes.ReviewInput;
 import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.client.ui.CommentLinkProcessor;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -35,6 +36,7 @@
   private final ChangeScreen2.Style style;
   private final CommentLinkProcessor clp;
   private final Widget replyButton;
+  private final Widget quickApproveButton;
 
   private NativeMap<LabelInfo> allLabels;
   private NativeMap<JsArrayString> permittedLabels;
@@ -47,7 +49,8 @@
       String revision,
       ChangeScreen2.Style style,
       CommentLinkProcessor clp,
-      Widget replyButton) {
+      Widget replyButton,
+      Widget quickApproveButton) {
     this.psId = new PatchSet.Id(
         info.legacy_id(),
         info.revisions().get(revision)._number());
@@ -55,6 +58,7 @@
     this.style = style;
     this.clp = clp;
     this.replyButton = replyButton;
+    this.quickApproveButton = quickApproveButton;
 
     boolean current = revision.equals(info.current_revision());
     allLabels = info.all_labels();
@@ -63,8 +67,12 @@
         : NativeMap.<JsArrayString> create();
   }
 
-  String getMessage() {
-    return replyBox != null ? replyBox.getMessage() : null;
+  boolean isVisible() {
+    return popup != null;
+  }
+
+  void quickApprove(ReviewInput input) {
+    replyBox.quickApprove(input);
   }
 
   void hide() {
@@ -96,9 +104,8 @@
 
     final PluginSafePopupPanel p = new PluginSafePopupPanel(true, false);
     p.setStyleName(style.replyBox());
-    p.setGlassEnabled(true);
-    p.setGlassStyleName("");
     p.addAutoHidePartner(replyButton.getElement());
+    p.addAutoHidePartner(quickApproveButton.getElement());
     p.addCloseHandler(new CloseHandler<PopupPanel>() {
       @Override
       public void onClose(CloseEvent<PopupPanel> event) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java
index 1c30928..ddd650e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java
@@ -189,7 +189,16 @@
 
   @UiHandler("post")
   void onPost(ClickEvent e) {
-    in.message(getMessage());
+    postReview();
+  }
+
+  void quickApprove(ReviewInput quickApproveInput) {
+    in.mergeLabels(quickApproveInput);
+    postReview();
+  }
+
+  private void postReview() {
+    in.message(message.getText().trim());
     in.prePost();
     ChangeApi.revision(psId.getParentKey().get(), revision)
       .view("review")
@@ -204,12 +213,9 @@
     hide();
   }
 
-  String getMessage() {
-    return message.getText().trim();
-  }
-
   @UiHandler("cancel")
   void onCancel(ClickEvent e) {
+    message.setText("");
     hide();
   }
 
@@ -306,10 +312,23 @@
     }
   }
 
+  private Short normalizeDefaultValue(Short defaultValue, Set<Short> permittedValues) {
+    Short pmin = Collections.min(permittedValues);
+    Short pmax = Collections.max(permittedValues);
+    Short dv = defaultValue;
+    if (dv > pmax) {
+      dv = pmax;
+    } else if (dv < pmin) {
+      dv = pmin;
+    }
+    return dv;
+  }
+
   private void renderRadio(int row,
       List<Short> columns,
       LabelAndValues lv) {
     String id = lv.info.name();
+    Short dv = normalizeDefaultValue(lv.info.defaultValue(), lv.permitted);
 
     labelHelpColumn = 1 + columns.size();
     labelsTable.setText(row, 0, id);
@@ -329,9 +348,10 @@
       if (lv.permitted.contains(v)) {
         String text = lv.info.value_text(LabelValue.formatValue(v));
         LabelRadioButton b = new LabelRadioButton(group, text, v);
-        if ((self != null && v == self.value()) || (self == null && v == 0)) {
+        if ((self != null && v == self.value()) || (self == null && v.equals(dv))) {
           b.setValue(true);
           group.select(b);
+          in.label(group.label, v);
           labelsTable.setText(row, labelHelpColumn, b.text);
         }
         group.buttons.add(b);
@@ -445,7 +465,7 @@
 
     void select(LabelRadioButton b) {
       selected = b;
-      labelsTable.setText(row, labelHelpColumn, b.value != 0 ? b.text : "");
+      labelsTable.setText(row, labelHelpColumn, b.text);
     }
 
     void selectMax() {
@@ -502,7 +522,7 @@
     @Override
     public void onMouseOut(MouseOutEvent event) {
       LabelRadioButton b = group.selected;
-      String s = b != null && b.value != 0 ? b.text : "";
+      String s = b != null ? b.text : "";
       labelsTable.setText(group.row, labelHelpColumn, s);
     }
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Resources.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Resources.java
index cbbb5b9..7ca3c53 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Resources.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Resources.java
@@ -21,8 +21,8 @@
 
 public interface Resources extends ClientBundle {
   public static final Resources I = GWT.create(Resources.class);
-  static final Constants C = GWT.create(Constants.class);
-  static final Messages M = GWT.create(Messages.class);
+  static final ChangeConstants C = GWT.create(ChangeConstants.class);
+  static final ChangeMessages M = GWT.create(ChangeMessages.class);
 
   @Source("star_open.png") ImageResource star_open();
   @Source("star_filled.png") ImageResource star_filled();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java
index 57ba4b9..87320b8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java
@@ -65,6 +65,7 @@
 
   @UiField Element reviewersText;
   @UiField Button openForm;
+  @UiField Button addMe;
   @UiField Element form;
   @UiField Element error;
   @UiField(provided = true)
@@ -146,6 +147,12 @@
     }
   }
 
+  @UiHandler("addMe")
+  void onAddMe(ClickEvent e) {
+    String accountId = String.valueOf(Gerrit.getUserAccountInfo()._account_id());
+    addReviewer(accountId, false);
+  }
+
   @UiHandler("cancel")
   void onCancel(ClickEvent e) {
     openForm.setVisible(true);
@@ -242,6 +249,13 @@
 
     reviewersText.setInnerSafeHtml(rHtml);
     ccText.setInnerSafeHtml(ccHtml);
+    if (Gerrit.isSignedIn()) {
+      int currentUser = Gerrit.getUserAccountInfo()._account_id();
+      boolean showAddMeButton = info.owner()._account_id() != currentUser
+          && !cc.containsKey(currentUser)
+          && !r.containsKey(currentUser);
+      addMe.setVisible(showAddMeButton);
+    }
   }
 
   private static Map<Integer, VotableInfo> votable(ChangeInfo change) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.ui.xml
index dd2ef78..024197d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.ui.xml
@@ -72,6 +72,10 @@
         <g:Button ui:field='add' styleName='{res.style.button}'>
           <div>Add</div>
         </g:Button>
+        <g:Button ui:field='addMe'
+            styleName='{res.style.button}' visible='false'>
+          <div>Add Me</div>
+        </g:Button>
         <g:Button ui:field='cancel'
             styleName='{res.style.button}'
             addStyleNames='{style.cancel}'>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java
index fbbff76..b41a082 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java
@@ -165,7 +165,7 @@
         && !change.mergeable()) {
       addMissingLabel(Util.C.messageNeedsRebaseOrHasDependency());
     }
-    missing.setVisible(DOM.getChildCount(missing.getElement()) > 0);
+    missing.setVisible(missing.getElement().getChildCount() > 0);
     addReviewer.setVisible(Gerrit.isSignedIn());
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
index b4f3e17..03c58d2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
@@ -47,6 +47,7 @@
   String changeTableColumnProject();
   String changeTableColumnBranch();
   String changeTableColumnLastUpdate();
+  String changeTableColumnID();
   String changeTableNone();
   String changeTableNotMergeable();
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
index fb713b6..c904cac 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
@@ -27,6 +27,7 @@
 changeTableColumnProject = Project
 changeTableColumnBranch = Branch
 changeTableColumnLastUpdate = Updated
+changeTableColumnID = ID
 changeTableNone = (None)
 changeTableNotMergeable = Merge Conflict
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java
index bfe70b8..0c1815f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.client.changes;
 
+import com.google.gerrit.client.WebLinkInfo;
 import com.google.gerrit.client.account.AccountInfo;
 import com.google.gerrit.client.actions.ActionInfo;
 import com.google.gerrit.client.diff.FileInfo;
@@ -161,6 +162,7 @@
 
     public final native boolean optional() /*-{ return this.optional ? true : false; }-*/;
     public final native boolean blocking() /*-{ return this.blocking ? true : false; }-*/;
+    public final native short defaultValue() /*-{ return this.default_value; }-*/;
     final native short _value()
     /*-{
       if (this.value) return this.value;
@@ -218,6 +220,7 @@
 
     public final native boolean has_fetch() /*-{ return this.hasOwnProperty('fetch') }-*/;
     public final native NativeMap<FetchInfo> fetch() /*-{ return this.fetch; }-*/;
+    public final native JsArray<WebLinkInfo> web_links() /*-{ return this.web_links; }-*/;
 
     public static void sortRevisionInfoByNumber(JsArray<RevisionInfo> list) {
       Collections.sort(Natives.asList(list), new Comparator<RevisionInfo>() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java
index 97ba3b1..63f5e29 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java
@@ -25,6 +25,8 @@
 /** List of changes available from {@code /changes/}. */
 public class ChangeList extends JsArray<ChangeInfo> {
   private static final String URI = "/changes/";
+  private static final EnumSet<ListChangesOption> OPTIONS = EnumSet.of(
+      ListChangesOption.LABELS, ListChangesOption.DETAILED_ACCOUNTS);
 
   /** Run 2 or more queries in a single remote invocation. */
   public static void query(
@@ -36,10 +38,8 @@
     for (String q : queries) {
       call.addParameterRaw("q", KeyUtil.encode(q));
     }
-
-    EnumSet<ListChangesOption> o = EnumSet.of(ListChangesOption.LABELS);
-    o.addAll(options);
-    addOptions(call, o);
+    OPTIONS.addAll(options);
+    addOptions(call, OPTIONS);
     call.get(callback);
   }
 
@@ -58,7 +58,7 @@
     if (limit > 0) {
       call.addParameter("n", limit);
     }
-    addOptions(call, EnumSet.of(ListChangesOption.LABELS));
+    addOptions(call, OPTIONS);
     if (start != 0) {
       call.addParameter("S", start);
     }
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 1d760f9..5a6e96c 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
@@ -42,6 +42,7 @@
 import com.google.gerrit.common.data.ChangeInfo;
 import com.google.gerrit.common.data.SubmitTypeRecord;
 import com.google.gerrit.extensions.common.ListChangesOption;
+import com.google.gerrit.extensions.common.SubmitType;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.CommentVisibilityStrategy;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Change.Status;
@@ -50,7 +51,6 @@
 import com.google.gerrit.reviewdb.client.Patch.ChangeType;
 import com.google.gerrit.reviewdb.client.Patch.PatchType;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.core.client.JsArrayString;
 import com.google.gwt.event.dom.client.ChangeEvent;
@@ -300,7 +300,7 @@
       CallbackGroup cbs1 = new CallbackGroup();
       final CallbackGroup cbs2 = new CallbackGroup();
       final PatchSet.Id psId = event.getValue().getCurrentPatchSet().getId();
-      final Map<String, Patch> patches = new HashMap<String, Patch>();
+      final Map<String, Patch> patches = new HashMap<>();
       String revId =
           event.getValue().getCurrentPatchSetDetail().getInfo().getRevId();
 
@@ -311,7 +311,7 @@
             @Override
             public void onSuccess(NativeString result) {
               event.getValue().setSubmitTypeRecord(SubmitTypeRecord.OK(
-                  Project.SubmitType.valueOf(result.asString())));
+                  SubmitType.valueOf(result.asString())));
             }
             public void onFailure(Throwable caught) {}
           }));
@@ -330,7 +330,7 @@
 
               private void dependsOn(RelatedChanges.RelatedInfo info) {
                 ChangeAndCommit self = null;
-                Map<String, ChangeAndCommit> m = new HashMap<String, ChangeAndCommit>();
+                Map<String, ChangeAndCommit> m = new HashMap<>();
                 for (int i = 0; i < info.changes().length(); i++) {
                   ChangeAndCommit c = info.changes().get(i);
                   if (changeId.equals(c.legacy_id())) {
@@ -342,7 +342,7 @@
                 }
                 if (self != null && self.commit() != null
                     && self.commit().parents() != null) {
-                  List<ChangeInfo> d = new ArrayList<ChangeInfo>();
+                  List<ChangeInfo> d = new ArrayList<>();
                   for (CommitInfo p : Natives.asList(self.commit().parents())) {
                     ChangeAndCommit pc = m.get(p.commit());
                     if (pc != null && pc.has_change_number()) {
@@ -356,12 +356,12 @@
               }
 
               private void neededBy(RelatedChanges.RelatedInfo info) {
-                Set<String> mine = new HashSet<String>();
+                Set<String> mine = new HashSet<>();
                 for (PatchSet ps : event.getValue().getPatchSets()) {
                   mine.add(ps.getRevision().get());
                 }
 
-                List<ChangeInfo> n = new ArrayList<ChangeInfo>();
+                List<ChangeInfo> n = new ArrayList<>();
                 for (int i = 0; i < info.changes().length(); i++) {
                   ChangeAndCommit c = info.changes().get(i);
                   if (c.has_change_number()
@@ -418,7 +418,7 @@
               }
               public void onFailure(Throwable caught) {}
             }));
-        final Set<PatchSet.Id> withDrafts = new HashSet<PatchSet.Id>();
+        final Set<PatchSet.Id> withDrafts = new HashSet<>();
         event.getValue().setPatchSetsWithDraftComments(withDrafts);
         for (PatchSet ps : event.getValue().getPatchSets()) {
           if (!ps.getId().equals(psId)) {
@@ -478,7 +478,7 @@
             public void onSuccess(NativeMap<FileInfo> result) {
               JsArray<FileInfo> fileInfos = result.values();
               FileInfo.sortFileInfoByPath(fileInfos);
-              List<Patch> list = new ArrayList<Patch>(fileInfos.length());
+              List<Patch> list = new ArrayList<>(fileInfos.length());
               for (FileInfo f : Natives.asList(fileInfos)) {
                 Patch p = patches.get(f.path());
                 if (p == null) {
@@ -664,7 +664,7 @@
       if (msg.getAuthor() != null) {
         author = FormatUtil.asInfo(accts.get(msg.getAuthor()));
       } else {
-        author = AccountInfo.create(0, Util.C.messageNoAuthor(), null);
+        author = AccountInfo.create(0, Util.C.messageNoAuthor(), null, null);
       }
 
       boolean isRecent;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
index 1cdf30f..4999795 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
@@ -59,7 +59,7 @@
       keysAction.add(new StarKeyCommand(0, 's', Util.C.changeTableStar()));
     }
 
-    sections = new ArrayList<Section>();
+    sections = new ArrayList<>();
     table.setText(0, C_STAR, "");
     table.setText(0, C_SUBJECT, Util.C.changeTableColumnSubject());
     table.setText(0, C_OWNER, Util.C.changeTableColumnOwner());
@@ -307,7 +307,7 @@
           parent.insertNoneRow(dataBegin);
         }
       } else {
-        Set<Change.Id> cids = new HashSet<Change.Id>();
+        Set<Change.Id> cids = new HashSet<>();
 
         if (!hadData) {
           parent.removeRow(dataBegin);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java
index ca9fbfd..270b9f5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java
@@ -19,6 +19,7 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
+import com.google.gerrit.client.account.AccountInfo;
 import com.google.gerrit.client.ui.AccountLinkPanel;
 import com.google.gerrit.client.ui.BranchLink;
 import com.google.gerrit.client.ui.ChangeLink;
@@ -26,12 +27,12 @@
 import com.google.gerrit.client.ui.NeedsSignInKeyCommand;
 import com.google.gerrit.client.ui.ProjectLink;
 import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.ReviewCategoryStrategy;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.HTMLTable.Cell;
@@ -48,14 +49,16 @@
 
 public class ChangeTable2 extends NavigationTable<ChangeInfo> {
   private static final int C_STAR = 1;
-  private static final int C_SUBJECT = 2;
-  private static final int C_STATUS = 3;
-  private static final int C_OWNER = 4;
-  private static final int C_PROJECT = 5;
-  private static final int C_BRANCH = 6;
-  private static final int C_LAST_UPDATE = 7;
-  private static final int C_SIZE = 8;
-  private static final int BASE_COLUMNS = 9;
+  private static final int C_ID = 2;
+  private static final int C_SUBJECT = 3;
+  private static final int C_STATUS = 4;
+  private static final int C_OWNER = 5;
+  private static final int C_PROJECT = 6;
+  private static final int C_BRANCH = 7;
+  private static final int C_LAST_UPDATE = 8;
+  private static final int C_SIZE = 9;
+  private static final int BASE_COLUMNS = 10;
+
 
   private final boolean useNewFeatures = Gerrit.getConfig().getNewFeatures();
   private final List<Section> sections;
@@ -73,6 +76,7 @@
 
     sections = new ArrayList<>();
     table.setText(0, C_STAR, "");
+    table.setText(0, C_ID, Util.C.changeTableColumnID());
     table.setText(0, C_SUBJECT, Util.C.changeTableColumnSubject());
     table.setText(0, C_STATUS, Util.C.changeTableColumnStatus());
     table.setText(0, C_OWNER, Util.C.changeTableColumnOwner());
@@ -85,10 +89,16 @@
 
     final FlexCellFormatter fmt = table.getFlexCellFormatter();
     fmt.addStyleName(0, C_STAR, Gerrit.RESOURCES.css().iconHeader());
-    for (int i = C_SUBJECT; i < columns; i++) {
+    for (int i = C_ID; i < columns; i++) {
       fmt.addStyleName(0, i, Gerrit.RESOURCES.css().dataHeader());
     }
 
+    if (!Gerrit.isSignedIn() ||
+       (!Gerrit.getUserAccount().getGeneralPreferences()
+         .isLegacycidInChangeTable())) {
+      fmt.addStyleName(0, C_ID, Gerrit.RESOURCES.css().dataHeaderHidden());
+    }
+
     table.addClickHandler(new ClickHandler() {
       @Override
       public void onClick(final ClickEvent event) {
@@ -139,7 +149,7 @@
     super.applyDataRowStyle(row);
     final CellFormatter fmt = table.getCellFormatter();
     fmt.addStyleName(row, C_STAR, Gerrit.RESOURCES.css().iconCell());
-    for (int i = C_SUBJECT; i < columns; i++) {
+    for (int i = C_ID; i < columns; i++) {
       fmt.addStyleName(row, i, Gerrit.RESOURCES.css().dataCell());
     }
     fmt.addStyleName(row, C_SUBJECT, Gerrit.RESOURCES.css().cSUBJECT());
@@ -147,6 +157,12 @@
     fmt.addStyleName(row, C_OWNER, Gerrit.RESOURCES.css().cOWNER());
     fmt.addStyleName(row, C_LAST_UPDATE, Gerrit.RESOURCES.css().cLastUpdate());
 
+    if (!Gerrit.isSignedIn() ||
+       (!Gerrit.getUserAccount().getGeneralPreferences()
+         .isLegacycidInChangeTable())) {
+      fmt.addStyleName(row, C_ID, Gerrit.RESOURCES.css().dataCellHidden());
+    }
+
     int i = C_SIZE;
     if (useNewFeatures) {
       fmt.addStyleName(row, i++, Gerrit.RESOURCES.css().cSIZE());
@@ -183,11 +199,8 @@
       String name = labelNames.get(i);
       int col = baseColumns + i;
 
-      StringBuilder abbrev = new StringBuilder();
-      for (String t : name.split("-")) {
-        abbrev.append(t.substring(0, 1).toUpperCase());
-      }
-      table.setText(0, col, abbrev.toString());
+      String abbrev = getAbbreviation(name, "-");
+      table.setText(0, col, abbrev);
       table.getCellFormatter().getElement(0, col).setTitle(name);
       fmt.addStyleName(0, col, Gerrit.RESOURCES.css().dataHeader());
     }
@@ -207,6 +220,7 @@
           c.legacy_id(),
           c.starred()));
     }
+    table.setWidget(row, C_ID, new TableChangeLink(String.valueOf(c.legacy_id()), c));
 
     String subject = Util.cropSubject(c.subject());
     table.setWidget(row, C_SUBJECT, new TableChangeLink(subject, c));
@@ -249,8 +263,8 @@
       col++;
     }
 
-    boolean displayName = Gerrit.isSignedIn() && Gerrit.getUserAccount()
-        .getGeneralPreferences().isShowUsernameInReviewCategory();
+    boolean displayInfo = Gerrit.isSignedIn() && Gerrit.getUserAccount()
+        .getGeneralPreferences().isShowInfoInReviewCategory();
 
     for (int idx = 0; idx < labelNames.size(); idx++, col++) {
       String name = labelNames.get(idx);
@@ -263,39 +277,52 @@
       }
 
       String user;
+      String info;
+      ReviewCategoryStrategy reviewCategoryStrategy = Gerrit.isSignedIn()
+          ? Gerrit.getUserAccount().getGeneralPreferences()
+                .getReviewCategoryStrategy()
+          : ReviewCategoryStrategy.NONE;
       if (label.rejected() != null) {
         user = label.rejected().name();
-        if (displayName && user != null) {
+        info = getReviewCategoryDisplayInfo(reviewCategoryStrategy,
+            label.rejected());
+        if (displayInfo && info != null) {
           FlowPanel panel = new FlowPanel();
           panel.add(new Image(Gerrit.RESOURCES.redNot()));
-          panel.add(new InlineLabel(user));
+          panel.add(new InlineLabel(info));
           table.setWidget(row, col, panel);
         } else {
           table.setWidget(row, col, new Image(Gerrit.RESOURCES.redNot()));
         }
       } else if (label.approved() != null) {
         user = label.approved().name();
-        if (displayName && user != null) {
+        info = getReviewCategoryDisplayInfo(reviewCategoryStrategy,
+            label.approved());
+        if (displayInfo && info != null) {
           FlowPanel panel = new FlowPanel();
           panel.add(new Image(Gerrit.RESOURCES.greenCheck()));
-          panel.add(new InlineLabel(user));
+          panel.add(new InlineLabel(info));
           table.setWidget(row, col, panel);
         } else {
           table.setWidget(row, col, new Image(Gerrit.RESOURCES.greenCheck()));
         }
       } else if (label.disliked() != null) {
         user = label.disliked().name();
+        info = getReviewCategoryDisplayInfo(reviewCategoryStrategy,
+            label.disliked());
         String vstr = String.valueOf(label._value());
-        if (displayName && user != null) {
-          vstr = vstr + " " + user;
+        if (displayInfo && info != null) {
+          vstr = vstr + " " + info;
         }
         fmt.addStyleName(row, col, Gerrit.RESOURCES.css().negscore());
         table.setText(row, col, vstr);
       } else if (label.recommended() != null) {
         user = label.recommended().name();
+        info = getReviewCategoryDisplayInfo(reviewCategoryStrategy,
+            label.recommended());
         String vstr = "+" + label._value();
-        if (displayName && user != null) {
-          vstr = vstr + " " + user;
+        if (displayInfo && info != null) {
+          vstr = vstr + " " + info;
         }
         fmt.addStyleName(row, col, Gerrit.RESOURCES.css().posscore());
         table.setText(row, col, vstr);
@@ -305,7 +332,8 @@
       }
       fmt.addStyleName(row, col, Gerrit.RESOURCES.css().singleLine());
 
-      if (!displayName && user != null) {
+      if ((!displayInfo || reviewCategoryStrategy == ReviewCategoryStrategy.ABBREV)
+          && user != null) {
         // Some web browsers ignore the embedded newline; some like it;
         // so we include a space before the newline to accommodate both.
         fmt.getElement(row, col).setTitle(name + " \nby " + user);
@@ -316,13 +344,39 @@
     if (highlightUnreviewed && !c.reviewed()) {
       needHighlight = true;
     }
-    final Element tr = DOM.getParent(fmt.getElement(row, 0));
+    final Element tr = fmt.getElement(row, 0).getParentElement();
     UIObject.setStyleName(tr, Gerrit.RESOURCES.css().needsReview(),
         needHighlight);
 
     setRowItem(row, c);
   }
 
+  private static String getReviewCategoryDisplayInfo(
+      ReviewCategoryStrategy reviewCategoryStrategy, AccountInfo accountInfo) {
+    switch (reviewCategoryStrategy) {
+      case NAME:
+        return accountInfo.name();
+      case EMAIL:
+        return accountInfo.email();
+      case USERNAME:
+        return accountInfo.username();
+      case ABBREV:
+        return getAbbreviation(accountInfo.name(), " ");
+      default:
+        return null;
+    }
+  }
+
+  private static String getAbbreviation(String name, String token) {
+    StringBuilder abbrev = new StringBuilder();
+    if (name != null) {
+      for (String t : name.split(token)) {
+        abbrev.append(t.substring(0, 1).toUpperCase());
+      }
+    }
+    return abbrev.toString();
+  }
+
   private static Widget getSizeWidget(ChangeInfo c) {
     int largeChangeSize = Gerrit.getConfig().getLargeChangeSize();
     int changedLines = c.insertions() + c.deletions();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CustomDashboardScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CustomDashboardScreen.java
index 840ebaa..320976e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CustomDashboardScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CustomDashboardScreen.java
@@ -26,12 +26,7 @@
 
   @Override
   protected void onInitUI() {
-    table = new DashboardTable(params) {
-      @Override
-      protected void onLoad() {
-        super.onLoad();
-      }
-
+    table = new DashboardTable(this, params) {
       @Override
       public void finishDisplay() {
         super.finishDisplay();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DashboardTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DashboardTable.java
index ac68722..53c5c6d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DashboardTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DashboardTable.java
@@ -18,10 +18,13 @@
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.InlineHyperlink;
+import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.extensions.common.ListChangesOption;
 import com.google.gwt.core.client.JsArray;
+import com.google.gwt.event.dom.client.KeyPressEvent;
 import com.google.gwt.http.client.URL;
+import com.google.gwtexpui.globalkey.client.KeyCommand;
 
 import java.util.ArrayList;
 import java.util.EnumSet;
@@ -34,7 +37,7 @@
   private List<String> titles;
   private List<String> queries;
 
-  public DashboardTable(String params) {
+  public DashboardTable(final Screen screen, String params) {
     titles = new ArrayList<>();
     queries = new ArrayList<>();
     String foreach = null;
@@ -67,18 +70,26 @@
     int i = 0;
     for (String title : titles) {
       Section s = new Section();
-      String query = removeLimit(queries.get(i++));
+      String query = removeLimitAndAge(queries.get(i++));
       s.setTitleWidget(new InlineHyperlink(title, PageLinks.toChangeQuery(query)));
       addSection(s);
       sections.add(s);
     }
+
+    keysNavigation.add(new KeyCommand(0, 'R', Util.C.keyReloadSearch()) {
+      @Override
+      public void onKeyPress(KeyPressEvent event) {
+        Gerrit.display(screen.getToken());
+      }
+    });
   }
 
-  private String removeLimit(String query) {
+  private String removeLimitAndAge(String query) {
     StringBuilder unlimitedQuery = new StringBuilder();
     String[] operators = query.split(" ");
     for (String o : operators) {
-      if (!o.startsWith("limit:")) {
+      if (!o.startsWith("limit:")
+          && !o.startsWith("age:") && !o.startsWith("-age:")) {
         unlimitedQuery.append(o).append(" ");
       }
     }
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 31685e4..b7a0ec8 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
@@ -76,7 +76,7 @@
   private Grid infoTable;
   private Panel actionsPanel;
   private PatchTable patchTable;
-  private final Set<ClickHandler> registeredClickHandler =  new HashSet<ClickHandler>();
+  private final Set<ClickHandler> registeredClickHandler =  new HashSet<>();
 
   private PatchSet.Id diffBaseId;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetsBlock.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetsBlock.java
index 4805185..01c8fdb 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetsBlock.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetsBlock.java
@@ -44,7 +44,7 @@
  */
 public class PatchSetsBlock extends Composite {
   private final Map<PatchSet.Id, PatchSetComplexDisclosurePanel> patchSetPanels =
-      new HashMap<PatchSet.Id, PatchSetComplexDisclosurePanel>();
+      new HashMap<>();
 
   private final FlowPanel body;
   private HandlerRegistration regNavigation;
@@ -84,7 +84,7 @@
       }
     }
 
-    patchSetPanelsList = new ArrayList<PatchSetComplexDisclosurePanel>();
+    patchSetPanelsList = new ArrayList<>();
 
     for (final PatchSet ps : patchSets) {
       final PatchSetComplexDisclosurePanel p =
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 b89d1cc..638ec13 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
@@ -105,7 +105,7 @@
 
   private Map<Key, Integer> patchMap() {
     if (patchMap == null) {
-      patchMap = new HashMap<Patch.Key, Integer>();
+      patchMap = new HashMap<>();
       for (int i = 0; i < patchList.size(); i++) {
         patchMap.put(patchList.get(i).getKey(), i);
       }
@@ -154,7 +154,7 @@
       myTable.addClickHandler(clickHandler);
     } else {
       if (clickHandlers == null) {
-        clickHandlers = new ArrayList<ClickHandler>(2);
+        clickHandlers = new ArrayList<>(2);
       }
       clickHandlers.add(clickHandler);
     }
@@ -268,8 +268,9 @@
       final PatchScreen.Type screenType) {
     if (Dispatcher.isChangeScreen2()) {
       return (patch.getPatchType().equals(PatchType.BINARY)
-          || Gerrit.getUserAccount().getGeneralPreferences().getDiffView()
-          .equals(DiffView.UNIFIED_DIFF));
+          || (Gerrit.isSignedIn()
+              && Gerrit.getUserAccount().getGeneralPreferences().getDiffView()
+                 .equals(DiffView.UNIFIED_DIFF)));
     }
     return screenType == PatchScreen.Type.UNIFIED;
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ProjectDashboardScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ProjectDashboardScreen.java
index 899e86b..d2eec27 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ProjectDashboardScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ProjectDashboardScreen.java
@@ -32,12 +32,7 @@
 
   @Override
   protected void onInitUI() {
-    table = new DashboardTable(params) {
-      @Override
-      protected void onLoad() {
-        super.onLoad();
-      }
-
+    table = new DashboardTable(this, params) {
       @Override
       public void finishDisplay() {
         super.finishDisplay();
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 38a8cf9..f718b5d 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
@@ -36,11 +36,11 @@
 import com.google.gerrit.common.data.ChangeDetail;
 import com.google.gerrit.common.data.SubmitTypeRecord;
 import com.google.gerrit.extensions.common.ListChangesOption;
+import com.google.gerrit.extensions.common.SubmitType;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.core.client.JsArrayString;
 import com.google.gwt.event.dom.client.ClickEvent;
@@ -97,7 +97,7 @@
     super.onInitUI();
     addStyleName(Gerrit.RESOURCES.css().publishCommentsScreen());
 
-    approvalButtons = new ArrayList<ValueRadioButton>();
+    approvalButtons = new ArrayList<>();
     descBlock = new ChangeDescriptionBlock(null);
     add(descBlock);
 
@@ -169,7 +169,7 @@
         @Override
         public void onSuccess(NativeString result) {
           submitTypeRecord = SubmitTypeRecord.OK(
-              Project.SubmitType.valueOf(result.asString()));
+              SubmitType.valueOf(result.asString()));
         }
         public void onFailure(Throwable caught) {}
       }));
@@ -289,7 +289,7 @@
     if (nativeValues == null || nativeValues.length() == 0) {
       return;
     }
-    List<String> values = new ArrayList<String>(nativeValues.length());
+    List<String> values = new ArrayList<>(nativeValues.length());
     for (int i = 0; i < nativeValues.length(); i++) {
       values.add(nativeValues.get(i));
     }
@@ -350,7 +350,7 @@
     }
 
     draftsPanel.clear();
-    commentEditors = new ArrayList<CommentEditorPanel>();
+    commentEditors = new ArrayList<>();
 
     if (!drafts.isEmpty()) {
       draftsPanel.add(new SmallHeading(Util.C.headingPatchComments()));
@@ -468,8 +468,8 @@
   }
 
   private List<PatchLineComment> draftList() {
-    List<PatchLineComment> d = new ArrayList<PatchLineComment>();
-    List<String> paths = new ArrayList<String>(drafts.keySet());
+    List<PatchLineComment> d = new ArrayList<>();
+    List<String> paths = new ArrayList<>(drafts.keySet());
     Collections.sort(paths);
     for (String path : paths) {
       JsArray<CommentInfo> comments = drafts.get(path);
@@ -512,7 +512,7 @@
     SavedState(final PublishCommentScreen p) {
       patchSetId = p.patchSetId;
       message = p.message.getText();
-      approvals = new HashMap<String, String>();
+      approvals = new HashMap<>();
       for (final ValueRadioButton b : p.approvalButtons) {
         if (b.getValue()) {
           approvals.put(b.label.name(), b.value);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ReviewInput.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ReviewInput.java
index 0599a8b..7651495 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ReviewInput.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ReviewInput.java
@@ -61,7 +61,6 @@
         var l=m[p];
         for (var i=0;i<l.length;i++) {
           var c=l[i];
-          delete c['kind'];
           delete c['path'];
           delete c['updated'];
         }
@@ -69,6 +68,14 @@
     }
   }-*/;
 
+  public final native void mergeLabels(ReviewInput o) /*-{
+    var l=o.labels;
+    if (l) {
+      for (var n in l)
+        this.labels[n]=l[n];
+    }
+  }-*/;
+
   protected ReviewInput() {
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/ConfigServerApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/ConfigServerApi.java
index 9cb6c37..5dedaf0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/ConfigServerApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/ConfigServerApi.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.client.config;
 
+import com.google.gerrit.client.account.Preferences;
 import com.google.gerrit.client.extensions.TopMenuList;
 import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.client.rpc.RestApi;
@@ -32,4 +33,8 @@
   public static void topMenus(AsyncCallback<TopMenuList> cb) {
     new RestApi("/config/server/top-menus").get(cb);
   }
+
+  public static void defaultPreferences(AsyncCallback<Preferences> cb) {
+    new RestApi("/config/server/preferences").get(cb);
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ChunkManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ChunkManager.java
index 36bd64f..bc13ac6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ChunkManager.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ChunkManager.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.client.diff;
 
+import static com.google.gerrit.client.diff.DisplaySide.A;
+import static com.google.gerrit.client.diff.DisplaySide.B;
 import static com.google.gerrit.client.diff.OverviewBar.MarkType.DELETE;
 import static com.google.gerrit.client.diff.OverviewBar.MarkType.EDIT;
 import static com.google.gerrit.client.diff.OverviewBar.MarkType.INSERT;
@@ -21,11 +23,14 @@
 import com.google.gerrit.client.diff.DiffInfo.Region;
 import com.google.gerrit.client.diff.DiffInfo.Span;
 import com.google.gerrit.client.rpc.Natives;
+import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.core.client.JsArrayString;
 import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.NativeEvent;
 import com.google.gwt.dom.client.Style.Unit;
 import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.EventListener;
 
 import net.codemirror.lib.CodeMirror;
 import net.codemirror.lib.CodeMirror.LineClassWhere;
@@ -41,6 +46,36 @@
 
 /** Colors modified regions for {@link SideBySide2}. */
 class ChunkManager {
+  private static final String DATA_LINES = "_cs2h";
+  private static double guessedLineHeightPx = 15;
+  private static final JavaScriptObject focusA = initOnClick(A);
+  private static final JavaScriptObject focusB = initOnClick(B);
+  private static final native JavaScriptObject initOnClick(DisplaySide s) /*-{
+    return $entry(function(e){
+      @com.google.gerrit.client.diff.ChunkManager::focus(
+        Lcom/google/gwt/dom/client/NativeEvent;
+        Lcom/google/gerrit/client/diff/DisplaySide;)(e,s)
+    });
+  }-*/;
+
+  private static void focus(NativeEvent event, DisplaySide side) {
+    Element e = Element.as(event.getEventTarget());
+    for (e = DOM.getParent(e); e != null; e = DOM.getParent(e)) {
+      EventListener l = DOM.getEventListener(e);
+      if (l instanceof SideBySide2) {
+        ((SideBySide2) l).getCmFromSide(side).focus();
+        event.stopPropagation();
+      }
+    }
+  };
+
+  static void focusOnClick(Element e, DisplaySide side) {
+    onClick(e, side == A ? focusA : focusB);
+  }
+
+  private static final native void onClick(Element e, JavaScriptObject f)
+  /*-{ e.onclick = f }-*/;
+
   private final SideBySide2 host;
   private final CodeMirror cmA;
   private final CodeMirror cmB;
@@ -51,6 +86,7 @@
   private List<TextMarker> markers;
   private List<Runnable> undo;
   private List<LineWidget> padding;
+  private List<Element> paddingDivs;
 
   ChunkManager(SideBySide2 host,
       CodeMirror cmA,
@@ -68,15 +104,7 @@
   }
 
   DiffChunkInfo getFirst() {
-    if (!chunks.isEmpty()) {
-      for (DiffChunkInfo d : chunks) {
-        if (d.getSide() == DisplaySide.B) {
-          return d;
-        }
-      }
-      return chunks.get(0);
-    }
-    return null;
+    return !chunks.isEmpty() ? chunks.get(0) : null;
   }
 
   void reset() {
@@ -97,6 +125,7 @@
     markers = new ArrayList<>();
     undo = new ArrayList<>();
     padding = new ArrayList<>();
+    paddingDivs = new ArrayList<>();
 
     String diffColor = diff.meta_a() == null || diff.meta_b() == null
         ? DiffTable.style.intralineBg()
@@ -113,6 +142,25 @@
         render(current, diffColor);
       }
     }
+
+    if (paddingDivs.isEmpty()) {
+      paddingDivs = null;
+    }
+  }
+
+  void adjustPadding() {
+    if (paddingDivs != null) {
+      double h = host.getLineHeightPx();
+      for (Element div : paddingDivs) {
+        int lines = div.getPropertyInt(DATA_LINES);
+        div.getStyle().setHeight(lines * h, Unit.PX);
+      }
+      for (LineWidget w : padding) {
+        w.changed();
+      }
+      paddingDivs = null;
+      guessedLineHeightPx = h;
+    }
   }
 
   private void render(Region region, String diffColor) {
@@ -218,16 +266,14 @@
    * @param len number of lines to pad. Padding is inserted only if
    *        {@code len >= 1}.
    */
-  private void addPadding(CodeMirror cm, int line, int len) {
+  private void addPadding(CodeMirror cm, int line, final int len) {
     if (0 < len) {
-      // DiffTable adds 1px bottom padding to each line to preserve
-      // sufficient space for underscores commonly appearing in code.
-      // Padding should be 1em + 1px high for each line. Add within
-      // the browser using height + padding-bottom.
       Element pad = DOM.createDiv();
       pad.setClassName(DiffTable.style.padding());
-      pad.getStyle().setHeight(len, Unit.EM);
-      pad.getStyle().setPaddingBottom(len, Unit.PX);
+      pad.setPropertyInt(DATA_LINES, len);
+      pad.getStyle().setHeight(guessedLineHeightPx * len, Unit.PX);
+      focusOnClick(pad, cm.side());
+      paddingDivs.add(pad);
       padding.add(cm.addLineWidget(
         line == -1 ? 0 : line,
         pad,
@@ -263,7 +309,7 @@
 
         DiffChunkInfo lookUp = chunks.get(res);
         // If edit, skip the deletion chunk and set focus on the insertion one.
-        if (lookUp.isEdit() && lookUp.getSide() == DisplaySide.A) {
+        if (lookUp.isEdit() && lookUp.getSide() == A) {
           res = res + (dir == Direction.PREV ? -1 : 1);
           if (res < 0 || chunks.size() <= res) {
             return;
@@ -291,7 +337,7 @@
       public int compare(DiffChunkInfo a, DiffChunkInfo b) {
         if (a.getSide() == b.getSide()) {
           return a.getStart() - b.getStart();
-        } else if (a.getSide() == DisplaySide.A) {
+        } else if (a.getSide() == A) {
           int comp = mapper.lineOnOther(a.getSide(), a.getStart())
               .getLine() - b.getStart();
           return comp == 0 ? -1 : comp;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBoxUi.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBoxUi.css
index baf475f..da0754b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBoxUi.css
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBoxUi.css
@@ -96,9 +96,12 @@
 .message p,
 .message ul,
 .message blockquote {
-  -webkit-margin-before: 0;
+  -webkit-margin-before: 0.2em;
   -webkit-margin-after: 0.3em;
 }
+.message {
+  white-space: pre-wrap;
+}
 .commentBox button {
   margin-right: 3px;
   margin-bottom: 1px;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentGroup.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentGroup.java
index 9fc36b0..7bcf4da 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentGroup.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentGroup.java
@@ -61,6 +61,7 @@
 
     padding = DOM.createDiv();
     padding.setClassName(DiffTable.style.padding());
+    ChunkManager.focusOnClick(padding, cm.side());
     getElement().appendChild(padding);
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java
index 8a64e76..d56654f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java
@@ -14,11 +14,15 @@
 
 package com.google.gerrit.client.diff;
 
+import com.google.gerrit.client.account.DiffPreferences;
 import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.reviewdb.client.Patch.ChangeType;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.core.client.JsArray;
+import com.google.gwt.core.client.JsArrayString;
 import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.Style.Unit;
 import com.google.gwt.resources.client.CssResource;
 import com.google.gwt.uibinder.client.UiBinder;
 import com.google.gwt.uibinder.client.UiField;
@@ -28,6 +32,8 @@
 import com.google.gwt.user.client.ui.UIObject;
 import com.google.gwt.user.client.ui.Widget;
 
+import net.codemirror.lib.CodeMirror;
+
 /**
  * A table with one row and two columns to hold the two CodeMirrors displaying
  * the files to be diffed.
@@ -47,6 +53,8 @@
     String rangeHighlight();
     String showTabs();
     String showLineNumbers();
+    String hideA();
+    String hideB();
     String columnMargin();
     String padding();
   }
@@ -57,6 +65,8 @@
   @UiField Element patchSetNavRow;
   @UiField Element patchSetNavCellA;
   @UiField Element patchSetNavCellB;
+  @UiField Element diffHeaderRow;
+  @UiField Element diffHeaderText;
   @UiField FlowPanel widgets;
   @UiField static DiffTableStyle style;
 
@@ -67,7 +77,10 @@
   PatchSetSelectBox2 patchSetSelectBoxB;
 
   private SideBySide2 parent;
+  private boolean header;
   private boolean headerVisible;
+  private boolean visibleA;
+  private ChangeType changeType;
 
   DiffTable(SideBySide2 parent, PatchSet.Id base, PatchSet.Id revision,
       String path) {
@@ -80,6 +93,39 @@
     initWidget(uiBinder.createAndBindUi(this));
     this.parent = parent;
     this.headerVisible = true;
+    this.visibleA = true;
+  }
+
+  boolean isVisibleA() {
+    return visibleA;
+  }
+
+  void setVisibleA(boolean show) {
+    visibleA = show;
+    if (show) {
+      removeStyleName(style.hideA());
+      parent.syncScroll(DisplaySide.B); // match B's viewport
+    } else {
+      addStyleName(style.hideA());
+    }
+  }
+
+  Runnable toggleA() {
+    return new Runnable() {
+      @Override
+      public void run() {
+        setVisibleA(!isVisibleA());
+      }
+    };
+  }
+
+  void setVisibleB(boolean show) {
+    if (show) {
+      removeStyleName(style.hideB());
+      parent.syncScroll(DisplaySide.A); // match A's viewport
+    } else {
+      addStyleName(style.hideB());
+    }
   }
 
   boolean isHeaderVisible() {
@@ -89,6 +135,7 @@
   void setHeaderVisible(boolean show) {
     headerVisible = show;
     UIObject.setVisible(patchSetNavRow, show);
+    UIObject.setVisible(diffHeaderRow, show && header);
     if (show) {
       parent.header.removeStyleName(style.fullscreen());
     } else {
@@ -98,12 +145,63 @@
   }
 
   int getHeaderHeight() {
-    return patchSetSelectBoxA.getOffsetHeight();
+    int h = patchSetSelectBoxA.getOffsetHeight();
+    if (header) {
+      h += diffHeaderRow.getOffsetHeight();
+    }
+    return h;
   }
 
-  void setUpPatchSetNav(JsArray<RevisionInfo> list, DiffInfo info) {
+  ChangeType getChangeType() {
+    return changeType;
+  }
+
+  void set(DiffPreferences prefs, JsArray<RevisionInfo> list, DiffInfo info) {
+    this.changeType = info.change_type();
     patchSetSelectBoxA.setUpPatchSetNav(list, info.meta_a());
     patchSetSelectBoxB.setUpPatchSetNav(list, info.meta_b());
+
+    JsArrayString hdr = info.diff_header();
+    if (hdr != null) {
+      StringBuilder b = new StringBuilder();
+      for (int i = 1; i < hdr.length(); i++) {
+        String s = hdr.get(i);
+        if (s.startsWith("diff --git ")
+            || s.startsWith("index ")
+            || s.startsWith("+++ ")
+            || s.startsWith("--- ")) {
+          continue;
+        }
+        b.append(s).append('\n');
+      }
+
+      String hdrTxt = b.toString().trim();
+      header = !hdrTxt.isEmpty();
+      diffHeaderText.setInnerText(hdrTxt);
+      UIObject.setVisible(diffHeaderRow, header);
+    } else {
+      header = false;
+      UIObject.setVisible(diffHeaderRow, false);
+    }
+    setHideEmptyPane(prefs.hideEmptyPane());
+  }
+
+  void setHideEmptyPane(boolean hide) {
+    if (changeType == ChangeType.ADDED) {
+      setVisibleA(!hide);
+    } else if (changeType == ChangeType.DELETED) {
+      setVisibleB(!hide);
+    }
+  }
+
+  void refresh() {
+    overview.refresh();
+    if (header) {
+      CodeMirror cm = parent.getCmFromSide(DisplaySide.A);
+      diffHeaderText.getStyle().setMarginLeft(
+          cm.getGutterElement().getOffsetWidth(),
+          Unit.PX);
+    }
   }
 
   void add(Widget widget) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.ui.xml
index ea931bd..e08a25d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.ui.xml
@@ -29,13 +29,15 @@
       border-bottom: 1px solid #ddd;
     }
 
-    .difftable {
+    .difftable .patchSetNav,
+    .difftable .CodeMirror {
       -webkit-touch-callout: none;
       -webkit-user-select: none;
       -khtml-user-select: none;
       -moz-user-select: none;
       -ms-user-select: none;
     }
+
     .difftable .CodeMirror-lines { padding: 0; }
     .difftable .CodeMirror pre {
       padding: 0;
@@ -53,6 +55,16 @@
       padding-bottom: 1px;
     }
 
+    .hideA .psNavA,
+    .hideA .a {
+      display: none;
+    }
+
+    .hideB .psNavB,
+    .hideB .b {
+      display: none;
+    }
+
     .table {
       width: 100%;
       table-layout: fixed;
@@ -60,6 +72,8 @@
     }
     .table td { padding: 0 }
     .a, .b { width: 50% }
+    .hideA .psNavB, .hideA .b { width: 100% }
+    .hideB .psNavA, .hideB .a { width: 100% }
 
     .overview {
       width: 10px;
@@ -88,7 +102,7 @@
     .dark .noIntraline .a .intralineBg { background-color: #400; }
     .dark .noIntraline .b .intralineBg { background-color: #444; }
 
-    .patchSetNav {
+    .patchSetNav, .diff_header {
       background-color: #f7f7f7;
       line-height: 1;
     }
@@ -153,18 +167,31 @@
       z-index: 2;
       cursor: text;
     }
+
+    .diff_header {
+      font-size: 12px;
+      font-weight: bold;
+      color: #5252ad;
+    }
+    .diff_header pre {
+      margin: 0 0 3px 0;
+    }
   </ui:style>
   <g:HTMLPanel styleName='{style.difftable}'>
     <table class='{style.table}'>
       <tr ui:field='patchSetNavRow' class='{style.patchSetNav}'>
-        <td ui:field='patchSetNavCellA'>
+        <td ui:field='patchSetNavCellA' class='{style.psNavA}'>
           <d:PatchSetSelectBox2 ui:field='patchSetSelectBoxA' />
         </td>
-        <td ui:field='patchSetNavCellB'>
+        <td ui:field='patchSetNavCellB' class='{style.psNavB}'>
           <d:PatchSetSelectBox2 ui:field='patchSetSelectBoxB' />
         </td>
         <td class='{style.overview}' />
       </tr>
+      <tr ui:field='diffHeaderRow' class='{style.diff_header}'>
+        <td colspan='2'><pre ui:field='diffHeaderText' /></td>
+        <td class='{style.overview}' />
+      </tr>
       <tr>
         <td ui:field='cmA' class='{style.a}' />
         <td ui:field='cmB' class='{style.b}' />
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java
index ed7c3b6..ac11cd5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java
@@ -31,7 +31,10 @@
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference.Theme;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
+import com.google.gerrit.reviewdb.client.Patch.ChangeType;
 import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.RepeatingCommand;
 import com.google.gwt.event.dom.client.ChangeEvent;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.KeyDownEvent;
@@ -52,6 +55,12 @@
 import com.google.gwt.user.client.ui.PopupPanel;
 import com.google.gwt.user.client.ui.ToggleButton;
 
+import net.codemirror.lib.ModeInjector;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.TreeMap;
+
 /** Displays current diff preferences. */
 class PreferencesBox extends Composite {
   interface Binder extends UiBinder<HTMLPanel, PreferencesBox> {}
@@ -78,11 +87,14 @@
   @UiField ToggleButton whitespaceErrors;
   @UiField ToggleButton showTabs;
   @UiField ToggleButton lineNumbers;
+  @UiField ToggleButton leftSide;
+  @UiField ToggleButton emptyPane;
   @UiField ToggleButton topMenu;
   @UiField ToggleButton manualReview;
   @UiField ToggleButton expandAllComments;
   @UiField ToggleButton renderEntireFile;
   @UiField ListBox theme;
+  @UiField ListBox mode;
   @UiField Button apply;
   @UiField Button save;
 
@@ -92,6 +104,7 @@
     initWidget(uiBinder.createAndBindUi(this));
     initIgnoreWhitespace();
     initTheme();
+    initMode();
   }
 
   @Override
@@ -114,6 +127,16 @@
         if (prefs.context() == WHOLE_FILE_CONTEXT) {
           contextEntireFile.setValue(true);
         }
+        if (view.canEnableRenderEntireFile(prefs)) {
+          renderEntireFile.setEnabled(true);
+        } else {
+          if (prefs.renderEntireFile()) {
+            prefs.renderEntireFile(false);
+            renderEntireFile.setValue(false);
+            view.updateRenderEntireFile();
+          }
+          renderEntireFile.setEnabled(false);
+        }
         view.setContext(prefs.context());
       }
     };
@@ -129,12 +152,22 @@
     whitespaceErrors.setValue(prefs.showWhitespaceErrors());
     showTabs.setValue(prefs.showTabs());
     lineNumbers.setValue(prefs.showLineNumbers());
+    leftSide.setValue(view.diffTable.isVisibleA());
+    emptyPane.setValue(!prefs.hideEmptyPane());
+    leftSide.setEnabled(!(prefs.hideEmptyPane()
+        && view.diffTable.getChangeType() == ChangeType.ADDED));
     topMenu.setValue(!prefs.hideTopMenu());
     manualReview.setValue(prefs.manualReview());
     expandAllComments.setValue(prefs.expandAllComments());
     renderEntireFile.setValue(prefs.renderEntireFile());
+    renderEntireFile.setEnabled(view.canEnableRenderEntireFile(prefs));
     setTheme(prefs.theme());
 
+    mode.setEnabled(prefs.syntaxHighlighting());
+    if (prefs.syntaxHighlighting()) {
+      setMode(view.getCmFromSide(DisplaySide.B).getStringOption("mode"));
+    }
+
     switch (view.getIntraLineStatus()) {
       case OFF:
       case OK:
@@ -262,6 +295,26 @@
     view.setShowLineNumbers(prefs.showLineNumbers());
   }
 
+  @UiHandler("leftSide")
+  void onLeftSide(ValueChangeEvent<Boolean> e) {
+    view.diffTable.setVisibleA(e.getValue());
+  }
+
+  @UiHandler("emptyPane")
+  void onHideEmptyPane(ValueChangeEvent<Boolean> e) {
+    prefs.hideEmptyPane(!e.getValue());
+    view.diffTable.setHideEmptyPane(prefs.hideEmptyPane());
+    if (prefs.hideEmptyPane()) {
+      if (view.diffTable.getChangeType() == ChangeType.ADDED) {
+        leftSide.setValue(false);
+        leftSide.setEnabled(false);
+      }
+    } else {
+      leftSide.setValue(view.diffTable.isVisibleA());
+      leftSide.setEnabled(true);
+    }
+  }
+
   @UiHandler("topMenu")
   void onTopMenu(ValueChangeEvent<Boolean> e) {
     prefs.hideTopMenu(!e.getValue());
@@ -277,9 +330,36 @@
   @UiHandler("syntaxHighlighting")
   void onSyntaxHighlighting(ValueChangeEvent<Boolean> e) {
     prefs.syntaxHighlighting(e.getValue());
+    mode.setEnabled(prefs.syntaxHighlighting());
+    if (prefs.syntaxHighlighting()) {
+      setMode(view.getContentType());
+    }
     view.setSyntaxHighlighting(prefs.syntaxHighlighting());
   }
 
+  @UiHandler("mode")
+  void onMode(ChangeEvent e) {
+    final String m = mode.getValue(mode.getSelectedIndex());
+    prefs.syntaxHighlighting(true);
+    syntaxHighlighting.setValue(true, false);
+    Scheduler.get().scheduleFixedDelay(new RepeatingCommand() {
+      @Override
+      public boolean execute() {
+        if (prefs.syntaxHighlighting() && view.isAttached()) {
+          view.operation(new Runnable() {
+            @Override
+            public void run() {
+              String mode = m != null && !m.isEmpty() ? m : null;
+              view.getCmFromSide(DisplaySide.A).setOption("mode", mode);
+              view.getCmFromSide(DisplaySide.B).setOption("mode", mode);
+            }
+          });
+        }
+        return false;
+      }
+    }, 50);
+  }
+
   @UiHandler("whitespaceErrors")
   void onWhitespaceErrors(ValueChangeEvent<Boolean> e) {
     prefs.showWhitespaceErrors(e.getValue());
@@ -374,6 +454,52 @@
         IGNORE_ALL_SPACE.name());
   }
 
+  private static final Map<String, String> NAME_TO_MODE;
+  private static final Map<String, String> NORMALIZED_MODES;
+  static {
+    NAME_TO_MODE = new TreeMap<>();
+    NORMALIZED_MODES = new HashMap<>();
+    for (String type : ModeInjector.getKnownMimeTypes()) {
+      String name = type;
+      if (name.startsWith("text/x-")) {
+        name = name.substring("text/x-".length());
+      } else if (name.startsWith("text/")) {
+        name = name.substring("text/".length());
+      } else if (name.startsWith("application/")) {
+        name = name.substring("application/".length());
+      }
+
+      String normalized = NAME_TO_MODE.get(name);
+      if (normalized == null) {
+        normalized = type;
+        NAME_TO_MODE.put(name, normalized);
+      }
+      NORMALIZED_MODES.put(type, normalized);
+    }
+  }
+
+  private void initMode() {
+    mode.addItem("", "");
+    for (Map.Entry<String, String> e : NAME_TO_MODE.entrySet()) {
+      mode.addItem(e.getKey(), e.getValue());
+    }
+  }
+
+  private void setMode(String modeType) {
+    if (modeType != null && !modeType.isEmpty()) {
+      if (NORMALIZED_MODES.containsKey(modeType)) {
+        modeType = NORMALIZED_MODES.get(modeType);
+      }
+      for (int i = 0; i < mode.getItemCount(); i++) {
+        if (mode.getValue(i).equals(modeType)) {
+          mode.setSelectedIndex(i);
+          return;
+        }
+      }
+    }
+    mode.setSelectedIndex(0);
+  }
+
   private void setTheme(Theme v) {
     String name = v != null ? v.name() : Theme.DEFAULT.name();
     for (int i = 0; i < theme.getItemCount(); i++) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.ui.xml
index d940590..af53916 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.ui.xml
@@ -204,6 +204,10 @@
         </g:ToggleButton></td>
       </tr>
       <tr>
+        <th><ui:msg>Language</ui:msg></th>
+        <td><g:ListBox ui:field='mode'/></td>
+      </tr>
+      <tr>
         <th><ui:msg>Whitespace Errors</ui:msg></th>
         <td><g:ToggleButton ui:field='whitespaceErrors'>
           <g:upFace><ui:msg>Hide</ui:msg></g:upFace>
@@ -225,6 +229,20 @@
         </g:ToggleButton></td>
       </tr>
       <tr>
+        <th><ui:msg>Empty Pane</ui:msg></th>
+        <td><g:ToggleButton ui:field='emptyPane'>
+          <g:upFace><ui:msg>Hide</ui:msg></g:upFace>
+          <g:downFace><ui:msg>Show</ui:msg></g:downFace>
+        </g:ToggleButton></td>
+      </tr>
+      <tr>
+        <th><ui:msg>Left Side</ui:msg></th>
+        <td><g:ToggleButton ui:field='leftSide'>
+          <g:upFace><ui:msg>Hide</ui:msg></g:upFace>
+          <g:downFace><ui:msg>Show</ui:msg></g:downFace>
+        </g:ToggleButton></td>
+      </tr>
+      <tr>
         <th><ui:msg>Top Menu</ui:msg></th>
         <td><g:ToggleButton ui:field='topMenu'>
           <g:upFace><ui:msg>Hide</ui:msg></g:upFace>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ScrollSynchronizer.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ScrollSynchronizer.java
index ba74202..abff8ef 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ScrollSynchronizer.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ScrollSynchronizer.java
@@ -24,6 +24,8 @@
   private LineMapper mapper;
   private OverviewBar overview;
   private ScrollCallback active;
+  private ScrollCallback callbackA;
+  private ScrollCallback callbackB;
 
   ScrollSynchronizer(DiffTable diffTable,
       CodeMirror cmA, CodeMirror cmB,
@@ -32,8 +34,14 @@
     this.mapper = mapper;
     this.overview = diffTable.overview;
 
-    cmA.on("scroll", new ScrollCallback(cmA, cmB, DisplaySide.A));
-    cmB.on("scroll", new ScrollCallback(cmB, cmA, DisplaySide.B));
+    callbackA = new ScrollCallback(cmA, cmB, DisplaySide.A);
+    callbackB = new ScrollCallback(cmB, cmA, DisplaySide.B);
+    cmA.on("scroll", callbackA);
+    cmB.on("scroll", callbackB);
+  }
+
+  void syncScroll(DisplaySide masterSide) {
+    (masterSide == DisplaySide.A ? callbackA : callbackB).sync();
   }
 
   private void updateScreenHeader(ScrollInfo si) {
@@ -66,6 +74,10 @@
       };
     }
 
+    void sync() {
+      dst.scrollToY(align(src.getScrollInfo().getTop()));
+    }
+
     @Override
     public void run() {
       if (active == null) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide2.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide2.java
index 20d4d69..30306f2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide2.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide2.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.client.diff;
 
+import static com.google.gerrit.reviewdb.client.AccountDiffPreference.WHOLE_FILE_CONTEXT;
 import static java.lang.Double.POSITIVE_INFINITY;
 
 import com.google.gerrit.client.Gerrit;
@@ -24,6 +25,7 @@
 import com.google.gerrit.client.changes.ChangeInfo;
 import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
 import com.google.gerrit.client.changes.ChangeList;
+import com.google.gerrit.client.diff.DiffInfo.FileMeta;
 import com.google.gerrit.client.diff.LineMapper.LineOnOtherInfo;
 import com.google.gerrit.client.patches.PatchUtil;
 import com.google.gerrit.client.projects.ConfigInfoCache;
@@ -83,6 +85,18 @@
   interface Binder extends UiBinder<FlowPanel, SideBySide2> {}
   private static final Binder uiBinder = GWT.create(Binder.class);
 
+  enum FileSize {
+    SMALL(0),
+    LARGE(500),
+    HUGE(4000);
+
+    final int lines;
+
+    FileSize(int n) {
+      this.lines = n;
+    }
+  }
+
   @UiField(provided = true)
   Header header;
 
@@ -102,9 +116,12 @@
   private Element columnMarginA;
   private Element columnMarginB;
   private double charWidthPx;
+  private double lineHeightPx;
+
   private HandlerRegistration resizeHandler;
+  private ScrollSynchronizer scrollSynchronizer;
   private DiffInfo diff;
-  private boolean largeFile;
+  private FileSize fileSize;
   private ChunkManager chunkManager;
   private CommentManager commentManager;
   private SkipManager skipManager;
@@ -164,9 +181,9 @@
         @Override
         public void onSuccess(DiffInfo diffInfo) {
           diff = diffInfo;
+          fileSize = bucketFileSize(diffInfo);
           if (prefs.syntaxHighlighting()) {
-            largeFile = isLargeFile(diffInfo);
-            if (largeFile) {
+            if (fileSize.compareTo(FileSize.SMALL) > 0) {
               modeInjectorCb.onSuccess(null);
             } else {
               injectMode(diffInfo, modeInjectorCb);
@@ -189,7 +206,7 @@
         info.revisions().copyKeysIntoChildren("name");
         JsArray<RevisionInfo> list = info.revisions().values();
         RevisionInfo.sortRevisionInfoByNumber(list);
-        diffTable.setUpPatchSetNav(list, diff);
+        diffTable.set(prefs, list, diff);
         header.setChangeInfo(info);
       }}));
 
@@ -228,18 +245,24 @@
       public void run() {
         cmA.setHeight(height);
         cmB.setHeight(height);
+        chunkManager.adjustPadding();
         cmA.refresh();
         cmB.refresh();
       }
     });
     setLineLength(prefs.lineLength());
-    diffTable.overview.refresh();
+    diffTable.refresh();
 
-    if (startLine == 0 && diff.meta_b() != null) {
+    if (startLine == 0) {
       DiffChunkInfo d = chunkManager.getFirst();
       if (d != null) {
-        startSide = d.getSide();
-        startLine = d.getStart() + 1;
+        if (d.isEdit() && d.getSide() == DisplaySide.A) {
+          startSide = DisplaySide.B;
+          startLine = lineOnOther(d.getSide(), d.getStart()).getLine() + 1;
+        } else {
+          startSide = d.getSide();
+          startLine = d.getStart() + 1;
+        }
       }
     }
     if (startSide != null && startLine > 0) {
@@ -312,8 +335,9 @@
         .on("C", commentManager.insertNewDraft(cm))
         .on("N", maybeNextVimSearch(cm))
         .on("P", chunkManager.diffChunkNav(cm, Direction.PREV))
+        .on("Shift-A", diffTable.toggleA())
         .on("Shift-M", header.reviewedAndNext())
-        .on("Shift-N", commentManager.commentNav(cm, Direction.NEXT))
+        .on("Shift-N", maybePrevVimSearch(cm))
         .on("Shift-P", commentManager.commentNav(cm, Direction.PREV))
         .on("Shift-O", commentManager.openCloseAll(cm))
         .on("Shift-Left", moveCursorToSide(cm, DisplaySide.A))
@@ -406,6 +430,9 @@
 
     keysNavigation.add(new UpToChangeCommand2(revision, 0, 'u'));
     keysNavigation.add(
+        new NoOpKeyCommand(KeyCommand.M_SHIFT, KeyCodes.KEY_LEFT, PatchUtil.C.focusSideA()),
+        new NoOpKeyCommand(KeyCommand.M_SHIFT, KeyCodes.KEY_RIGHT, PatchUtil.C.focusSideB()));
+    keysNavigation.add(
         new NoOpKeyCommand(0, 'j', PatchUtil.C.lineNext()),
         new NoOpKeyCommand(0, 'k', PatchUtil.C.linePrev()));
     keysNavigation.add(
@@ -444,6 +471,13 @@
         upToChange(true).run();
       }
     });
+    keysAction.add(new KeyCommand(
+        KeyCommand.M_SHIFT, 'a', PatchUtil.C.toggleSideA()) {
+      @Override
+      public void onKeyPress(KeyPressEvent event) {
+        diffTable.toggleA().run();
+      }
+    });
     keysAction.add(new KeyCommand(0, ',', PatchUtil.C.showPreferences()) {
       @Override
       public void onKeyPress(KeyPressEvent event) {
@@ -472,11 +506,11 @@
     }
 
     removeKeyHandlerRegistrations();
+    handlers.add(GlobalKey.add(this, keysAction));
     handlers.add(GlobalKey.add(this, keysNavigation));
     if (keysComment != null) {
       handlers.add(GlobalKey.add(this, keysComment));
     }
-    handlers.add(GlobalKey.add(this, keysAction));
     handlers.add(ShowHelpCommand.addFocusHandler(new FocusHandler() {
       @Override
       public void onFocus(FocusEvent event) {
@@ -506,6 +540,11 @@
     cmA.getMoverElement().appendChild(columnMarginA);
     cmB.getMoverElement().appendChild(columnMarginB);
 
+    if (prefs.renderEntireFile() && !canEnableRenderEntireFile(prefs)) {
+      // CodeMirror is too slow to layout an entire huge file.
+      prefs.renderEntireFile(false);
+    }
+
     operation(new Runnable() {
       public void run() {
         // Estimate initial CM3 height, fixed up in onShowView.
@@ -522,12 +561,13 @@
 
     registerCmEvents(cmA);
     registerCmEvents(cmB);
-    new ScrollSynchronizer(diffTable, cmA, cmB, chunkManager.getLineMapper());
+    scrollSynchronizer = new ScrollSynchronizer(diffTable, cmA, cmB,
+            chunkManager.getLineMapper());
 
     prefsAction = new PreferencesAction(this, prefs);
     header.init(prefsAction);
 
-    if (largeFile && prefs.syntaxHighlighting()) {
+    if (prefs.syntaxHighlighting() && fileSize.compareTo(FileSize.SMALL) > 0) {
       Scheduler.get().scheduleFixedDelay(new RepeatingCommand() {
         @Override
         public boolean execute() {
@@ -545,13 +585,16 @@
       String contents,
       DisplaySide side,
       Element parent) {
+    String mode = fileSize == FileSize.SMALL
+        ? getContentType(meta)
+        : null;
     return CodeMirror.create(side, parent, Configuration.create()
       .set("readOnly", true)
       .set("cursorBlinkRate", 0)
       .set("cursorHeight", 0.85)
       .set("lineNumbers", prefs.showLineNumbers())
       .set("tabSize", prefs.tabSize())
-      .set("mode", largeFile ? null : getContentType(meta))
+      .set("mode", mode)
       .set("lineWrapping", false)
       .set("styleSelectedText", true)
       .set("showTrailingSpace", prefs.showWhitespaceErrors())
@@ -565,6 +608,15 @@
     return diff.intraline_status();
   }
 
+  boolean canEnableRenderEntireFile(DiffPreferences prefs) {
+    return fileSize.compareTo(FileSize.HUGE) < 0
+        || (prefs.context() != WHOLE_FILE_CONTEXT && prefs.context() < 100);
+  }
+
+  String getContentType() {
+    return getContentType(diff.meta_b());
+  }
+
   void setThemeStyles(boolean d) {
     if (d) {
       diffTable.addStyleName(DiffTable.style.dark());
@@ -587,6 +639,26 @@
     columnMarginB.getStyle().setMarginLeft(w, Style.Unit.PX);
   }
 
+  double getLineHeightPx() {
+    if (lineHeightPx <= 1) {
+      Element p = DOM.createDiv();
+      int lines = 1;
+      for (int i = 0; i < lines; i++) {
+        Element e = DOM.createDiv();
+        p.appendChild(e);
+
+        Element pre = DOM.createElement("pre");
+        pre.setInnerText("gqyŚŻŹŃ");
+        e.appendChild(pre);
+      }
+
+      cmB.getMeasureElement().appendChild(p);
+      lineHeightPx = ((double) p.getOffsetHeight()) / lines;
+      p.removeFromParent();
+    }
+    return lineHeightPx;
+  }
+
   private double getCharWidthPx() {
     if (charWidthPx <= 1) {
       int len = 100;
@@ -598,11 +670,11 @@
       e.getStyle().setDisplay(Style.Display.INLINE_BLOCK);
       e.setInnerText(s.toString());
 
-      cmA.getMoverElement().appendChild(e);
+      cmA.getMeasureElement().appendChild(e);
       double a = ((double) e.getOffsetWidth()) / len;
       e.removeFromParent();
 
-      cmB.getMoverElement().appendChild(e);
+      cmB.getMeasureElement().appendChild(e);
       double b = ((double) e.getOffsetWidth()) / len;
       e.removeFromParent();
       charWidthPx = Math.max(a, b);
@@ -804,6 +876,19 @@
     };
   }
 
+  private Runnable maybePrevVimSearch(final CodeMirror cm) {
+    return new Runnable() {
+      @Override
+      public void run() {
+        if (cm.hasVimSearchHighlight()) {
+          CodeMirror.handleVimKey(cm, "N");
+        } else {
+          commentManager.commentNav(cm, Direction.NEXT).run();
+        }
+      }
+    };
+  }
+
   private Runnable maybeNextVimSearch(final CodeMirror cm) {
     return new Runnable() {
       @Override
@@ -844,6 +929,12 @@
     return Window.getClientHeight() - rest;
   }
 
+  void syncScroll(DisplaySide masterSide) {
+    if (scrollSynchronizer != null) {
+      scrollSynchronizer.syncScroll(masterSide);
+    }
+  }
+
   private String getContentType(DiffInfo.FileMeta meta) {
     return prefs.syntaxHighlighting()
           && meta != null
@@ -933,6 +1024,7 @@
                 diffTable.overview.clearDiffMarkers();
                 setShowIntraline(prefs.intralineDifference());
                 render(diff);
+                chunkManager.adjustPadding();
                 skipManager.render(prefs.context(), diff);
               }
             });
@@ -941,8 +1033,17 @@
       });
   }
 
-  private static boolean isLargeFile(DiffInfo diffInfo) {
-    return (diffInfo.meta_a() != null && diffInfo.meta_a().lines() > 500)
-        || (diffInfo.meta_b() != null && diffInfo.meta_b().lines() > 500);
+  private static FileSize bucketFileSize(DiffInfo diff) {
+    FileMeta a = diff.meta_a();
+    FileMeta b = diff.meta_b();
+    FileSize[] sizes = FileSize.values();
+    for (int i = sizes.length - 1; 0 <= i; i--) {
+      FileSize s = sizes[i];
+      if ((a != null && s.lines <= a.lines())
+          || (b != null && s.lines <= b.lines())) {
+        return s;
+      }
+    }
+    return FileSize.SMALL;
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuItem.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuItem.java
index 22bb981..ba43068 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuItem.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuItem.java
@@ -17,11 +17,21 @@
 import com.google.gwt.core.client.JavaScriptObject;
 
 public class TopMenuItem extends JavaScriptObject {
+  public static TopMenuItem create(String name, String url) {
+    TopMenuItem i = createObject().cast();
+    i.name(name);
+    i.url(url);
+    return i;
+  }
+
   public final native String getName() /*-{ return this.name; }-*/;
   public final native String getUrl() /*-{ return this.url; }-*/;
   public final native String getTarget() /*-{ return this.target; }-*/;
   public final native String getId() /*-{ return this.id; }-*/;
 
+  public final native void name(String n) /*-{ this.name = n }-*/;
+  public final native void url(String u) /*-{ this.url = u }-*/;
+
   protected TopMenuItem() {
   }
 }
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 6a507fe..b3602a2 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
@@ -547,6 +547,10 @@
   height: 20px;
 }
 
+.changeTable .dataCellHidden {
+  display: none;
+}
+
 .changeTable a.gwt-InlineHyperlink,
 .changeTable a.gwt-Anchor {
   color: #222 !important;
@@ -634,6 +638,10 @@
   color: textColor;
 }
 
+.changeTable .dataHeaderHidden {
+  display: none;
+}
+
 .changeTable .sectionHeader {
   border-top: 8px solid backgroundColor;
   padding: 2px 6px 1px;
@@ -1630,3 +1638,13 @@
   padding-top: 5px;
   padding-left: 5px;
 }
+
+/* StringListPanel */
+.stringListPanelButtons {
+  margin-left: 0.5em;
+}
+.stringListPanelButtons .gwt-Button {
+  margin-right: 2em;
+  font-size: 7pt;
+  padding: 1px;
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/listAdd.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/listAdd.png
new file mode 100644
index 0000000..1aa7f09
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/listAdd.png
Binary files differ
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 ae0b786..9ff9893 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
@@ -49,9 +49,12 @@
   String chunkNext2();
   String commentPrev();
   String commentNext();
+  String focusSideA();
+  String focusSideB();
   String fileList();
   String expandComment();
   String expandAllCommentsOnCurrentLine();
+  String toggleSideA();
   String toggleIntraline();
   String showPreferences();
 
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 3e021c7..0a47613 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
@@ -31,9 +31,12 @@
 chunkNext2 = Next diff chunk or search result
 commentPrev = Previous comment
 commentNext = Next comment
+focusSideA = Focus left side
+focusSideB = Focus right side
 fileList = Browse files in patch set
 expandComment = Expand or collapse comment
 expandAllCommentsOnCurrentLine = Expand or collapse all comments on current line
+toggleSideA = Toggle left side
 toggleIntraline = Toggle intraline difference
 showPreferences = Show diff preferences
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.java
index e45ee1f..083820b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.java
@@ -90,7 +90,7 @@
     this.idSideA = idSideA;
     this.idSideB = idSideB;
     this.idActive = (side == Side.A) ? idSideA : idSideB;
-    this.links = new HashMap<Integer, Anchor>();
+    this.links = new HashMap<>();
 
     linkPanel.clear();
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/BranchInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/BranchInfo.java
index 3f79afe..849863e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/BranchInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/BranchInfo.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.client.projects;
 
+import com.google.gerrit.client.actions.ActionInfo;
+import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gwt.core.client.JavaScriptObject;
 
@@ -27,6 +29,7 @@
   public final native String ref() /*-{ return this.ref; }-*/;
   public final native String revision() /*-{ return this.revision; }-*/;
   public final native boolean canDelete() /*-{ return this['can_delete'] ? true : false; }-*/;
+  public final native NativeMap<ActionInfo> actions() /*-{ return this.actions }-*/;
 
   protected BranchInfo() {
   }
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
index 1d6e6b2..e3c77b8 100644
--- 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
@@ -17,9 +17,9 @@
 import com.google.gerrit.client.ErrorDialog;
 import com.google.gerrit.client.actions.ActionInfo;
 import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
-import com.google.gerrit.reviewdb.client.Project.SubmitType;
+import com.google.gerrit.extensions.api.projects.ProjectState;
+import com.google.gerrit.extensions.common.InheritableBoolean;
+import com.google.gerrit.extensions.common.SubmitType;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.core.client.JsArrayString;
@@ -63,11 +63,11 @@
   private final native String submit_typeRaw()
   /*-{ return this.submit_type }-*/;
 
-  public final Project.State state() {
+  public final ProjectState state() {
     if (stateRaw() == null) {
-      return Project.State.ACTIVE;
+      return ProjectState.ACTIVE;
     }
-    return Project.State.valueOf(stateRaw());
+    return ProjectState.valueOf(stateRaw());
   }
   private final native String stateRaw()
   /*-{ return this.state }-*/;
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 63f7e15..4762027 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
@@ -19,9 +19,10 @@
 import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.client.rpc.NativeString;
 import com.google.gerrit.client.rpc.RestApi;
+import com.google.gerrit.extensions.api.projects.ProjectState;
+import com.google.gerrit.extensions.common.InheritableBoolean;
+import com.google.gerrit.extensions.common.SubmitType;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
-import com.google.gerrit.reviewdb.client.Project.SubmitType;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.user.client.rpc.AsyncCallback;
@@ -85,7 +86,7 @@
       InheritableBoolean useContributorAgreements,
       InheritableBoolean useContentMerge, InheritableBoolean useSignedOffBy,
       InheritableBoolean requireChangeId, String maxObjectSizeLimit,
-      SubmitType submitType, Project.State state,
+      SubmitType submitType, ProjectState state,
       Map<String, Map<String, ConfigParameterValue>> pluginConfigValues,
       AsyncCallback<ConfigInfo> cb) {
     ConfigInput in = ConfigInput.create();
@@ -217,7 +218,7 @@
     private final native void setSubmitTypeRaw(String t)
     /*-{ if(t)this.submit_type=t; }-*/;
 
-    final void setState(Project.State s) {
+    final void setState(ProjectState s) {
       setStateRaw(s.name());
     }
     private final native void setStateRaw(String s)
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectInfo.java
index cab45b5..048ebbd 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectInfo.java
@@ -14,8 +14,11 @@
 
 package com.google.gerrit.client.projects;
 
+import com.google.gerrit.client.WebLinkInfo;
+import com.google.gerrit.extensions.api.projects.ProjectState;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
 import com.google.gwt.user.client.ui.SuggestOracle;
 
 public class ProjectInfo
@@ -27,9 +30,10 @@
 
   public final native String name() /*-{ return this.name; }-*/;
   public final native String description() /*-{ return this.description; }-*/;
+  public final native JsArray<WebLinkInfo> web_links() /*-{ return this.web_links; }-*/;
 
-  public final Project.State state() {
-    return Project.State.valueOf(getStringState());
+  public final ProjectState state() {
+    return ProjectState.valueOf(getStringState());
   }
 
   private final native String getStringState() /*-{ return this.state; }-*/;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectMap.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectMap.java
index f262660..29a8a01 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectMap.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectMap.java
@@ -56,7 +56,11 @@
   public static void match(String match, int limit, int start, AsyncCallback<ProjectMap> cb) {
     RestApi call = new RestApi("/projects/");
     if (match != null) {
-      call.addParameter("m", match);
+      if (match.startsWith("^")) {
+        call.addParameter("r", match);
+      } else {
+        call.addParameter("m", match);
+      }
     }
     if (limit > 0) {
       call.addParameter("n", limit);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java
index da620d8..2bc4ac1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java
@@ -327,7 +327,7 @@
 
   private <T extends JavaScriptObject> void send(
       Method method, AsyncCallback<T> cb) {
-    HttpCallback<T> httpCallback = new HttpCallback<T>(background, cb);
+    HttpCallback<T> httpCallback = new HttpCallback<>(background, cb);
     try {
       if (!background) {
         RpcStatus.INSTANCE.onRpcStart();
@@ -367,7 +367,7 @@
   private <T extends JavaScriptObject> void sendJSON(
       Method method, JavaScriptObject content,
       AsyncCallback<T> cb) {
-    HttpCallback<T> httpCallback = new HttpCallback<T>(background, cb);
+    HttpCallback<T> httpCallback = new HttpCallback<>(background, cb);
     try {
       if (!background) {
         RpcStatus.INSTANCE.onRpcStart();
@@ -384,7 +384,7 @@
 
   private <T extends JavaScriptObject> void sendRaw(Method method, String body,
       AsyncCallback<T> cb) {
-    HttpCallback<T> httpCallback = new HttpCallback<T>(background, cb);
+    HttpCallback<T> httpCallback = new HttpCallback<>(background, cb);
     try {
       if (!background) {
         RpcStatus.INSTANCE.onRpcStart();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLinkPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLinkPanel.java
index b883594..cdad972 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLinkPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLinkPanel.java
@@ -41,7 +41,8 @@
     this(AccountInfo.create(
         ident.getAccount().get(),
         ident.getName(),
-        ident.getEmail()));
+        ident.getEmail(),
+        ident.getUsername()));
   }
 
   public AccountLinkPanel(AccountInfo info) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CherryPickDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CherryPickDialog.java
index 1d5a74f..54c4c53 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CherryPickDialog.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CherryPickDialog.java
@@ -50,8 +50,7 @@
     newBranch = new SuggestBox(new HighlightSuggestOracle() {
       @Override
       protected void onRequestSuggestions(Request request, Callback done) {
-        LinkedList<BranchSuggestion> suggestions =
-            new LinkedList<BranchSuggestion>();
+        LinkedList<BranchSuggestion> suggestions = new LinkedList<>();
         for (final BranchInfo b : branches) {
           if (b.ref().contains(request.getQuery())) {
             suggestions.add(new BranchSuggestion(b));
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTable.java
index c7d9658..88ee293 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTable.java
@@ -134,8 +134,8 @@
 
   protected void scrollIntoView(final int topRow, final int endRow) {
     final CellFormatter fmt = table.getCellFormatter();
-    final Element top = DOM.getParent(fmt.getElement(topRow, C_ARROW));
-    final Element end = DOM.getParent(fmt.getElement(endRow, C_ARROW));
+    final Element top = fmt.getElement(topRow, C_ARROW).getParentElement();
+    final Element end = fmt.getElement(endRow, C_ARROW).getParentElement();
 
     final int rTop = top.getAbsoluteTop();
     final int rEnd = end.getAbsoluteTop() + end.getOffsetHeight();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuBar.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuBar.java
index bc88eed..deca808 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuBar.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuBar.java
@@ -41,10 +41,12 @@
   }
 
   public void addItem(final LinkMenuItem i) {
+    i.setMenuBar(this);
     add(i);
   }
 
   public void insertItem(final LinkMenuItem i, int beforeIndex) {
+    i.setMenuBar(this);
     insert(i, beforeIndex);
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuItem.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuItem.java
index 20569c3..29c053b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuItem.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuItem.java
@@ -19,6 +19,8 @@
 import com.google.gwt.dom.client.AnchorElement;
 
 public class LinkMenuItem extends InlineHyperlink implements ScreenLoadHandler {
+  private LinkMenuBar bar;
+
   public LinkMenuItem(final String text, final String targetHistoryToken) {
     super(text, targetHistoryToken);
     setStyleName(Gerrit.RESOURCES.css().menuItem());
@@ -32,11 +34,20 @@
     AnchorElement.as(getElement()).blur();
   }
 
+  public void setMenuBar(LinkMenuBar bar) {
+    this.bar = bar;
+  }
+
   public void onScreenLoad(ScreenLoadEvent event) {
-    if (event.getScreen().getToken().equals(getTargetHistoryToken())){
+    if (match(event.getScreen().getToken())) {
+      Gerrit.selectMenu(bar);
       addStyleName(Gerrit.RESOURCES.css().activeRow());
     } else {
       removeStyleName(Gerrit.RESOURCES.css().activeRow());
     }
   }
+
+  protected boolean match(String token) {
+    return token.equals(getTargetHistoryToken());
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NavigationTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NavigationTable.java
index 83a22f5..4a56558 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NavigationTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NavigationTable.java
@@ -168,7 +168,7 @@
     final int sEnd = sTop + Document.get().getClientHeight();
 
     while (0 <= row && row < max) {
-      final Element cur = DOM.getParent(fmt.getElement(row, C_ARROW));
+      final Element cur = fmt.getElement(row, C_ARROW).getParentElement();
       final int cTop = cur.getAbsoluteTop();
       final int cEnd = cTop + cur.getOffsetHeight();
 
@@ -194,13 +194,13 @@
     final CellFormatter fmt = table.getCellFormatter();
     final boolean clear = 0 <= currentRow && currentRow < table.getRowCount();
     if (clear) {
-      final Element tr = DOM.getParent(fmt.getElement(currentRow, C_ARROW));
+      final Element tr = fmt.getElement(currentRow, C_ARROW).getParentElement();
       UIObject.setStyleName(tr, Gerrit.RESOURCES.css().activeRow(), false);
     }
     if (0 <= newRow && newRow < table.getRowCount()
         && getRowItem(newRow) != null) {
       table.setWidget(newRow, C_ARROW, pointer);
-      final Element tr = DOM.getParent(fmt.getElement(newRow, C_ARROW));
+      final Element tr = fmt.getElement(newRow, C_ARROW).getParentElement();
       UIObject.setStyleName(tr, Gerrit.RESOURCES.css().activeRow(), true);
       if (scroll && isAttached()) {
         scrollIntoView(tr);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/OnEditEnabler.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/OnEditEnabler.java
index a2155d4..819a11f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/OnEditEnabler.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/OnEditEnabler.java
@@ -87,7 +87,7 @@
     // Resetting the "original text" on focus ensures that we are
     // up to date with non-user updates of the text (calls to
     // setText()...) and also up to date with user changes which
-    // occured after enabling "widget".
+    // occurred after enabling "widget".
     tb.addFocusHandler(new FocusHandler() {
         @Override
         public void onFocus(FocusEvent event) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectLinkMenuItem.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectLinkMenuItem.java
new file mode 100644
index 0000000..2917505
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectLinkMenuItem.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.ui;
+
+import com.google.gerrit.client.Dispatcher;
+import com.google.gerrit.client.admin.ProjectScreen;
+import com.google.gerrit.reviewdb.client.Project;
+
+public class ProjectLinkMenuItem extends LinkMenuItem {
+  private final String panel;
+
+  public ProjectLinkMenuItem(String text, String panel) {
+    super(text, "");
+    this.panel = panel;
+  }
+
+  @Override
+  public void onScreenLoad(ScreenLoadEvent event) {
+    Screen screen = event.getScreen();
+    Project.NameKey projectKey;
+    if (screen instanceof ProjectScreen) {
+      projectKey = ((ProjectScreen)screen).getProjectKey();
+    } else {
+      projectKey = ProjectScreen.getSavedKey();
+    }
+
+    if (projectKey != null) {
+      setVisible(true);
+      setTargetHistoryToken(Dispatcher.toProjectAdmin(projectKey, panel));
+    } else {
+      setVisible(false);
+    }
+    super.onScreenLoad(event);
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ScreenLoadEvent.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ScreenLoadEvent.java
index 562e53a..45ba808 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ScreenLoadEvent.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ScreenLoadEvent.java
@@ -24,7 +24,7 @@
     this.screen = screen;
   }
 
-  public static final Type<ScreenLoadHandler> TYPE = new Type<ScreenLoadHandler>();
+  public static final Type<ScreenLoadHandler> TYPE = new Type<>();
 
   @Override
   protected void dispatch(ScreenLoadHandler handler) {
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java
index 0e5f9a5..85b1fa6 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java
@@ -57,13 +57,13 @@
     this.setOption(option, val);
   }-*/;
 
+  public final native String getStringOption(String o) /*-{ return this.getOption(o) }-*/;
   public final native void setValue(String v) /*-{ this.setValue(v); }-*/;
 
   public final native void setWidth(double w) /*-{ this.setSize(w, null); }-*/;
   public final native void setWidth(String w) /*-{ this.setSize(w, null); }-*/;
   public final native void setHeight(double h) /*-{ this.setSize(null, h); }-*/;
   public final native void setHeight(String h) /*-{ this.setSize(null, h); }-*/;
-  public final native double defaultCharWidth() /*-{ return this.defaultCharWidth() }-*/;
   public final native String getLine(int n) /*-{ return this.getLine(n) }-*/;
 
   public final native void refresh() /*-{ this.refresh(); }-*/;
@@ -290,6 +290,10 @@
     return this.display.mover;
   }-*/;
 
+  public final native Element getMeasureElement() /*-{
+    return this.display.measure;
+  }-*/;
+
   public final native Element getScrollbarV() /*-{
     return this.display.scrollbarV;
   }-*/;
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/ModeInjector.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/ModeInjector.java
index 5cd88b4..3098c43 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/ModeInjector.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/ModeInjector.java
@@ -21,6 +21,7 @@
 
 import net.codemirror.mode.Modes;
 
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
@@ -50,6 +51,7 @@
       Modes.I.dtd(),
       Modes.I.erlang(),
       Modes.I.gas(),
+      Modes.I.gerrit_commit(),
       Modes.I.gfm(),
       Modes.I.go(),
       Modes.I.groovy(),
@@ -111,6 +113,10 @@
     return real != null ? real : mode;
   }
 
+  public static Collection<String> getKnownMimeTypes() {
+    return mimeModes.keySet();
+  }
+
   private static native boolean isModeLoaded(String n)
   /*-{ return $wnd.CodeMirror.modes.hasOwnProperty(n); }-*/;
 
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java b/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java
index 547c4a1..d51479f 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java
@@ -34,6 +34,7 @@
   @Source("dtd/dtd.js") @DoNotEmbed DataResource dtd();
   @Source("erlang/erlang.js") @DoNotEmbed DataResource erlang();
   @Source("gas/gas.js") @DoNotEmbed DataResource gas();
+  @Source("gerrit/commit.js") @DoNotEmbed DataResource gerrit_commit();
   @Source("gfm/gfm.js") @DoNotEmbed DataResource gfm();
   @Source("go/go.js") @DoNotEmbed DataResource go();
   @Source("groovy/groovy.js") @DoNotEmbed DataResource groovy();
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/mode/gerrit/commit.js b/gerrit-gwtui/src/main/java/net/codemirror/mode/gerrit/commit.js
new file mode 100644
index 0000000..a846e50
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/mode/gerrit/commit.js
@@ -0,0 +1,26 @@
+CodeMirror.defineMode('gerrit_commit', function() {
+  var header = /^(Parent|Author|AuthorDate|Commit|CommitDate):/;
+  var id = /^Change-Id: I[0-9a-f]{40}/;
+  var footer = /^[A-Z][A-Za-z0-9-]+:/;
+  var sha1 = /[0-9a-f]{6,40}/;
+
+  return {
+    token: function(stream) {
+      if (stream.sol()) {
+        if (stream.match(header))
+          return 'keyword';
+        if (stream.match(id) || stream.match(footer))
+          return 'builtin';
+      }
+
+      stream.eatSpace();
+      if (stream.match(sha1))
+        return 'variable-2';
+      if (stream.match(/".*"/))
+        return 'string';
+      stream.next();
+      return null;
+    }
+  };
+});
+CodeMirror.defineMIME('text/x-gerrit-commit-message', 'gerrit_commit');
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/mode/mode_map b/gerrit-gwtui/src/main/java/net/codemirror/mode/mode_map
index 8c8a0cd..2bff364 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/mode/mode_map
+++ b/gerrit-gwtui/src/main/java/net/codemirror/mode/mode_map
@@ -36,6 +36,9 @@
 gas:
 text/x-gas
 
+gerrit_commit:
+text/x-gerrit-commit-message
+
 gfm:
 text/x-github-markdown
 
diff --git a/gerrit-httpd/BUCK b/gerrit-httpd/BUCK
index 6005e4a..4ecc9e0 100644
--- a/gerrit-httpd/BUCK
+++ b/gerrit-httpd/BUCK
@@ -3,7 +3,7 @@
 )
 RESOURCES = glob(['src/main/resources/**/*'])
 
-java_library2(
+java_library(
   name = 'httpd',
   srcs = SRCS,
   resources = RESOURCES,
@@ -34,7 +34,7 @@
     '//lib/log:api',
     '//lib/lucene:core',
   ],
-  compile_deps = ['//lib:servlet-api-3_1'],
+  provided_deps = ['//lib:servlet-api-3_1'],
   visibility = ['PUBLIC'],
 )
 
@@ -53,12 +53,12 @@
     '//gerrit-extension-api:api',
     '//gerrit-reviewdb:server',
     '//gerrit-server:server',
-    '//lib:easymock',
     '//lib:junit',
     '//lib:gson',
     '//lib:gwtorm',
     '//lib:guava',
     '//lib:servlet-api-3_1',
+    '//lib/easymock:easymock',
     '//lib/guice:guice',
     '//lib/jgit:jgit',
     '//lib/jgit:junit',
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
index 9968e2a..d315fa3 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.httpd;
 
 import static java.util.concurrent.TimeUnit.HOURS;
-import static java.util.concurrent.TimeUnit.MINUTES;
 
 import com.google.gerrit.httpd.WebSessionManager.Key;
 import com.google.gerrit.httpd.WebSessionManager.Val;
@@ -26,10 +25,7 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AuthResult;
-import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.config.AuthConfig;
-import com.google.inject.Inject;
-import com.google.inject.Module;
 import com.google.inject.Provider;
 import com.google.inject.servlet.RequestScoped;
 
@@ -42,25 +38,9 @@
 import javax.servlet.http.HttpServletResponse;
 
 @RequestScoped
-public final class CacheBasedWebSession implements WebSession {
+public abstract class CacheBasedWebSession implements WebSession {
   private static final String ACCOUNT_COOKIE = "GerritAccount";
-  static final long MAX_AGE_MINUTES = HOURS.toMinutes(12);
-
-  public static Module module() {
-    return new CacheModule() {
-      @Override
-      protected void configure() {
-        persist(WebSessionManager.CACHE_NAME, String.class, Val.class)
-            .maximumWeight(1024) // reasonable default for many sites
-            .expireAfterWrite(MAX_AGE_MINUTES, MINUTES) // expire sessions if they are inactive
-        ;
-        bind(WebSessionManager.class);
-        bind(WebSession.class)
-          .to(CacheBasedWebSession.class)
-          .in(RequestScoped.class);
-      }
-    };
-  }
+  protected static final long MAX_AGE_MINUTES = HOURS.toMinutes(12);
 
   private final HttpServletRequest request;
   private final HttpServletResponse response;
@@ -75,9 +55,9 @@
   private Val val;
   private CurrentUser user;
 
-  @Inject
-  CacheBasedWebSession(final HttpServletRequest request,
-      final HttpServletResponse response, final WebSessionManager manager,
+  protected CacheBasedWebSession(final HttpServletRequest request,
+      final HttpServletResponse response,
+      final WebSessionManager manager,
       final AuthConfig authConfig,
       final Provider<AnonymousUser> anonymousProvider,
       final IdentifiedUser.RequestFactory identified) {
@@ -88,7 +68,8 @@
     this.anonymousProvider = anonymousProvider;
     this.identified = identified;
 
-    if (!GitSmartHttpTools.isGitClient(request)) {
+    if (request.getRequestURI() == null
+        || !GitSmartHttpTools.isGitClient(request)) {
       String cookie = readCookie();
       if (cookie != null) {
         key = new Key(cookie);
@@ -204,6 +185,10 @@
   }
 
   private void saveCookie() {
+    if (response == null) {
+      return;
+    }
+
     final String token;
     final int ageSeconds;
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ContainerAuthFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ContainerAuthFilter.java
index 433b4f5..91fb6af 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ContainerAuthFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ContainerAuthFilter.java
@@ -17,12 +17,12 @@
 import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
 import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
 
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.lib.Config;
@@ -54,12 +54,12 @@
 class ContainerAuthFilter implements Filter {
   public static final String REALM_NAME = "Gerrit Code Review";
 
-  private final Provider<WebSession> session;
+  private final DynamicItem<WebSession> session;
   private final AccountCache accountCache;
   private final Config config;
 
   @Inject
-  ContainerAuthFilter(Provider<WebSession> session, AccountCache accountCache,
+  ContainerAuthFilter(DynamicItem<WebSession> session, AccountCache accountCache,
       @GerritServerConfig Config config) {
     this.session = session;
     this.accountCache = accountCache;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/DirectChangeByCommit.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/DirectChangeByCommit.java
index 402ea1b..74dc56f 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/DirectChangeByCommit.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/DirectChangeByCommit.java
@@ -3,25 +3,20 @@
 package com.google.gerrit.httpd;
 
 import com.google.common.base.CharMatcher;
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.extensions.api.changes.Changes;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.query.Predicate;
-import com.google.gerrit.server.query.QueryParseException;
-import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.query.change.ChangeDataSource;
-import com.google.gerrit.server.query.change.ChangeQueryBuilder;
-import com.google.gerrit.server.query.change.ChangeQueryRewriter;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
-import java.util.HashSet;
+import java.util.List;
 
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
@@ -33,53 +28,30 @@
   private static final Logger log =
       LoggerFactory.getLogger(DirectChangeByCommit.class);
 
-  private final ChangeQueryBuilder.Factory queryBuilder;
-  private final Provider<ChangeQueryRewriter> queryRewriter;
-  private final Provider<CurrentUser> currentUser;
+  private final Changes changes;
 
   @Inject
-  DirectChangeByCommit(ChangeQueryBuilder.Factory queryBuilder,
-      Provider<ChangeQueryRewriter> queryRewriter,
-      Provider<CurrentUser> currentUser) {
-    this.queryBuilder = queryBuilder;
-    this.queryRewriter = queryRewriter;
-    this.currentUser = currentUser;
+  DirectChangeByCommit(Changes changes) {
+    this.changes = changes;
   }
 
   @Override
   protected void doGet(final HttpServletRequest req,
       final HttpServletResponse rsp) throws IOException {
     String query = CharMatcher.is('/').trimTrailingFrom(req.getPathInfo());
-    HashSet<Change.Id> ids = new HashSet<Change.Id>();
+    List<ChangeInfo> results;
     try {
-      ChangeQueryBuilder builder = queryBuilder.create(currentUser.get());
-      Predicate<ChangeData> visibleToMe = builder.is_visible();
-      Predicate<ChangeData> q = builder.parse(query);
-      q = Predicate.and(q, builder.sortkey_before("z"), builder.limit(2), visibleToMe);
-
-      ChangeQueryRewriter rewriter = queryRewriter.get();
-      Predicate<ChangeData> s = rewriter.rewrite(q, 0);
-      if (!(s instanceof ChangeDataSource)) {
-        s = rewriter.rewrite(Predicate.and(builder.status_open(), q), 0);
-      }
-
-      if (s instanceof ChangeDataSource) {
-        for (ChangeData d : ((ChangeDataSource) s).read()) {
-          ids.add(d.getId());
-        }
-      }
-    } catch (QueryParseException e) {
-      log.info("Received invalid query by URL: /r/" + query);
-    } catch (OrmException e) {
+      results = changes.query(query).withLimit(2).get();
+    } catch (RestApiException e) {
       log.warn("Cannot process query by URL: /r/" + query, e);
+      results = ImmutableList.of();
     }
-
     String token;
-    if (ids.size() == 1) {
+    if (results.size() == 1) {
       // If exactly one change matches, link to that change.
       // TODO Link to a specific patch set, if one matched.
-      token = PageLinks.toChange(ids.iterator().next());
-
+      token = PageLinks.toChange(
+          new Change.Id(results.iterator().next()._number));
     } else {
       // Otherwise, link to the query page.
       token = PageLinks.toChangeQuery(query);
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 7f6a2df..f41f67c 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java
@@ -14,11 +14,16 @@
 
 package com.google.gerrit.httpd;
 
+import com.google.common.base.Function;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.GerritConfig;
 import com.google.gerrit.common.data.GitwebConfig;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
 import com.google.gerrit.server.account.Realm;
+import com.google.gerrit.server.change.ArchiveFormat;
+import com.google.gerrit.server.change.GetArchive;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.AuthConfig;
@@ -46,6 +51,7 @@
   private final Config cfg;
   private final AuthConfig authConfig;
   private final DownloadConfig downloadConfig;
+  private final GetArchive.AllowedFormats archiveFormats;
   private final GitWebConfig gitWebConfig;
   private final AllProjectsName wildProject;
   private final SshInfo sshInfo;
@@ -60,11 +66,13 @@
       final AuthConfig ac, final GitWebConfig gwc, final AllProjectsName wp,
       final SshInfo si, final ContactStore cs,
       final ServletContext sc, final DownloadConfig dc,
+      final GetArchive.AllowedFormats af,
       final @AnonymousCowardName String acn) {
     realm = r;
     cfg = gsc;
     authConfig = ac;
     downloadConfig = dc;
+    archiveFormats = af;
     gitWebConfig = gwc;
     sshInfo = si;
     wildProject = wp;
@@ -86,6 +94,7 @@
         config.setRegisterUrl(cfg.getString("auth", null, "registerurl"));
         config.setRegisterText(cfg.getString("auth", null, "registertext"));
         config.setEditFullNameUrl(cfg.getString("auth", null, "editFullNameUrl"));
+        config.setHttpPasswordSettingsEnabled(!authConfig.isGitBasicAuth());
         break;
 
       case CUSTOM_EXTENSION:
@@ -103,6 +112,7 @@
 
       case CLIENT_SSL_CERT_LDAP:
       case DEVELOPMENT_BECOME_ANY_ACCOUNT:
+      case OAUTH:
       case OPENID:
       case OPENID_SSO:
         break;
@@ -127,6 +137,15 @@
         "gerrit", null, "changeScreen",
         AccountGeneralPreferences.ChangeScreen.CHANGE_SCREEN2));
     config.setLargeChangeSize(cfg.getInt("change", "largeChange", 500));
+    config.setArchiveFormats(Lists.newArrayList(Iterables.transform(
+        archiveFormats.getAllowed(),
+        new Function<ArchiveFormat, String>() {
+          @Override
+          public String apply(ArchiveFormat in) {
+            return in.getShortName();
+          }
+        })));
+
     config.setNewFeatures(cfg.getBoolean("gerrit", "enableNewFeatures", true));
 
     final String reportBugUrl = cfg.getString("gerrit", null, "reportBugUrl");
@@ -134,9 +153,7 @@
         reportBugUrl : "http://code.google.com/p/gerrit/issues/list");
     config.setReportBugText(cfg.getString("gerrit", null, "reportBugText"));
 
-    config.setGitBasicAuth(authConfig.isGitBasicAuth());
-
-    final Set<Account.FieldName> fields = new HashSet<Account.FieldName>();
+    final Set<Account.FieldName> fields = new HashSet<>();
     for (final Account.FieldName n : Account.FieldName.values()) {
       if (realm.allowsEdit(n)) {
         fields.add(n);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java
index 4f4c783..edd2594 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java
@@ -14,7 +14,9 @@
 
 package com.google.gerrit.httpd;
 
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
 import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.server.config.DownloadConfig;
 import com.google.inject.Inject;
 import com.google.inject.servlet.ServletModule;
 
@@ -23,10 +25,13 @@
 /** Configures Git access over HTTP with authentication. */
 public class GitOverHttpModule extends ServletModule {
   private final AuthConfig authConfig;
+  private final DownloadConfig downloadConfig;
 
   @Inject
-  GitOverHttpModule(AuthConfig authConfig) {
+  GitOverHttpModule(AuthConfig authConfig,
+      DownloadConfig downloadConfig) {
     this.authConfig = authConfig;
+    this.downloadConfig = downloadConfig;
   }
 
   @Override
@@ -40,10 +45,18 @@
       authFilter = ProjectDigestFilter.class;
     }
 
-    String git = GitOverHttpServlet.URL_REGEX;
-    filterRegex(git).through(authFilter);
-    serveRegex(git).with(GitOverHttpServlet.class);
+    if (isHttpEnabled()) {
+      String git = GitOverHttpServlet.URL_REGEX;
+      filterRegex(git).through(authFilter);
+      serveRegex(git).with(GitOverHttpServlet.class);
+    }
 
     filter("/a/*").through(authFilter);
   }
+
+  private boolean isHttpEnabled(){
+    return downloadConfig.getDownloadSchemes().contains(DownloadScheme.DEFAULT_DOWNLOADS)
+        || downloadConfig.getDownloadSchemes().contains(DownloadScheme.ANON_HTTP)
+        || downloadConfig.getDownloadSchemes().contains(DownloadScheme.HTTP);
+  }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
index 03d54e4..0c4f60c 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
@@ -33,6 +33,7 @@
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.git.TransferConfig;
 import com.google.gerrit.server.git.VisibleRefFilter;
+import com.google.gerrit.server.git.validators.UploadValidators;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.inject.AbstractModule;
@@ -51,6 +52,8 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.transport.PostReceiveHook;
 import org.eclipse.jgit.transport.PostReceiveHookChain;
+import org.eclipse.jgit.transport.PreUploadHook;
+import org.eclipse.jgit.transport.PreUploadHookChain;
 import org.eclipse.jgit.transport.ReceivePack;
 import org.eclipse.jgit.transport.UploadPack;
 import org.eclipse.jgit.transport.resolver.ReceivePackFactory;
@@ -188,10 +191,12 @@
 
   static class UploadFactory implements UploadPackFactory<HttpServletRequest> {
     private final TransferConfig config;
+    private final DynamicSet<PreUploadHook> preUploadHooks;
 
     @Inject
-    UploadFactory(TransferConfig tc) {
+    UploadFactory(TransferConfig tc, DynamicSet<PreUploadHook> preUploadHooks) {
       this.config = tc;
+      this.preUploadHooks = preUploadHooks;
     }
 
     @Override
@@ -199,6 +204,8 @@
       UploadPack up = new UploadPack(repo);
       up.setPackConfig(config.getPackConfig());
       up.setTimeout(config.getTimeout());
+      up.setPreUploadHook(PreUploadHookChain.newChain(
+          Lists.newArrayList(preUploadHooks)));
       return up;
     }
   }
@@ -207,12 +214,15 @@
     private final Provider<ReviewDb> db;
     private final TagCache tagCache;
     private final ChangeCache changeCache;
+    private final UploadValidators.Factory uploadValidatorsFactory;
 
     @Inject
-    UploadFilter(Provider<ReviewDb> db, TagCache tagCache, ChangeCache changeCache) {
+    UploadFilter(Provider<ReviewDb> db, TagCache tagCache, ChangeCache changeCache,
+        UploadValidators.Factory uploadValidatorsFactory) {
       this.db = db;
       this.tagCache = tagCache;
       this.changeCache = changeCache;
+      this.uploadValidatorsFactory = uploadValidatorsFactory;
     }
 
     @Override
@@ -229,9 +239,15 @@
             "upload-pack not permitted on this server");
         return;
       }
-
+      // We use getRemoteHost() here instead of getRemoteAddr() because REMOTE_ADDR
+      // may have been overridden by a proxy server -- we'll try to avoid this.
+      UploadValidators uploadValidators =
+          uploadValidatorsFactory.create(pc.getProject(), repo, request.getRemoteHost());
+      up.setPreUploadHook(PreUploadHookChain.newChain(
+          Lists.newArrayList(up.getPreUploadHook(), uploadValidators)));
       if (!pc.allRefsAreVisible()) {
-        up.setAdvertiseRefsHook(new VisibleRefFilter(tagCache, changeCache, repo, pc, db.get(), true));
+        up.setAdvertiseRefsHook(new VisibleRefFilter(tagCache, changeCache,
+            repo, pc, db.get(), true));
       }
 
       next.doFilter(request, response);
@@ -356,7 +372,7 @@
 
       if (isGet) {
         cache.put(cacheKey, Collections.unmodifiableSet(
-            new HashSet<ObjectId>(rp.getAdvertisedObjects())));
+            new HashSet<>(rp.getAdvertisedObjects())));
       }
     }
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/H2CacheBasedWebSession.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/H2CacheBasedWebSession.java
new file mode 100644
index 0000000..4b68f02
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/H2CacheBasedWebSession.java
@@ -0,0 +1,69 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd;
+
+import static java.util.concurrent.TimeUnit.MINUTES;
+
+import com.google.common.cache.Cache;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.httpd.WebSessionManager.Val;
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.IdentifiedUser.RequestFactory;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Provider;
+import com.google.inject.assistedinject.FactoryModuleBuilder;
+import com.google.inject.name.Named;
+import com.google.inject.servlet.RequestScoped;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@RequestScoped
+public class H2CacheBasedWebSession extends CacheBasedWebSession {
+  public static Module module() {
+    return new CacheModule() {
+      @Override
+      protected void configure() {
+        persist(WebSessionManager.CACHE_NAME, String.class, Val.class)
+            .maximumWeight(1024) // reasonable default for many sites
+            .expireAfterWrite(CacheBasedWebSession.MAX_AGE_MINUTES, MINUTES) // expire sessions if they are inactive
+        ;
+        install(new FactoryModuleBuilder()
+            .build(WebSessionManagerFactory.class));
+        DynamicItem.itemOf(binder(), WebSession.class);
+        DynamicItem.bind(binder(), WebSession.class)
+          .to(H2CacheBasedWebSession.class)
+          .in(RequestScoped.class);
+      }
+    };
+  }
+
+  @Inject
+  H2CacheBasedWebSession(
+      HttpServletRequest request,
+      @Nullable HttpServletResponse response,
+      WebSessionManagerFactory managerFactory,
+      @Named(WebSessionManager.CACHE_NAME) Cache<String, Val> cache,
+      AuthConfig authConfig,
+      Provider<AnonymousUser> anonymousProvider,
+      RequestFactory identified) {
+    super(request, response, managerFactory.create(cache), authConfig,
+        anonymousProvider, identified);
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpLogoutServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpLogoutServlet.java
index ddbba3b..b2228a9 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpLogoutServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpLogoutServlet.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.audit.AuditEvent;
 import com.google.gerrit.audit.AuditService;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.config.AuthConfig;
@@ -34,17 +35,17 @@
 import javax.servlet.http.HttpServletResponse;
 
 @Singleton
-class HttpLogoutServlet extends HttpServlet {
+public class HttpLogoutServlet extends HttpServlet {
   private static final long serialVersionUID = 1L;
 
-  private final Provider<WebSession> webSession;
+  private final DynamicItem<WebSession> webSession;
   private final Provider<String> urlProvider;
   private final String logoutUrl;
   private final AuditService audit;
 
   @Inject
-  HttpLogoutServlet(final AuthConfig authConfig,
-      final Provider<WebSession> webSession,
+  protected HttpLogoutServlet(final AuthConfig authConfig,
+      final DynamicItem<WebSession> webSession,
       @CanonicalWebUrl @Nullable final Provider<String> urlProvider,
       final AccountManager accountManager,
       final AuditService audit) {
@@ -54,7 +55,7 @@
     this.audit = audit;
   }
 
-  private void doLogout(final HttpServletRequest req,
+  protected void doLogout(final HttpServletRequest req,
       final HttpServletResponse rsp) throws IOException {
     webSession.get().logout();
     if (logoutUrl != null) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpRequestContext.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpRequestContext.java
index 2db3534..47593aa 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpRequestContext.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpRequestContext.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.httpd;
 
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.RequestScopedReviewDbProvider;
@@ -22,11 +23,11 @@
 import com.google.inject.Provider;
 
 class HttpRequestContext implements RequestContext {
-  private final WebSession session;
+  private final DynamicItem<WebSession> session;
   private final RequestScopedReviewDbProvider provider;
 
   @Inject
-  HttpRequestContext(WebSession session,
+  HttpRequestContext(DynamicItem<WebSession> session,
       RequestScopedReviewDbProvider provider) {
     this.session = session;
     this.provider = provider;
@@ -34,7 +35,7 @@
 
   @Override
   public CurrentUser getCurrentUser() {
-    return session.getCurrentUser();
+    return session.get().getCurrentUser();
   }
 
   @Override
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/LoginUrlToken.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/LoginUrlToken.java
new file mode 100644
index 0000000..044c18c
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/LoginUrlToken.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Strings;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.extensions.restapi.Url;
+
+import javax.servlet.http.HttpServletRequest;
+
+public class LoginUrlToken {
+  private static final String DEFAULT_TOKEN = '#' + PageLinks.MINE;
+
+  public static String getToken(final HttpServletRequest req){
+    String encodedToken = req.getPathInfo();
+    if (Strings.isNullOrEmpty(encodedToken)) {
+      return DEFAULT_TOKEN;
+    } else {
+      return CharMatcher.is('/').trimLeadingFrom(Url.decode(encodedToken));
+    }
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
index 0e41ef7..b9b5731 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
@@ -18,6 +18,7 @@
 
 import com.google.common.base.Objects;
 import com.google.common.base.Strings;
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountException;
@@ -28,7 +29,6 @@
 import com.google.gerrit.server.auth.NoSuchUserException;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 import org.apache.commons.codec.binary.Base64;
@@ -68,13 +68,13 @@
   private static final String AUTHORIZATION = "Authorization";
   private static final String LIT_BASIC = "Basic ";
 
-  private final Provider<WebSession> session;
+  private final DynamicItem<WebSession> session;
   private final AccountCache accountCache;
   private final AccountManager accountManager;
   private final AuthConfig authConfig;
 
   @Inject
-  ProjectBasicAuthFilter(Provider<WebSession> session,
+  ProjectBasicAuthFilter(DynamicItem<WebSession> session,
       AccountCache accountCache, AccountManager accountManager,
       AuthConfig authConfig) {
     this.session = session;
@@ -138,6 +138,14 @@
       return false;
     }
 
+    if (!authConfig.isLdapAuthType()
+        && !passwordMatchesTheUserGeneratedOne(who, username, password)) {
+      log.warn("Authentication failed for " + username
+          + ": password does not match the one stored in Gerrit");
+      rsp.sendError(SC_UNAUTHORIZED);
+      return false;
+    }
+
     AuthRequest whoAuth = AuthRequest.forUser(username);
     whoAuth.setPassword(password);
 
@@ -167,6 +175,13 @@
     }
   }
 
+  private boolean passwordMatchesTheUserGeneratedOne(AccountState who,
+      String username, String password) {
+    String accountPassword = who.getPassword(username);
+    return accountPassword != null && password != null
+        && accountPassword.equals(password);
+  }
+
   private String encoding(HttpServletRequest req) {
     return Objects.firstNonNull(req.getCharacterEncoding(), "UTF-8");
   }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java
index 45e4504..12de344 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java
@@ -21,6 +21,7 @@
 import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
 
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
@@ -70,7 +71,7 @@
   private static final String AUTHORIZATION = "Authorization";
 
   private final Provider<String> urlProvider;
-  private final Provider<WebSession> session;
+  private final DynamicItem<WebSession> session;
   private final AccountCache accountCache;
   private final Config config;
   private final SignedToken tokens;
@@ -78,7 +79,7 @@
 
   @Inject
   ProjectDigestFilter(@CanonicalWebUrl @Nullable Provider<String> urlProvider,
-      Provider<WebSession> session, AccountCache accountCache,
+      DynamicItem<WebSession> session, AccountCache accountCache,
       @GerritServerConfig Config config) throws XsrfException {
     this.urlProvider = urlProvider;
     this.session = session;
@@ -231,7 +232,7 @@
   }
 
   private Map<String, String> parseAuthorization(String auth) {
-    Map<String, String> p = new HashMap<String, String>();
+    Map<String, String> p = new HashMap<>();
     int next = "Digest ".length();
     while (next < auth.length()) {
       if (next < auth.length() && auth.charAt(next) == ',') {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
index e0bef35..3418354 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
@@ -17,6 +17,7 @@
 import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
 import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
 
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.httpd.restapi.RestApiServlet;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
@@ -24,7 +25,6 @@
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import com.google.inject.servlet.ServletModule;
 
@@ -56,12 +56,12 @@
   }
 
   private final boolean enabled;
-  private final Provider<WebSession> session;
+  private final DynamicItem<WebSession> session;
   private final AccountResolver accountResolver;
 
   @Inject
   RunAsFilter(AuthConfig config,
-      Provider<WebSession> session,
+      DynamicItem<WebSession> session,
       AccountResolver accountResolver) {
     this.enabled = config.isRunAsEnabled();
     this.session = session;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
index 3c4dfc5..031e3a2 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
@@ -33,8 +33,10 @@
 import com.google.gerrit.httpd.rpc.doc.QueryDocumentationFilter;
 import com.google.gerrit.httpd.rpc.group.GroupsRestApiServlet;
 import com.google.gerrit.httpd.rpc.project.ProjectsRestApiServlet;
+import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gwtexpui.server.CacheControlFilter;
 import com.google.inject.Inject;
@@ -64,10 +66,12 @@
 
   private final UrlConfig cfg;
   private GerritUiOptions uiOptions;
+  private AuthConfig authConfig;
 
-  UrlModule(UrlConfig cfg, GerritUiOptions uiOptions) {
+  UrlModule(UrlConfig cfg, GerritUiOptions uiOptions, AuthConfig authConfig) {
     this.cfg = cfg;
     this.uiOptions = uiOptions;
+    this.authConfig = authConfig;
   }
 
   @Override
@@ -81,8 +85,12 @@
       serve("/Gerrit/*").with(legacyGerritScreen());
     }
     serve("/cat/*").with(CatServlet.class);
-    serve("/logout").with(HttpLogoutServlet.class);
-    serve("/signout").with(HttpLogoutServlet.class);
+
+    if (authConfig.getAuthType() != AuthType.OAUTH &&
+        authConfig.getAuthType() != AuthType.OPENID) {
+      serve("/logout").with(HttpLogoutServlet.class);
+      serve("/signout").with(HttpLogoutServlet.class);
+    }
     serve("/ssh_info").with(SshInfoServlet.class);
     serve("/static/*").with(StaticServlet.class);
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
index 3443968..76e1e41 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
@@ -101,6 +101,8 @@
         install(new BecomeAnyAccountModule());
         break;
 
+      case OAUTH:
+        // OAuth support is bound in WebAppInitializer and Daemon.
       case OPENID:
       case OPENID_SSO:
         // OpenID support is bound in WebAppInitializer and Daemon.
@@ -110,7 +112,7 @@
         throw new ProvisionException("Unsupported loginType: " + authConfig.getAuthType());
     }
 
-    install(new UrlModule(urlConfig, uiOptions));
+    install(new UrlModule(urlConfig, uiOptions, authConfig));
     install(new UiRpcModule());
     install(new GerritRequestModule());
     install(new GitOverHttpServlet.Module());
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java
index 03eca9f..24e2c56 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java
@@ -34,8 +34,7 @@
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.Inject;
-import com.google.inject.Singleton;
-import com.google.inject.name.Named;
+import com.google.inject.assistedinject.Assisted;
 
 import org.eclipse.jgit.lib.Config;
 import org.slf4j.Logger;
@@ -49,10 +48,9 @@
 import java.security.SecureRandom;
 import java.util.concurrent.TimeUnit;
 
-@Singleton
-class WebSessionManager {
+public class WebSessionManager {
   private static final Logger log = LoggerFactory.getLogger(WebSessionManager.class);
-  static final String CACHE_NAME = "web_sessions";
+  public static final String CACHE_NAME = "web_sessions";
 
   private final long sessionMaxAgeMillis;
   private final SecureRandom prng;
@@ -60,7 +58,7 @@
 
   @Inject
   WebSessionManager(@GerritServerConfig Config cfg,
-      @Named(CACHE_NAME) final Cache<String, Val> cache) {
+      @Assisted final Cache<String, Val> cache) {
     prng = new SecureRandom();
     self = cache;
 
@@ -180,7 +178,7 @@
     }
   }
 
-  static final class Val implements Serializable {
+  public static final class Val implements Serializable {
     static final long serialVersionUID = 2L;
 
     private transient Account.Id accountId;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_75.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManagerFactory.java
similarity index 72%
rename from gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_75.java
rename to gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManagerFactory.java
index a847f25..d617e1b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_75.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManagerFactory.java
@@ -12,14 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.schema;
+package com.google.gerrit.httpd;
 
-import com.google.inject.Inject;
-import com.google.inject.Provider;
+import com.google.common.cache.Cache;
+import com.google.gerrit.httpd.WebSessionManager.Val;
 
-public class Schema_75 extends SchemaVersion {
-  @Inject
-  Schema_75(Provider<Schema_74> prior) {
-    super(prior);
-  }
+public interface WebSessionManagerFactory {
+  WebSessionManager create(Cache<String, Val> cache);
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
index 2dde16e..8884b4d 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
@@ -16,10 +16,10 @@
 
 import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME;
 
-import com.google.common.base.Objects;
-import com.google.common.base.Strings;
 import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.httpd.HtmlDomUtil;
+import com.google.gerrit.httpd.LoginUrlToken;
 import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.httpd.template.SiteHeaderFooter;
 import com.google.gerrit.reviewdb.client.Account;
@@ -34,7 +34,6 @@
 import com.google.gwtorm.server.ResultSet;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 import org.w3c.dom.Document;
@@ -59,12 +58,12 @@
   private static final boolean IS_DEV = Boolean.getBoolean("Gerrit.GwtDevMode");
 
   private final SchemaFactory<ReviewDb> schema;
-  private final Provider<WebSession> webSession;
+  private final DynamicItem<WebSession> webSession;
   private final AccountManager accountManager;
   private final SiteHeaderFooter headers;
 
   @Inject
-  BecomeAnyAccountLoginServlet(final Provider<WebSession> ws,
+  BecomeAnyAccountLoginServlet(final DynamicItem<WebSession> ws,
       final SchemaFactory<ReviewDb> sf,
       final AccountManager am,
       final ServletContext servletContext,
@@ -121,9 +120,8 @@
     if (res != null) {
       webSession.get().login(res, false);
       final StringBuilder rdr = new StringBuilder();
-      rdr.append(Objects.firstNonNull(
-          Strings.emptyToNull(req.getContextPath()),
-          "/"));
+      rdr.append(req.getContextPath());
+      rdr.append("/");
       if (IS_DEV && req.getParameter("gwt.codesvr") != null) {
         if (rdr.indexOf("?") < 0) {
           rdr.append("?");
@@ -132,11 +130,12 @@
         }
         rdr.append("gwt.codesvr=").append(req.getParameter("gwt.codesvr"));
       }
-      rdr.append('#');
+
       if (res.isNew()) {
-        rdr.append(PageLinks.REGISTER);
+        rdr.append('#' + PageLinks.REGISTER);
+      } else {
+        rdr.append(LoginUrlToken.getToken(req));
       }
-      rdr.append(PageLinks.MINE);
       rsp.sendRedirect(rdr.toString());
 
     } else {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
index eb6d1f7..b737cd7 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
@@ -19,6 +19,7 @@
 import static com.google.common.net.HttpHeaders.AUTHORIZATION;
 import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_GERRIT;
 
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.httpd.HtmlDomUtil;
 import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.httpd.raw.HostPageServlet;
@@ -27,7 +28,6 @@
 import com.google.gwtexpui.server.CacheHeaders;
 import com.google.gwtjsonrpc.server.RPCServletUtils;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.util.Base64;
@@ -57,7 +57,7 @@
  */
 @Singleton
 class HttpAuthFilter implements Filter {
-  private final Provider<WebSession> sessionProvider;
+  private final DynamicItem<WebSession> sessionProvider;
   private final byte[] signInRaw;
   private final byte[] signInGzip;
   private final String loginHeader;
@@ -65,7 +65,7 @@
   private final String emailHeader;
 
   @Inject
-  HttpAuthFilter(final Provider<WebSession> webSession,
+  HttpAuthFilter(final DynamicItem<WebSession> webSession,
       final AuthConfig authConfig) throws IOException {
     this.sessionProvider = webSession;
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
index e62fde5..734addb 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
@@ -15,8 +15,10 @@
 package com.google.gerrit.httpd.auth.container;
 
 import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.httpd.CanonicalWebUrl;
 import com.google.gerrit.httpd.HtmlDomUtil;
+import com.google.gerrit.httpd.LoginUrlToken;
 import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
@@ -25,7 +27,6 @@
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gwtexpui.server.CacheHeaders;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 import org.slf4j.Logger;
@@ -56,14 +57,14 @@
   private static final Logger log =
       LoggerFactory.getLogger(HttpLoginServlet.class);
 
-  private final Provider<WebSession> webSession;
+  private final DynamicItem<WebSession> webSession;
   private final CanonicalWebUrl urlProvider;
   private final AccountManager accountManager;
   private final HttpAuthFilter authFilter;
   private final AuthConfig authConfig;
 
   @Inject
-  HttpLoginServlet(final Provider<WebSession> webSession,
+  HttpLoginServlet(final DynamicItem<WebSession> webSession,
       final CanonicalWebUrl urlProvider,
       final AccountManager accountManager,
       final HttpAuthFilter authFilter,
@@ -78,11 +79,7 @@
   @Override
   protected void doGet(final HttpServletRequest req,
       final HttpServletResponse rsp) throws ServletException, IOException {
-    final String token = getToken(req);
-    if ("/logout".equals(token) || "/signout".equals(token)) {
-      req.getRequestDispatcher("/logout").forward(req, rsp);
-      return;
-    }
+    final String token = LoginUrlToken.getToken(req);
 
     CacheHeaders.setNotCacheable(rsp);
     final String user = authFilter.getRemoteUser(req);
@@ -130,9 +127,8 @@
       rdr.append(authConfig.getRegisterPageUrl());
     } else {
       rdr.append(urlProvider.get(req));
-      rdr.append('#');
       if (arsp.isNew() && !token.startsWith(PageLinks.REGISTER + "/")) {
-        rdr.append(PageLinks.REGISTER);
+        rdr.append('#' + PageLinks.REGISTER);
       }
       rdr.append(token);
     }
@@ -163,14 +159,4 @@
       replaceByClass(n, name, value);
     }
   }
-
-  private String getToken(final HttpServletRequest req) {
-    String token = req.getPathInfo();
-    if (token == null || token.isEmpty()) {
-      token = PageLinks.MINE;
-    } else if (!token.startsWith("/")) {
-      token = "/" + token;
-    }
-    return token;
-  }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertAuthFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertAuthFilter.java
index c892c9d..ea9b1c0 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertAuthFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertAuthFilter.java
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.httpd.auth.container;
 
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.AuthResult;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 import org.slf4j.Logger;
@@ -45,11 +45,11 @@
   private static final Logger log =
     LoggerFactory.getLogger(HttpsClientSslCertAuthFilter.class);
 
-  private final Provider<WebSession> webSession;
+  private final DynamicItem<WebSession> webSession;
   private final AccountManager accountManager;
 
   @Inject
-  HttpsClientSslCertAuthFilter(final Provider<WebSession> webSession,
+  HttpsClientSslCertAuthFilter(final DynamicItem<WebSession> webSession,
       final AccountManager accountManager) {
     this.webSession = webSession;
     this.accountManager = accountManager;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertLoginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertLoginServlet.java
index 3c3174d..8652ef0 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertLoginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertLoginServlet.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.httpd.auth.container;
 
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.httpd.LoginUrlToken;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gwtexpui.server.CacheHeaders;
 import com.google.inject.Inject;
@@ -53,18 +53,9 @@
       final HttpServletResponse rsp) throws IOException {
     final StringBuilder rdr = new StringBuilder();
     rdr.append(urlProvider.get());
-    rdr.append('#');
-    rdr.append(getToken(req));
+    rdr.append(LoginUrlToken.getToken(req));
 
     CacheHeaders.setNotCacheable(rsp);
     rsp.sendRedirect(rdr.toString());
   }
-
-  private String getToken(final HttpServletRequest req) {
-    String token = req.getPathInfo();
-    if (token == null || token.isEmpty()) {
-      token = PageLinks.MINE;
-    }
-    return token;
-  }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/LdapLoginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/LdapLoginServlet.java
index 24dd5bf..2deb2eb 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/LdapLoginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/LdapLoginServlet.java
@@ -17,9 +17,10 @@
 import com.google.common.base.Objects;
 import com.google.common.base.Strings;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.httpd.CanonicalWebUrl;
 import com.google.gerrit.httpd.HtmlDomUtil;
+import com.google.gerrit.httpd.LoginUrlToken;
 import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.httpd.template.SiteHeaderFooter;
 import com.google.gerrit.server.account.AccountException;
@@ -30,7 +31,6 @@
 import com.google.gerrit.server.auth.AuthenticationUnavailableException;
 import com.google.gwtexpui.server.CacheHeaders;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 import org.slf4j.Logger;
@@ -54,13 +54,13 @@
       .getLogger(LdapLoginServlet.class);
 
   private final AccountManager accountManager;
-  private final Provider<WebSession> webSession;
+  private final DynamicItem<WebSession> webSession;
   private final CanonicalWebUrl urlProvider;
   private final SiteHeaderFooter headers;
 
   @Inject
   LdapLoginServlet(AccountManager accountManager,
-      Provider<WebSession> webSession,
+      DynamicItem<WebSession> webSession,
       CanonicalWebUrl urlProvider,
       SiteHeaderFooter headers) {
     this.accountManager = accountManager;
@@ -73,10 +73,7 @@
       @Nullable String errorMessage) throws IOException {
     String self = req.getRequestURI();
     String cancel = Objects.firstNonNull(urlProvider.get(req), "/");
-    String token = getToken(req);
-    if (!token.equals("/")) {
-      cancel += "#" + token;
-    }
+    cancel += LoginUrlToken.getToken(req);
 
     Document doc = headers.parse(LdapLoginServlet.class, "LoginForm.html");
     HtmlDomUtil.find(doc, "hostName").setTextContent(req.getServerName());
@@ -144,21 +141,10 @@
 
     StringBuilder dest = new StringBuilder();
     dest.append(urlProvider.get(req));
-    dest.append('#');
-    dest.append(getToken(req));
+    dest.append(LoginUrlToken.getToken(req));
 
     CacheHeaders.setNotCacheable(res);
     webSession.get().login(ares, "1".equals(remember));
     res.sendRedirect(dest.toString());
   }
-
-  private static String getToken(final HttpServletRequest req) {
-    String token = req.getPathInfo();
-    if (token == null || token.isEmpty()) {
-      token = PageLinks.MINE;
-    } else if (!token.startsWith("/")) {
-      token = "/" + token;
-    }
-    return token;
-  }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java
index c2b0cc8..3b4c998 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java
@@ -29,11 +29,13 @@
 
 package com.google.gerrit.httpd.gitweb;
 
+import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.GerritConfig;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.httpd.GitWebConfig;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.LocalDiskRepositoryManager;
@@ -78,6 +80,8 @@
   private static final Logger log =
       LoggerFactory.getLogger(GitWebServlet.class);
 
+  private static final String PROJECT_LIST_ACTION = "project_list";
+
   private final Set<String> deniedActions;
   private final int bufferSize = 8192;
   private final File gitwebCgi;
@@ -85,20 +89,23 @@
   private final LocalDiskRepositoryManager repoManager;
   private final ProjectControl.Factory projectControl;
   private final Provider<AnonymousUser> anonymousUserProvider;
+  private final Provider<CurrentUser> userProvider;
   private final EnvList _env;
 
   @Inject
   GitWebServlet(final LocalDiskRepositoryManager repoManager,
       final ProjectControl.Factory projectControl,
       final Provider<AnonymousUser> anonymousUserProvider,
+      final Provider<CurrentUser> userProvider,
       final SitePaths site,
       final GerritConfig gerritConfig, final GitWebConfig gitWebConfig)
       throws IOException {
     this.repoManager = repoManager;
     this.projectControl = projectControl;
     this.anonymousUserProvider = anonymousUserProvider;
+    this.userProvider = userProvider;
     this.gitwebCgi = gitWebConfig.getGitwebCGI();
-    this.deniedActions = new HashSet<String>();
+    this.deniedActions = new HashSet<>();
 
     final String url = gitWebConfig.getUrl();
     if ((url != null) && (!url.equals("gitweb"))) {
@@ -115,7 +122,6 @@
 
     deniedActions.add("forks");
     deniedActions.add("opml");
-    deniedActions.add("project_list");
     deniedActions.add("project_index");
 
     _env = new EnvList();
@@ -354,9 +360,18 @@
     }
 
     final Map<String, String> params = getParameters(req);
-    if (deniedActions.contains(params.get("a"))) {
-      rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
-      return;
+    String a = params.get("a");
+    if (a != null) {
+      if (deniedActions.contains(a)) {
+        rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
+        return;
+      }
+
+      if (a.equals(PROJECT_LIST_ACTION)) {
+        rsp.sendRedirect(req.getContextPath() + "/#" + PageLinks.ADMIN_PROJECTS
+            + "?filter=" + Url.encode(params.get("pf") + "/"));
+        return;
+      }
     }
 
     String name = params.get("p");
@@ -377,7 +392,14 @@
         throw new NoSuchProjectException(nameKey);
       }
     } catch (NoSuchProjectException e) {
-      rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
+      if (userProvider.get().isIdentifiedUser()) {
+        rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
+      } else {
+        // Allow anonymous users a chance to login.
+        // Avoid leaking information by not distinguishing between
+        // project not existing and no access rights.
+        rsp.sendRedirect(getLoginRedirectUrl(req));
+      }
       return;
     }
 
@@ -397,8 +419,23 @@
     }
   }
 
+  private static String getLoginRedirectUrl(HttpServletRequest req) {
+    String contextPath = req.getContextPath();
+    String loginUrl = contextPath + "/login/";
+    String token = req.getRequestURI();
+    if (!contextPath.isEmpty()) {
+      token = token.substring(contextPath.length());
+    }
+
+    String queryString = req.getQueryString();
+    if (queryString != null && !queryString.isEmpty()) {
+      token = token.concat("?" + queryString);
+    }
+    return (loginUrl + Url.encode(token));
+  }
+
   private static Map<String, String> getParameters(HttpServletRequest req) {
-    final Map<String, String> params = new HashMap<String, String>();
+    final Map<String, String> params = new HashMap<>();
     for (final String pair : req.getQueryString().split("[&;]")) {
       final int eq = pair.indexOf('=');
       if (0 < eq) {
@@ -669,11 +706,11 @@
     private Map<String, String> envMap;
 
     EnvList() {
-      envMap = new HashMap<String, String>();
+      envMap = new HashMap<>();
     }
 
     EnvList(final EnvList l) {
-      envMap = new HashMap<String, String>(l.envMap);
+      envMap = new HashMap<>(l.envMap);
     }
 
     /** Set a name/value pair, null values will be treated as an empty String */
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
index 5fc632d..5886636 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
@@ -14,12 +14,17 @@
 
 package com.google.gerrit.httpd.plugins;
 
+import static com.google.gerrit.server.plugins.PluginEntry.ATTR_CHARACTER_ENCODING;
+import static com.google.gerrit.server.plugins.PluginEntry.ATTR_CONTENT_TYPE;
+
 import com.google.common.base.CharMatcher;
+import com.google.common.base.Optional;
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
 import com.google.common.cache.Cache;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.common.net.HttpHeaders;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.httpd.restapi.RestApiServlet;
 import com.google.gerrit.server.MimeUtilFileTypeRegistry;
@@ -27,6 +32,9 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.documentation.MarkdownFormatter;
 import com.google.gerrit.server.plugins.Plugin;
+import com.google.gerrit.server.plugins.Plugin.ApiType;
+import com.google.gerrit.server.plugins.PluginContentScanner;
+import com.google.gerrit.server.plugins.PluginEntry;
 import com.google.gerrit.server.plugins.PluginsCollection;
 import com.google.gerrit.server.plugins.ReloadPluginListener;
 import com.google.gerrit.server.plugins.StartPluginListener;
@@ -54,14 +62,11 @@
 import java.io.UnsupportedEncodingException;
 import java.nio.charset.Charset;
 import java.util.Collections;
-import java.util.Comparator;
 import java.util.Enumeration;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentMap;
 import java.util.jar.Attributes;
-import java.util.jar.JarEntry;
-import java.util.jar.JarFile;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -256,7 +261,7 @@
     String file = pathInfo.substring(1);
     ResourceKey key = new ResourceKey(holder.plugin, file);
     Resource rsc = resourceCache.getIfPresent(key);
-    if (rsc != null) {
+    if (rsc != null && req.getHeader(HttpHeaders.IF_MODIFIED_SINCE) == null) {
       rsc.send(req, res);
       return;
     }
@@ -268,17 +273,21 @@
     }
 
     if (file.startsWith(holder.staticPrefix)) {
-      JarFile jar = holder.plugin.getJarFile();
-      if (jar != null) {
-        JarEntry entry = jar.getJarEntry(file);
-        if (exists(entry)) {
-          sendResource(jar, entry, key, res);
+      if (holder.plugin.getApiType() == ApiType.JS) {
+        sendJsPlugin(holder.plugin, key, req, res);
+      } else {
+        PluginContentScanner scanner = holder.plugin.getContentScanner();
+        Optional<PluginEntry> entry = scanner.getEntry(file);
+        if (entry.isPresent()) {
+          if (hasUpToDateCachedResource(rsc, entry.get().getTime())) {
+            rsc.send(req, res);
+          } else {
+            sendResource(scanner, entry.get(), key, res);
+          }
         } else {
           resourceCache.put(key, Resource.NOT_FOUND);
           Resource.NOT_FOUND.send(req, res);
         }
-      } else {
-        sendJsPlugin(holder.plugin, key, req, res);
       }
     } else if (file.equals(
         holder.docPrefix.substring(0, holder.docPrefix.length() - 1))) {
@@ -286,18 +295,33 @@
     } else if (file.startsWith(holder.docPrefix) && file.endsWith("/")) {
       res.sendRedirect(uri + "index.html");
     } else if (file.startsWith(holder.docPrefix)) {
-      JarFile jar = holder.plugin.getJarFile();
-      JarEntry entry = jar.getJarEntry(file);
-      if (!exists(entry)) {
-        entry = findSource(jar, file);
+      PluginContentScanner scanner = holder.plugin.getContentScanner();
+      Optional<PluginEntry> entry = scanner.getEntry(file);
+      if (!entry.isPresent()) {
+        entry = findSource(scanner, file);
       }
-      if (!exists(entry) && file.endsWith("/index.html")) {
+      if (!entry.isPresent() && file.endsWith("/index.html")) {
         String pfx = file.substring(0, file.length() - "index.html".length());
-        sendAutoIndex(jar, pfx, holder.plugin.getName(), key, res);
-      } else if (exists(entry) && entry.getName().endsWith(".md")) {
-        sendMarkdownAsHtml(jar, entry, holder.plugin.getName(), key, res);
-      } else if (exists(entry)) {
-        sendResource(jar, entry, key, res);
+        long pluginLastModified = holder.plugin.getSrcFile().lastModified();
+        if (hasUpToDateCachedResource(rsc, pluginLastModified)) {
+          rsc.send(req, res);
+        } else {
+          sendAutoIndex(scanner, pfx, holder.plugin.getName(), key, res,
+              pluginLastModified);
+        }
+      } else if (entry.isPresent() && entry.get().getName().endsWith(".md")) {
+        if (hasUpToDateCachedResource(rsc, entry.get().getTime())) {
+          rsc.send(req, res);
+        } else {
+          sendMarkdownAsHtml(scanner, entry.get(), holder.plugin.getName(),
+              key, res);
+        }
+      } else if (entry.isPresent()) {
+        if (hasUpToDateCachedResource(rsc, entry.get().getTime())) {
+          rsc.send(req, res);
+        } else {
+          sendResource(scanner, entry.get(), key, res);
+        }
       } else {
         resourceCache.put(key, Resource.NOT_FOUND);
         Resource.NOT_FOUND.send(req, res);
@@ -308,18 +332,23 @@
     }
   }
 
-  private void appendEntriesSection(JarFile jar, List<JarEntry> entries,
+  private boolean hasUpToDateCachedResource(Resource cachedResource, long lastUpdateTime) {
+    return cachedResource != null
+        && cachedResource.isUnchanged(lastUpdateTime);
+  }
+
+  private void appendEntriesSection(PluginContentScanner scanner, List<PluginEntry> entries,
       String sectionTitle, StringBuilder md, String prefix,
       int nameOffset) throws IOException {
     if (!entries.isEmpty()) {
       md.append("## ").append(sectionTitle).append(" ##\n");
-      for(JarEntry entry : entries) {
+      for(PluginEntry entry : entries) {
         String rsrc = entry.getName().substring(prefix.length());
         String entryTitle;
         if (rsrc.endsWith(".html")) {
           entryTitle = rsrc.substring(nameOffset, rsrc.length() - 5).replace('-', ' ');
         } else if (rsrc.endsWith(".md")) {
-          entryTitle = extractTitleFromMarkdown(jar, entry);
+          entryTitle = extractTitleFromMarkdown(scanner, entry);
           if (Strings.isNullOrEmpty(entryTitle)) {
             entryTitle = rsrc.substring(nameOffset, rsrc.length() - 3).replace('-', ' ');
           }
@@ -333,23 +362,25 @@
     }
   }
 
-  private void sendAutoIndex(JarFile jar,
+  private void sendAutoIndex(PluginContentScanner scanner,
       String prefix, String pluginName,
-      ResourceKey cacheKey, HttpServletResponse res) throws IOException {
-    List<JarEntry> cmds = Lists.newArrayList();
-    List<JarEntry> servlets = Lists.newArrayList();
-    List<JarEntry> restApis = Lists.newArrayList();
-    List<JarEntry> docs = Lists.newArrayList();
-    JarEntry about = null;
-    Enumeration<JarEntry> entries = jar.entries();
+      ResourceKey cacheKey, HttpServletResponse res,long lastModifiedTime)
+      throws IOException {
+    List<PluginEntry> cmds = Lists.newArrayList();
+    List<PluginEntry> servlets = Lists.newArrayList();
+    List<PluginEntry> restApis = Lists.newArrayList();
+    List<PluginEntry> docs = Lists.newArrayList();
+    PluginEntry about = null;
+    Enumeration<PluginEntry> entries = scanner.entries();
     while (entries.hasMoreElements()) {
-      JarEntry entry = entries.nextElement();
+      PluginEntry entry = entries.nextElement();
       String name = entry.getName();
-      long size = entry.getSize();
+      Optional<Long> size = entry.getSize();
       if (name.startsWith(prefix)
           && (name.endsWith(".md")
               || name.endsWith(".html"))
-          && 0 < size && size <= SMALL_RESOURCE) {
+              && size.isPresent()
+          && 0 < size.get() && size.get() <= SMALL_RESOURCE) {
         name = name.substring(prefix.length());
         if (name.startsWith("cmd-")) {
           cmds.add(entry);
@@ -366,26 +397,17 @@
         }
       }
     }
-    Collections.sort(cmds, new Comparator<JarEntry>() {
-      @Override
-      public int compare(JarEntry a, JarEntry b) {
-        return a.getName().compareTo(b.getName());
-      }
-    });
-    Collections.sort(docs, new Comparator<JarEntry>() {
-      @Override
-      public int compare(JarEntry a, JarEntry b) {
-        return a.getName().compareTo(b.getName());
-      }
-    });
+
+    Collections.sort(cmds, PluginEntry.COMPARATOR_BY_NAME);
+    Collections.sort(docs, PluginEntry.COMPARATOR_BY_NAME);
 
     StringBuilder md = new StringBuilder();
     md.append(String.format("# Plugin %s #\n", pluginName));
     md.append("\n");
-    appendPluginInfoTable(md, jar.getManifest().getMainAttributes());
+    appendPluginInfoTable(md, scanner.getManifest().getMainAttributes());
 
     if (about != null) {
-      InputStreamReader isr = new InputStreamReader(jar.getInputStream(about));
+      InputStreamReader isr = new InputStreamReader(scanner.getInputStream(about));
       BufferedReader reader = new BufferedReader(isr);
       StringBuilder aboutContent = new StringBuilder();
       String line;
@@ -406,16 +428,16 @@
       }
     }
 
-    appendEntriesSection(jar, docs, "Documentation", md, prefix, 0);
-    appendEntriesSection(jar, servlets, "Servlets", md, prefix, "servlet-".length());
-    appendEntriesSection(jar, restApis, "REST APIs", md, prefix, "rest-api-".length());
-    appendEntriesSection(jar, cmds, "Commands", md, prefix, "cmd-".length());
+    appendEntriesSection(scanner, docs, "Documentation", md, prefix, 0);
+    appendEntriesSection(scanner, servlets, "Servlets", md, prefix, "servlet-".length());
+    appendEntriesSection(scanner, restApis, "REST APIs", md, prefix, "rest-api-".length());
+    appendEntriesSection(scanner, cmds, "Commands", md, prefix, "cmd-".length());
 
-    sendMarkdownAsHtml(md.toString(), pluginName, cacheKey, res);
+    sendMarkdownAsHtml(md.toString(), pluginName, cacheKey, res, lastModifiedTime);
   }
 
   private void sendMarkdownAsHtml(String md, String pluginName,
-      ResourceKey cacheKey, HttpServletResponse res)
+      ResourceKey cacheKey, HttpServletResponse res, long lastModifiedTime)
       throws UnsupportedEncodingException, IOException {
     Map<String, String> macros = Maps.newHashMap();
     macros.put("PLUGIN", pluginName);
@@ -446,10 +468,12 @@
       .markdownToDocHtml(sb.toString(), "UTF-8");
     resourceCache.put(cacheKey, new SmallResource(html)
         .setContentType("text/html")
-        .setCharacterEncoding("UTF-8"));
+        .setCharacterEncoding("UTF-8")
+        .setLastModified(lastModifiedTime));
     res.setContentType("text/html");
     res.setCharacterEncoding("UTF-8");
     res.setContentLength(html.length);
+    res.setDateHeader("Last-Modified", lastModifiedTime);
     res.getOutputStream().write(html);
   }
 
@@ -491,41 +515,38 @@
     }
   }
 
-  private static String extractTitleFromMarkdown(JarFile jar, JarEntry entry)
+  private static String extractTitleFromMarkdown(PluginContentScanner scanner, PluginEntry entry)
         throws IOException {
     String charEnc = null;
-    Attributes atts = entry.getAttributes();
+    Map<Object, String> atts = entry.getAttrs();
     if (atts != null) {
-      charEnc = Strings.emptyToNull(atts.getValue("Character-Encoding"));
+      charEnc = Strings.emptyToNull(atts.get(ATTR_CHARACTER_ENCODING));
     }
     if (charEnc == null) {
       charEnc = "UTF-8";
     }
     return new MarkdownFormatter().extractTitleFromMarkdown(
-          readWholeEntry(jar, entry),
+          readWholeEntry(scanner, entry),
           charEnc);
   }
 
-  private static JarEntry findSource(JarFile jar, String file) {
+  private static Optional<PluginEntry> findSource(
+      PluginContentScanner scanner, String file) throws IOException {
     if (file.endsWith(".html")) {
       int d = file.lastIndexOf('.');
-      return jar.getJarEntry(file.substring(0, d) + ".md");
+      return scanner.getEntry(file.substring(0, d) + ".md");
     }
-    return null;
+    return Optional.absent();
   }
 
-  private static boolean exists(JarEntry entry) {
-    return entry != null && entry.getSize() > 0;
-  }
-
-  private void sendMarkdownAsHtml(JarFile jar, JarEntry entry,
+  private void sendMarkdownAsHtml(PluginContentScanner scanner, PluginEntry entry,
       String pluginName, ResourceKey key, HttpServletResponse res)
       throws IOException {
-    byte[] rawmd = readWholeEntry(jar, entry);
+    byte[] rawmd = readWholeEntry(scanner, entry);
     String encoding = null;
-    Attributes atts = entry.getAttributes();
+    Map<Object, String> atts = entry.getAttrs();
     if (atts != null) {
-      encoding = Strings.emptyToNull(atts.getValue("Character-Encoding"));
+      encoding = Strings.emptyToNull(atts.get(ATTR_CHARACTER_ENCODING));
     }
 
     String txtmd = RawParseUtils.decode(
@@ -535,29 +556,33 @@
     if (0 < time) {
       res.setDateHeader("Last-Modified", time);
     }
-    sendMarkdownAsHtml(txtmd, pluginName, key, res);
+    sendMarkdownAsHtml(txtmd, pluginName, key, res, time);
   }
 
-  private void sendResource(JarFile jar, JarEntry entry,
+  private void sendResource(PluginContentScanner scanner, PluginEntry entry,
       ResourceKey key, HttpServletResponse res)
       throws IOException {
     byte[] data = null;
-    if (entry.getSize() <= SMALL_RESOURCE) {
-      data = readWholeEntry(jar, entry);
+    Optional<Long> size = entry.getSize();
+    if (size.isPresent() && size.get() <= SMALL_RESOURCE) {
+      data = readWholeEntry(scanner, entry);
     }
 
     String contentType = null;
     String charEnc = null;
-    Attributes atts = entry.getAttributes();
+    Map<Object, String> atts = entry.getAttrs();
     if (atts != null) {
-      contentType = Strings.emptyToNull(atts.getValue("Content-Type"));
-      charEnc = Strings.emptyToNull(atts.getValue("Character-Encoding"));
+      contentType = Strings.emptyToNull(atts.get(ATTR_CONTENT_TYPE));
+      charEnc = Strings.emptyToNull(atts.get(ATTR_CHARACTER_ENCODING));
     }
     if (contentType == null) {
       contentType = mimeUtil.getMimeType(entry.getName(), data).toString();
       if ("application/octet-stream".equals(contentType)
           && entry.getName().endsWith(".js")) {
         contentType = "application/javascript";
+      } else if ("application/x-pointplus".equals(contentType)
+          && entry.getName().endsWith(".css")) {
+        contentType = "text/css";
       }
     }
 
@@ -565,7 +590,9 @@
     if (0 < time) {
       res.setDateHeader("Last-Modified", time);
     }
-    res.setHeader("Content-Length", Long.toString(entry.getSize()));
+    if (size.isPresent()) {
+      res.setHeader("Content-Length", size.get().toString());
+    }
     res.setContentType(contentType);
     if (charEnc != null) {
       res.setCharacterEncoding(charEnc);
@@ -577,7 +604,7 @@
           .setLastModified(time));
       res.getOutputStream().write(data);
     } else {
-      writeToResponse(res, jar.getInputStream(entry));
+      writeToResponse(res, scanner.getInputStream(entry));
     }
   }
 
@@ -618,10 +645,10 @@
     }
   }
 
-  private static byte[] readWholeEntry(JarFile jar, JarEntry entry)
+  private static byte[] readWholeEntry(PluginContentScanner scanner, PluginEntry entry)
       throws IOException {
-    byte[] data = new byte[(int) entry.getSize()];
-    InputStream in = jar.getInputStream(entry);
+    byte[] data = new byte[entry.getSize().get().intValue()];
+    InputStream in = scanner.getInputStream(entry);
     try {
       IO.readFully(in, data, 0, data.length);
     } finally {
@@ -646,13 +673,14 @@
     }
 
     private static String getPrefix(Plugin plugin, String attr, String def) {
-      JarFile jarFile = plugin.getJarFile();
-      if (jarFile == null) {
+      File srcFile = plugin.getSrcFile();
+      PluginContentScanner scanner = plugin.getContentScanner();
+      if (srcFile == null || scanner == PluginContentScanner.EMPTY) {
         return def;
       }
       try {
-        String prefix = jarFile.getManifest().getMainAttributes()
-            .getValue(attr);
+        String prefix =
+            scanner.getManifest().getMainAttributes().getValue(attr);
         if (prefix != null) {
           return CharMatcher.is('/').trimFrom(prefix) + "/";
         } else {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/Resource.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/Resource.java
index f354fd5..b361fdc 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/Resource.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/Resource.java
@@ -34,9 +34,17 @@
       CacheHeaders.setNotCacheable(res);
       res.sendError(HttpServletResponse.SC_NOT_FOUND);
     }
+
+    @Override
+    boolean isUnchanged(long latestModifiedDate) {
+      return false;
+    }
   };
 
+  abstract boolean isUnchanged(long latestModifiedDate);
+
   abstract int weigh();
+
   abstract void send(HttpServletRequest req, HttpServletResponse res)
       throws IOException;
-}
\ No newline at end of file
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/SmallResource.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/SmallResource.java
index 0db7a88..2a3da57 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/SmallResource.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/SmallResource.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.httpd.plugins;
 
+import com.google.common.net.HttpHeaders;
 import com.google.gerrit.common.Nullable;
 
 import java.io.IOException;
@@ -55,7 +56,13 @@
   void send(HttpServletRequest req, HttpServletResponse res)
       throws IOException {
     if (0 < lastModified) {
-      res.setDateHeader("Last-Modified", lastModified);
+      long ifModifiedSince = req.getDateHeader(HttpHeaders.IF_MODIFIED_SINCE);
+      if (ifModifiedSince > 0 && ifModifiedSince == lastModified) {
+        res.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
+        return;
+      } else {
+        res.setDateHeader("Last-Modified", lastModified);
+      }
     }
     res.setContentType(contentType);
     if (characterEncoding != null) {
@@ -64,4 +71,9 @@
     res.setContentLength(data.length);
     res.getOutputStream().write(data);
   }
+
+  @Override
+  boolean isUnchanged(long lastModified) {
+    return this.lastModified == lastModified;
+  }
 }
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 fda2c0d..18a2ae5 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
@@ -22,6 +22,7 @@
 import com.google.gerrit.common.Version;
 import com.google.gerrit.common.data.GerritConfig;
 import com.google.gerrit.common.data.HostPageData;
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.systemstatus.MessageOfTheDay;
 import com.google.gerrit.extensions.webui.WebUiPlugin;
@@ -70,7 +71,7 @@
   private static final String HPD_ID = "gerrit_hostpagedata";
 
   private final Provider<CurrentUser> currentUser;
-  private final Provider<WebSession> session;
+  private final DynamicItem<WebSession> session;
   private final GerritConfig config;
   private final DynamicSet<WebUiPlugin> plugins;
   private final DynamicSet<MessageOfTheDay> messages;
@@ -84,7 +85,7 @@
   private volatile Page page;
 
   @Inject
-  HostPageServlet(final Provider<CurrentUser> cu, final Provider<WebSession> w,
+  HostPageServlet(final Provider<CurrentUser> cu, final DynamicItem<WebSession> w,
       final SitePaths sp, final ThemeFactory themeFactory,
       final GerritConfig gc, final ServletContext servletContext,
       final DynamicSet<WebUiPlugin> webUiPlugins,
@@ -330,7 +331,7 @@
       nocache.setAttribute("src", noCacheName);
       opt = new Content(hostDoc);
 
-      nocache.setAttribute("src", "gerrit_ui/gerrit_dbg.nocache.js");
+      nocache.setAttribute("src", "gerrit_ui/dbg_gerrit_ui.nocache.js");
       debug = new Content(hostDoc);
     }
 
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 ce450dc..45144d7 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
@@ -47,6 +47,7 @@
 import com.google.gerrit.audit.AuditService;
 import com.google.gerrit.audit.HttpAuditEvent;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AcceptsCreate;
 import com.google.gerrit.extensions.restapi.AcceptsPost;
@@ -102,6 +103,7 @@
 import java.io.BufferedReader;
 import java.io.BufferedWriter;
 import java.io.EOFException;
+import java.io.FilterOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -151,13 +153,13 @@
 
   public static class Globals {
     final Provider<CurrentUser> currentUser;
-    final Provider<WebSession> webSession;
+    final DynamicItem<WebSession> webSession;
     final Provider<ParameterParser> paramParser;
     final AuditService auditService;
 
     @Inject
     Globals(Provider<CurrentUser> currentUser,
-        Provider<WebSession> webSession,
+        DynamicItem<WebSession> webSession,
         Provider<ParameterParser> paramParser,
         AuditService auditService) {
       this.currentUser = currentUser;
@@ -206,7 +208,7 @@
       RestResource rsrc = TopLevelResource.INSTANCE;
       ViewData viewData = new ViewData(null, null);
       if (path.isEmpty()) {
-        if ("GET".equals(req.getMethod())) {
+        if (isGetOrHead(req)) {
           viewData = new ViewData(null, rc.list());
         } else if (rc instanceof AcceptsPost && "POST".equals(req.getMethod())) {
           @SuppressWarnings("unchecked")
@@ -247,7 +249,7 @@
             (RestCollection<RestResource, RestResource>) viewData.view;
 
         if (path.isEmpty()) {
-          if ("GET".equals(req.getMethod())) {
+          if (isGetOrHead(req)) {
             viewData = new ViewData(null, c.list());
           } else if (c instanceof AcceptsPost && "POST".equals(req.getMethod())) {
             @SuppressWarnings("unchecked")
@@ -363,7 +365,7 @@
   }
 
   private static boolean notModified(HttpServletRequest req, RestResource rsrc) {
-    if (!"GET".equals(req.getMethod())) {
+    if (!isGetOrHead(req)) {
       return false;
     }
 
@@ -386,7 +388,7 @@
 
   private static <T> void configureCaching(HttpServletRequest req,
       HttpServletResponse res, RestResource rsrc, CacheControl c) {
-    if ("GET".equals(req.getMethod())) {
+    if (isGetOrHead(req)) {
       switch (c.getType()) {
         case NONE:
         default:
@@ -716,11 +718,13 @@
         res.setHeader("Content-Length", Long.toString(len));
       }
 
-      OutputStream dst = res.getOutputStream();
-      try {
-        bin.writeTo(dst);
-      } finally {
-        dst.close();
+      if (req == null || !"HEAD".equals(req.getMethod())) {
+        OutputStream dst = res.getOutputStream();
+        try {
+          bin.writeTo(dst);
+        } finally {
+          dst.close();
+        }
       }
     } finally {
       appResult.close();
@@ -737,10 +741,16 @@
       b64 = new BinaryResult() {
         @Override
         public void writeTo(OutputStream out) throws IOException {
-          OutputStream e = BaseEncoding.base64().encodingStream(
-              new OutputStreamWriter(out, ISO_8859_1));
-          src.writeTo(e);
-          e.flush();
+          try (OutputStreamWriter w = new OutputStreamWriter(
+                new FilterOutputStream(out) {
+                  @Override
+                  public void close() {
+                    // Do not close out, but only w and e.
+                  }
+                }, ISO_8859_1);
+              OutputStream e = BaseEncoding.base64().encodingStream(w)) {
+            src.writeTo(e);
+          }
         }
       };
     }
@@ -787,6 +797,8 @@
       // is chosen, look for the projection based upon GET as the method as
       // the client thinks it is a nested collection.
       method = "GET";
+    } else if ("HEAD".equals(method)) {
+      method = "GET";
     }
 
     List<String> p = splitProjection(projection);
@@ -815,6 +827,13 @@
     RestView<RestResource> core = views.get("gerrit", name);
     if (core != null) {
       return new ViewData(null, core);
+    } else {
+      core = views.get("gerrit", "GET." + p.get(0));
+      if (core instanceof AcceptsPost && "POST".equals(method)) {
+        @SuppressWarnings("unchecked")
+        AcceptsPost<RestResource> ap = (AcceptsPost<RestResource>) core;
+        return new ViewData(null, ap.post(rsrc));
+      }
     }
 
     Map<String, RestView<RestResource>> r = Maps.newTreeMap();
@@ -879,9 +898,12 @@
     user.setAccessPath(AccessPath.REST_API);
   }
 
+  private static boolean isGetOrHead(HttpServletRequest req) {
+    return "GET".equals(req.getMethod()) || "HEAD".equals(req.getMethod());
+  }
+
   private static boolean isStateChange(HttpServletRequest req) {
-    String method = req.getMethod();
-    return !("GET".equals(method) || "HEAD".equals(method));
+    return !isGetOrHead(req);
   }
 
   private void checkRequiresCapability(ViewData viewData) throws AuthException {
@@ -913,12 +935,12 @@
       CacheControl c) throws IOException {
     res.setStatus(statusCode);
     configureCaching(req, res, null, c);
-    replyText(null, res, msg);
+    replyText(req, res, msg);
   }
 
   static void replyText(@Nullable HttpServletRequest req,
       HttpServletResponse res, String text) throws IOException {
-    if ((req == null || "GET".equals(req.getMethod())) && isMaybeHTML(text)) {
+    if ((req == null || isGetOrHead(req)) && isMaybeHTML(text)) {
       replyJson(req, res, ImmutableMultimap.of("pp", "0"), new JsonPrimitive(text));
     } else {
       if (!text.endsWith("\n")) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java
index ec96f0b..32b4958 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.common.audit.Audit;
 import com.google.gerrit.common.auth.SignInRequired;
 import com.google.gerrit.common.errors.NotSignedInException;
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.CurrentUser;
@@ -32,7 +33,6 @@
 import com.google.gwtjsonrpc.server.MethodHandle;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -51,16 +51,16 @@
 final class GerritJsonServlet extends JsonServlet<GerritJsonServlet.GerritCall> {
   private static final Logger log = LoggerFactory.getLogger(GerritJsonServlet.class);
   private static final ThreadLocal<GerritCall> currentCall =
-      new ThreadLocal<GerritCall>();
+      new ThreadLocal<>();
   private static final ThreadLocal<MethodHandle> currentMethod =
-      new ThreadLocal<MethodHandle>();
-  private final Provider<WebSession> session;
+      new ThreadLocal<>();
+  private final DynamicItem<WebSession> session;
   private final RemoteJsonService service;
   private final AuditService audit;
 
 
   @Inject
-  GerritJsonServlet(final Provider<WebSession> w, final RemoteJsonService s,
+  GerritJsonServlet(final DynamicItem<WebSession> w, final RemoteJsonService s,
       final AuditService a) {
     session = w;
     service = s;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java
index f8d43a8..dcd181c 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java
@@ -137,8 +137,7 @@
     final int max = 10;
     final int n = limit <= 0 ? max : Math.min(limit, max);
 
-    final LinkedHashMap<Account.Id, AccountInfo> r =
-        new LinkedHashMap<Account.Id, AccountInfo>();
+    LinkedHashMap<Account.Id, AccountInfo> r = new LinkedHashMap<>();
     for (final Account p : db.accounts().suggestByFullName(a, b, n)) {
       addSuggestion(r, p, new AccountInfo(p), active, visibilityControl);
     }
@@ -159,7 +158,7 @@
         }
       }
     }
-    return new ArrayList<AccountInfo>(r.values());
+    return new ArrayList<>(r.values());
   }
 
   private void addSuggestion(Map<Account.Id, AccountInfo> map, Account account,
@@ -251,7 +250,7 @@
         final List<AccountInfo> suggestedAccounts =
             suggestAccount(db, query, Boolean.TRUE, limit, visibilityControl);
         final List<ReviewerInfo> reviewer =
-            new ArrayList<ReviewerInfo>(suggestedAccounts.size());
+            new ArrayList<>(suggestedAccounts.size());
         for (final AccountInfo a : suggestedAccounts) {
           reviewer.add(new ReviewerInfo(a));
         }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java
index 931605c..56a6a50 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java
@@ -72,7 +72,7 @@
   }
 
   public void daemonHostKeys(final AsyncCallback<List<SshHostKey>> callback) {
-    final ArrayList<SshHostKey> r = new ArrayList<SshHostKey>(hostKeys.size());
+    final ArrayList<SshHostKey> r = new ArrayList<>(hostKeys.size());
     for (final HostKey hk : hostKeys) {
       String host = hk.getHost();
       if (host.startsWith("*:")) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java
index 90357ae..bdb0028 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java
@@ -113,8 +113,7 @@
       final AsyncCallback<List<AccountProjectWatchInfo>> callback) {
     run(callback, new Action<List<AccountProjectWatchInfo>>() {
       public List<AccountProjectWatchInfo> run(ReviewDb db) throws OrmException {
-        final List<AccountProjectWatchInfo> r =
-            new ArrayList<AccountProjectWatchInfo>();
+        List<AccountProjectWatchInfo> r = new ArrayList<>();
 
         for (final AccountProjectWatch w : db.accountProjectWatches()
             .byAccount(getAccountId()).toList()) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/DeleteExternalIds.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/DeleteExternalIds.java
index 12bc761..1d45c7d 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/DeleteExternalIds.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/DeleteExternalIds.java
@@ -63,7 +63,7 @@
   public Set<AccountExternalId.Key> call() throws OrmException {
     final Map<AccountExternalId.Key, AccountExternalId> have = have();
 
-    List<AccountExternalId> toDelete = new ArrayList<AccountExternalId>();
+    List<AccountExternalId> toDelete = new ArrayList<>();
     for (AccountExternalId.Key k : keys) {
       final AccountExternalId id = have.get(k);
       if (id != null && id.canDelete()) {
@@ -86,7 +86,7 @@
       throws OrmException {
     Map<AccountExternalId.Key, AccountExternalId> r;
 
-    r = new HashMap<AccountExternalId.Key, AccountExternalId>();
+    r = new HashMap<>();
     for (AccountExternalId i : detailFactory.call()) {
       r.put(i.getKey(), i);
     }
@@ -94,7 +94,7 @@
   }
 
   private Set<AccountExternalId.Key> toKeySet(List<AccountExternalId> toDelete) {
-    Set<AccountExternalId.Key> r = new HashSet<AccountExternalId.Key>();
+    Set<AccountExternalId.Key> r = new HashSet<>();
     for (AccountExternalId i : toDelete) {
       r.add(i.getKey());
     }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/ExternalIdDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/ExternalIdDetailFactory.java
index d876f2e..b97d46a 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/ExternalIdDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/ExternalIdDetailFactory.java
@@ -16,6 +16,7 @@
 
 import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME;
 
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
@@ -36,11 +37,11 @@
   private final ReviewDb db;
   private final IdentifiedUser user;
   private final AuthConfig authConfig;
-  private final WebSession session;
+  private final DynamicItem<WebSession> session;
 
   @Inject
   ExternalIdDetailFactory(final ReviewDb db, final IdentifiedUser user,
-      final AuthConfig authConfig, final WebSession session) {
+      final AuthConfig authConfig, final DynamicItem<WebSession> session) {
     this.db = db;
     this.user = user;
     this.authConfig = authConfig;
@@ -49,7 +50,7 @@
 
   @Override
   public List<AccountExternalId> call() throws OrmException {
-    final AccountExternalId.Key last = session.getLastLoginExternalId();
+    final AccountExternalId.Key last = session.get().getLastLoginExternalId();
     final List<AccountExternalId> ids =
         db.accountExternalIds().byAccount(user.getAccountId()).toList();
 
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 fce8637..73cc83a 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
@@ -34,10 +34,12 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.PatchLineCommentsUtil;
 import com.google.gerrit.server.change.ChangesCollection;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.change.Revisions;
 import com.google.gerrit.server.extensions.webui.UiActions;
+import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchListKey;
@@ -78,6 +80,7 @@
   private final ChangeControl.Factory changeControlFactory;
   private final ChangesCollection changes;
   private final Revisions revisions;
+  private final PatchLineCommentsUtil plcUtil;
 
   private Project.NameKey projectKey;
   private final PatchSet.Id psIdBase;
@@ -96,6 +99,7 @@
       final ChangeControl.Factory changeControlFactory,
       final ChangesCollection changes,
       final Revisions revisions,
+      final PatchLineCommentsUtil plcUtil,
       @Assisted("psIdBase") @Nullable final PatchSet.Id psIdBase,
       @Assisted("psIdNew") final PatchSet.Id psIdNew,
       @Assisted @Nullable final AccountDiffPreference diffPrefs) {
@@ -105,6 +109,7 @@
     this.changeControlFactory = changeControlFactory;
     this.changes = changes;
     this.revisions = revisions;
+    this.plcUtil = plcUtil;
 
     this.psIdBase = psIdBase;
     this.psIdNew = psIdNew;
@@ -138,12 +143,13 @@
     }
 
     final List<Patch> patches = list.toPatchList(patchSet.getId());
-    final Map<Patch.Key, Patch> byKey = new HashMap<Patch.Key, Patch>();
+    final Map<Patch.Key, Patch> byKey = new HashMap<>();
     for (final Patch p : patches) {
       byKey.put(p.getKey(), p);
     }
 
-    for (final PatchLineComment c : db.patchComments().publishedByPatchSet(psIdNew)) {
+    ChangeNotes notes = control.getNotes();
+    for (PatchLineComment c : plcUtil.publishedByPatchSet(db, notes, psIdNew)) {
       final Patch p = byKey.get(c.getKey().getParentKey());
       if (p != null) {
         p.setCommentCount(p.getCommentCount() + 1);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
index ee4439f..3615610 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
@@ -114,8 +114,8 @@
     }
 
     final RefControl metaConfigControl = pc.controlForRef(RefNames.REFS_CONFIG);
-    List<AccessSection> local = new ArrayList<AccessSection>();
-    Set<String> ownerOf = new HashSet<String>();
+    List<AccessSection> local = new ArrayList<>();
+    Set<String> ownerOf = new HashSet<>();
     Map<AccountGroup.UUID, Boolean> visibleGroups = new HashMap<>();
 
     for (AccessSection section : config.getAccessSections()) {
@@ -206,8 +206,6 @@
     detail.setOwnerOf(ownerOf);
     detail.setCanUpload(pc.isOwner()
         || (metaConfigControl.isVisible() && metaConfigControl.canUpload()));
-    detail.setCanChangeParent(pc.getCurrentUser().getCapabilities()
-        .canAdministrateServer());
     detail.setConfigVisible(pc.isOwner() || metaConfigControl.isVisible());
     detail.setGroupInfo(buildGroupInfo(local));
     detail.setLabelTypes(pc.getLabelTypes());
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
index b2b8bb3..52283b2 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
@@ -185,7 +185,7 @@
   }
 
   private static Set<String> scanSectionNames(ProjectConfig config) {
-    Set<String> names = new HashSet<String>();
+    Set<String> names = new HashSet<>();
     for (AccessSection section : config.getAccessSections()) {
       names.add(section.getName());
     }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
index 6537a6b..51aa2c0 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
@@ -182,7 +182,7 @@
   private void insertAncestors(PatchSet.Id id, RevCommit src)
       throws OrmException {
     final int cnt = src.getParentCount();
-    List<PatchSetAncestor> toInsert = new ArrayList<PatchSetAncestor>(cnt);
+    List<PatchSetAncestor> toInsert = new ArrayList<>(cnt);
     for (int p = 0; p < cnt; p++) {
       PatchSetAncestor a;
 
diff --git a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/container/LoginRedirect.html b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/container/LoginRedirect.html
index d88af9a..0567468 100644
--- a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/container/LoginRedirect.html
+++ b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/container/LoginRedirect.html
@@ -7,10 +7,7 @@
       var p = href.indexOf('#');
       var token;
       if (p >= 0) {
-        token = href.substring(p + 1);
-        if (token.length != 0 && token.charAt(0) == '/') {
-          token = token.substring(1);
-        }
+        token = href.substring(p);
         href = href.substring(0, p);
       } else {
         token = '';
diff --git a/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java b/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
index 33a3534..474b284 100644
--- a/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
+++ b/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
@@ -50,7 +50,7 @@
     System.exit(mainImpl(argv));
   }
 
-  private static int mainImpl(final String argv[]) throws Exception {
+  public static int mainImpl(final String argv[]) throws Exception {
     if (argv.length == 0) {
       File me;
       try {
@@ -192,7 +192,7 @@
       throw e;
     }
 
-    final SortedMap<String, URL> jars = new TreeMap<String, URL>();
+    final SortedMap<String, URL> jars = new TreeMap<>();
     try {
       final ZipFile zf = new ZipFile(path);
       try {
@@ -220,7 +220,7 @@
 
     // The extension API needs to be its own ClassLoader, along
     // with a few of its dependencies. Try to construct this first.
-    List<URL> extapi = new ArrayList<URL>();
+    List<URL> extapi = new ArrayList<>();
     move(jars, "gerrit-extension-api-", extapi);
     move(jars, "guice-", extapi);
     move(jars, "javax.inject-1.jar", extapi);
@@ -592,7 +592,7 @@
   private static ClassLoader useDevClasspath()
       throws MalformedURLException, FileNotFoundException {
     File out = getDeveloperBuckOut();
-    List<URL> dirs = new ArrayList<URL>();
+    List<URL> dirs = new ArrayList<>();
     dirs.add(new File(new File(out, "eclipse"), "classes").toURI().toURL());
     ClassLoader cl = GerritLauncher.class.getClassLoader();
     for (URL u : ((URLClassLoader) cl).getURLs()) {
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index d03e202..3e3a7fa 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -72,6 +72,7 @@
 import org.apache.lucene.index.IndexWriter;
 import org.apache.lucene.index.IndexWriterConfig;
 import org.apache.lucene.index.IndexWriterConfig.OpenMode;
+import org.apache.lucene.index.IndexableField;
 import org.apache.lucene.index.Term;
 import org.apache.lucene.search.BooleanQuery;
 import org.apache.lucene.search.IndexSearcher;
@@ -114,9 +115,14 @@
 
   public static final String CHANGES_OPEN = "open";
   public static final String CHANGES_CLOSED = "closed";
-  private static final String ID_FIELD = ChangeField.LEGACY_ID.getName();
-  private static final String CHANGE_FIELD = ChangeField.CHANGE.getName();
+
+  private static final String ADDED_FIELD = ChangeField.ADDED.getName();
   private static final String APPROVAL_FIELD = ChangeField.APPROVAL.getName();
+  private static final String CHANGE_FIELD = ChangeField.CHANGE.getName();
+  private static final String DELETED_FIELD = ChangeField.DELETED.getName();
+  private static final String ID_FIELD = ChangeField.LEGACY_ID.getName();
+  private static final ImmutableSet<String> FIELDS = ImmutableSet.of(
+      ADDED_FIELD, APPROVAL_FIELD, CHANGE_FIELD, DELETED_FIELD, ID_FIELD);
 
   private static final Map<Schema<ChangeData>, Version> LUCENE_VERSIONS;
   static {
@@ -126,14 +132,22 @@
     Version lucene43 = Version.LUCENE_43;
     @SuppressWarnings("deprecation")
     Version lucene44 = Version.LUCENE_44;
+    @SuppressWarnings("deprecation")
+    Version lucene46 = Version.LUCENE_46;
+    @SuppressWarnings("deprecation")
+    Version lucene47 = Version.LUCENE_47;
     for (Map.Entry<Integer, Schema<ChangeData>> e
         : ChangeSchemas.ALL.entrySet()) {
       if (e.getKey() <= 3) {
         versions.put(e.getValue(), lucene43);
       } else if (e.getKey() <= 5) {
         versions.put(e.getValue(), lucene44);
+      } else if (e.getKey() <= 8) {
+        versions.put(e.getValue(), lucene46);
+      } else if (e.getKey() <= 10) {
+        versions.put(e.getValue(), lucene47);
       } else {
-        versions.put(e.getValue(), Version.LUCENE_46);
+        versions.put(e.getValue(), Version.LUCENE_48);
       }
     }
     LUCENE_VERSIONS = versions.build();
@@ -361,9 +375,6 @@
     setReady(sitePaths, schema.getVersion(), ready);
   }
 
-  private static final ImmutableSet<String> FIELDS =
-      ImmutableSet.of(ID_FIELD, CHANGE_FIELD, APPROVAL_FIELD);
-
   @SuppressWarnings("deprecation")
   private static Sort getSort(Schema<ChangeData> schema,
       Predicate<ChangeData> p) {
@@ -473,10 +484,12 @@
       return changeDataFactory.create(db.get(), new Change.Id(id));
     }
 
+    // Change proto.
     Change change = ChangeProtoField.CODEC.decode(
         cb.bytes, cb.offset, cb.length);
     ChangeData cd = changeDataFactory.create(db.get(), change);
 
+    // Approvals.
     BytesRef[] approvalsBytes = doc.getBinaryValues(APPROVAL_FIELD);
     if (approvalsBytes != null) {
       List<PatchSetApproval> approvals =
@@ -487,6 +500,16 @@
       }
       cd.setCurrentApprovals(approvals);
     }
+
+    // Changed lines.
+    IndexableField added = doc.getField(ADDED_FIELD);
+    IndexableField deleted = doc.getField(DELETED_FIELD);
+    if (added != null && deleted != null) {
+      cd.setChangedLines(
+          added.numericValue().intValue(),
+          deleted.numericValue().intValue());
+    }
+
     return cd;
   }
 
@@ -510,7 +533,7 @@
     FieldType<?> type = values.getField().getType();
     Store store = store(values.getField());
 
-    if (type == FieldType.INTEGER) {
+    if (type == FieldType.INTEGER || type == FieldType.INTEGER_RANGE) {
       for (Object value : values.getValues()) {
         doc.add(new IntField(name, (Integer) value, store));
       }
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/OnlineReindexer.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/OnlineReindexer.java
index d3dc963..41c83eb 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/OnlineReindexer.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/OnlineReindexer.java
@@ -79,7 +79,9 @@
     ChangeBatchIndexer.Result result = batchIndexer.indexAll(
         index, projectCache.all(), -1, -1, null, null);
     if (!result.success()) {
-      log.error("Online reindex of schema version {} failed", version(index));
+      log.error("Online reindex of schema version {} failed. Successfully"
+          + " indexed {} changes, failed to index {} changes",
+          version(index), result.doneCount(), result.failedCount());
       return;
     }
 
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/QueryBuilder.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/QueryBuilder.java
index 392d1645..2365929 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/QueryBuilder.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/QueryBuilder.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.server.index.ChangeField;
 import com.google.gerrit.server.index.FieldType;
 import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gerrit.server.index.IntegerRangePredicate;
 import com.google.gerrit.server.index.RegexPredicate;
 import com.google.gerrit.server.index.Schema;
 import com.google.gerrit.server.index.TimestampRangePredicate;
@@ -139,6 +140,8 @@
       throws QueryParseException {
     if (p.getType() == FieldType.INTEGER) {
       return intQuery(p);
+    } else if (p.getType() == FieldType.INTEGER_RANGE) {
+      return intRangeQuery(p);
     } else if (p.getType() == FieldType.TIMESTAMP) {
       return timestampQuery(p);
     } else if (p.getType() == FieldType.EXACT) {
@@ -173,6 +176,28 @@
     return new TermQuery(intTerm(p.getField().getName(), value));
   }
 
+  private Query intRangeQuery(IndexPredicate<ChangeData> p)
+      throws QueryParseException {
+    if (p instanceof IntegerRangePredicate) {
+      IntegerRangePredicate<ChangeData> r =
+          (IntegerRangePredicate<ChangeData>) p;
+      int minimum = r.getMinimumValue();
+      int maximum = r.getMaximumValue();
+      if (minimum == maximum) {
+        // Just fall back to a standard integer query.
+        return new TermQuery(intTerm(p.getField().getName(), minimum));
+      } else {
+        return NumericRangeQuery.newIntRange(
+            r.getField().getName(),
+            minimum,
+            maximum,
+            true,
+            true);
+      }
+    }
+    throw new QueryParseException("not an integer range: " + p);
+  }
+
   private Query sortKeyQuery(SortKeyPredicate p) {
     long min = p.getMinValue(schema);
     long max = p.getMaxValue(schema);
diff --git a/gerrit-oauth/BUCK b/gerrit-oauth/BUCK
new file mode 100644
index 0000000..fa5a8e2
--- /dev/null
+++ b/gerrit-oauth/BUCK
@@ -0,0 +1,26 @@
+SRCS = glob(
+  ['src/main/java/**/*.java'],
+)
+RESOURCES = glob(['src/main/resources/**/*'])
+
+java_library(
+  name = 'oauth',
+  srcs = SRCS,
+  resources = RESOURCES,
+  deps = [
+    '//gerrit-common:annotations',
+    '//gerrit-extension-api:api',
+    '//gerrit-httpd:httpd',
+    '//gerrit-reviewdb:server',
+    '//gerrit-server:server',
+    '//lib:gson',
+    '//lib:guava',
+    '//lib:gwtorm',
+    '//lib/commons:codec',
+    '//lib/guice:guice',
+    '//lib/guice:guice-servlet',
+    '//lib/log:api',
+  ],
+  provided_deps = ['//lib:servlet-api-3_1'],
+  visibility = ['PUBLIC'],
+)
diff --git a/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthLogoutServlet.java b/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthLogoutServlet.java
new file mode 100644
index 0000000..35253a1
--- /dev/null
+++ b/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthLogoutServlet.java
@@ -0,0 +1,59 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd.auth.oauth;
+
+import com.google.gerrit.audit.AuditService;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.httpd.HttpLogoutServlet;
+import com.google.gerrit.httpd.WebSession;
+import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import java.io.IOException;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@Singleton
+class OAuthLogoutServlet extends HttpLogoutServlet {
+  private static final long serialVersionUID = 1L;
+
+  private final Provider<OAuthSession> oauthSession;
+
+  @Inject
+  OAuthLogoutServlet(AuthConfig authConfig,
+      DynamicItem<WebSession> webSession,
+      @CanonicalWebUrl @Nullable Provider<String> urlProvider,
+      AccountManager accountManager,
+      AuditService audit,
+      Provider<OAuthSession> oauthSession) {
+      super(authConfig, webSession, urlProvider, accountManager, audit);
+    this.oauthSession = oauthSession;
+  }
+
+  @Override
+  protected void doLogout(HttpServletRequest req, HttpServletResponse rsp)
+      throws IOException {
+    super.doLogout(req, rsp);
+    if (req.getSession(false) != null) {
+      oauthSession.get().logout();
+    }
+  }
+}
diff --git a/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthModule.java b/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthModule.java
new file mode 100644
index 0000000..f74e005
--- /dev/null
+++ b/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthModule.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd.auth.oauth;
+
+import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.inject.servlet.ServletModule;
+
+/** Servlets and support related to OAuth authentication. */
+public class OAuthModule extends ServletModule {
+
+  @Override
+  protected void configureServlets() {
+    filter("/login", "/login/*", "/oauth").through(OAuthWebFilter.class);
+    // This is needed to invalidate OAuth session during logout
+    serve("/logout").with(OAuthLogoutServlet.class);
+    DynamicMap.mapOf(binder(), OAuthServiceProvider.class);
+  }
+}
diff --git a/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java b/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java
new file mode 100644
index 0000000..d24c8a0
--- /dev/null
+++ b/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java
@@ -0,0 +1,264 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd.auth.oauth;
+
+import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
+import com.google.gerrit.extensions.auth.oauth.OAuthToken;
+import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo;
+import com.google.gerrit.extensions.auth.oauth.OAuthVerifier;
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.httpd.CanonicalWebUrl;
+import com.google.gerrit.httpd.WebSession;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountException;
+import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.account.AuthRequest;
+import com.google.gerrit.server.account.AuthResult;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.servlet.SessionScoped;
+
+import org.apache.commons.codec.binary.Base64;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+
+import javax.servlet.ServletRequest;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@SessionScoped
+/* OAuth protocol implementation */
+class OAuthSession {
+  private static final Logger log = LoggerFactory.getLogger(OAuthSession.class);
+  private static final SecureRandom randomState = newRandomGenerator();
+  private final String state;
+  private final DynamicItem<WebSession> webSession;
+  private final Provider<IdentifiedUser> identifiedUser;
+  private final AccountManager accountManager;
+  private final CanonicalWebUrl urlProvider;
+  private OAuthServiceProvider serviceProvider;
+  private OAuthToken token;
+  private OAuthUserInfo user;
+  private String redirectToken;
+  private boolean linkMode;
+
+  @Inject
+  OAuthSession(DynamicItem<WebSession> webSession,
+      Provider<IdentifiedUser> identifiedUser,
+      AccountManager accountManager,
+      CanonicalWebUrl urlProvider) {
+    this.state = generateRandomState();
+    this.identifiedUser = identifiedUser;
+    this.webSession = webSession;
+    this.accountManager = accountManager;
+    this.urlProvider = urlProvider;
+  }
+
+  boolean isLoggedIn() {
+    return token != null && user != null;
+  }
+
+  boolean isOAuthFinal(HttpServletRequest request) {
+    return Strings.emptyToNull(request.getParameter("code")) != null;
+  }
+
+  boolean login(HttpServletRequest request, HttpServletResponse response,
+      OAuthServiceProvider oauth) throws IOException {
+    log.debug("Login " + this);
+
+    if (isOAuthFinal(request)) {
+      if (!checkState(request)) {
+        response.sendError(HttpServletResponse.SC_NOT_FOUND);
+        return false;
+      }
+
+      log.debug("Login-Retrieve-User " + this);
+      token = oauth.getAccessToken(new OAuthVerifier(request.getParameter("code")));
+
+      user = oauth.getUserInfo(token);
+
+      if (isLoggedIn()) {
+        log.debug("Login-SUCCESS " + this);
+        authenticateAndRedirect(request, response);
+        return true;
+      } else {
+        response.sendError(SC_UNAUTHORIZED);
+        return false;
+      }
+    } else {
+      log.debug("Login-PHASE1 " + this);
+      redirectToken = request.getRequestURI();
+      // We are here in content of filter.
+      // Due to this Jetty limitation:
+      // https://bz.apache.org/bugzilla/show_bug.cgi?id=28323
+      // we cannot use LoginUrlToken.getToken() method,
+      // because it relies on getPathInfo() and it is always null here.
+      redirectToken = redirectToken.substring(
+          request.getContextPath().length());
+      response.sendRedirect(oauth.getAuthorizationUrl() +
+          "&state=" + state);
+      return false;
+    }
+  }
+
+  private void authenticateAndRedirect(HttpServletRequest req,
+      HttpServletResponse rsp) throws IOException {
+    AuthRequest areq = new AuthRequest(user.getExternalId());
+    AuthResult arsp;
+    try {
+      String claimedIdentifier = user.getClaimedIdentity();
+      if (!Strings.isNullOrEmpty(claimedIdentifier)) {
+        if (!authenticateWithIdentityClaimedDuringHandshake(areq, rsp,
+            claimedIdentifier)) {
+          return;
+        }
+      } else if (linkMode) {
+        if (!authenticateWithLinkedIdentity(areq, rsp)) {
+          return;
+        }
+      }
+      areq.setUserName(user.getUserName());
+      areq.setEmailAddress(user.getEmailAddress());
+      areq.setDisplayName(user.getDisplayName());
+      arsp = accountManager.authenticate(areq);
+    } catch (AccountException e) {
+      log.error("Unable to authenticate user \"" + user + "\"", e);
+      rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
+      return;
+    }
+
+    webSession.get().login(arsp, true);
+    String suffix = redirectToken.substring(
+        OAuthWebFilter.GERRIT_LOGIN.length() + 1);
+    StringBuilder rdr = new StringBuilder(urlProvider.get(req));
+    rdr.append(Url.decode(suffix));
+    rsp.sendRedirect(rdr.toString());
+  }
+
+  private boolean authenticateWithIdentityClaimedDuringHandshake(
+      AuthRequest req, HttpServletResponse rsp, String claimedIdentifier)
+      throws AccountException, IOException {
+    Account.Id claimedId = accountManager.lookup(claimedIdentifier);
+    Account.Id actualId = accountManager.lookup(user.getExternalId());
+    if (claimedId != null && actualId != null) {
+      if (claimedId.equals(actualId)) {
+        // Both link to the same account, that's what we expected.
+        log.debug("OAuth2: claimed identity equals current id");
+      } else {
+        // This is (for now) a fatal error. There are two records
+        // for what might be the same user.
+        //
+        log.error("OAuth accounts disagree over user identity:\n"
+            + "  Claimed ID: " + claimedId + " is " + claimedIdentifier
+            + "\n" + "  Delgate ID: " + actualId + " is "
+            + user.getExternalId());
+        rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
+        return false;
+      }
+    } else if (claimedId != null && actualId == null) {
+      // Claimed account already exists: link to it.
+      //
+      log.info("OAuth2: linking claimed identity to {}",
+          claimedId.toString());
+      try {
+        accountManager.link(claimedId, req);
+      } catch (OrmException e) {
+        log.error("Cannot link: " +  user.getExternalId()
+            + " to user identity:\n"
+            + "  Claimed ID: " + claimedId + " is " + claimedIdentifier);
+        rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
+        return false;
+      }
+    }
+    return true;
+  }
+
+  private boolean authenticateWithLinkedIdentity(AuthRequest areq,
+      HttpServletResponse rsp) throws AccountException, IOException {
+    try {
+      accountManager.link(identifiedUser.get().getAccountId(), areq);
+    } catch (OrmException e) {
+      log.error("Cannot link: " + user.getExternalId()
+          + " to user identity: " + identifiedUser.get().getAccountId());
+      rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
+      return false;
+    } finally {
+      linkMode = false;
+    }
+    return true;
+  }
+
+  void logout() {
+    token = null;
+    user = null;
+    redirectToken = null;
+    serviceProvider = null;
+  }
+
+  private boolean checkState(ServletRequest request) {
+    String s = Strings.nullToEmpty(request.getParameter("state"));
+    if (!s.equals(state)) {
+      log.error("Illegal request state '" + s + "' on OAuthProtocol " + this);
+      return false;
+    }
+    return true;
+  }
+
+  private static SecureRandom newRandomGenerator() {
+    try {
+      return SecureRandom.getInstance("SHA1PRNG");
+    } catch (NoSuchAlgorithmException e) {
+      throw new IllegalArgumentException(
+          "No SecureRandom available for GitHub authentication", e);
+    }
+  }
+
+  private static String generateRandomState() {
+    byte[] state = new byte[32];
+    randomState.nextBytes(state);
+    return Base64.encodeBase64URLSafeString(state);
+  }
+
+  @Override
+  public String toString() {
+    return "OAuthSession [token=" + token + ", user=" + user + "]";
+  }
+
+  public void setServiceProvider(OAuthServiceProvider provider) {
+    this.serviceProvider = provider;
+  }
+
+  public OAuthServiceProvider getServiceProvider() {
+    return serviceProvider;
+  }
+
+  public void setLinkMode(boolean linkMode) {
+    this.linkMode = linkMode;
+  }
+
+  public boolean isLinkMode() {
+    return linkMode;
+  }
+}
diff --git a/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthWebFilter.java b/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthWebFilter.java
new file mode 100644
index 0000000..91c3e33
--- /dev/null
+++ b/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthWebFilter.java
@@ -0,0 +1,212 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd.auth.oauth;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.httpd.HtmlDomUtil;
+import com.google.gerrit.httpd.LoginUrlToken;
+import com.google.gerrit.httpd.template.SiteHeaderFooter;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.SortedSet;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@Singleton
+/* OAuth web filter uses active OAuth session to perform OAuth requests */
+class OAuthWebFilter implements Filter {
+  static final String GERRIT_LOGIN = "/login";
+
+  private final Provider<String> urlProvider;
+  private final Provider<OAuthSession> oauthSessionProvider;
+  private final DynamicMap<OAuthServiceProvider> oauthServiceProviders;
+  private final SiteHeaderFooter header;
+  private OAuthServiceProvider ssoProvider;
+
+  @Inject
+  OAuthWebFilter(@CanonicalWebUrl @Nullable Provider<String> urlProvider,
+      DynamicMap<OAuthServiceProvider> oauthServiceProviders,
+      Provider<OAuthSession> oauthSessionProvider,
+      SiteHeaderFooter header) {
+    this.urlProvider = urlProvider;
+    this.oauthServiceProviders = oauthServiceProviders;
+    this.oauthSessionProvider = oauthSessionProvider;
+    this.header = header;
+  }
+
+  @Override
+  public void init(FilterConfig filterConfig) throws ServletException {
+    pickSSOServiceProvider();
+  }
+
+  @Override
+  public void destroy() {
+  }
+
+  @Override
+  public void doFilter(ServletRequest request, ServletResponse response,
+      FilterChain chain) throws IOException, ServletException {
+    HttpServletRequest httpRequest = (HttpServletRequest) request;
+    HttpServletResponse httpResponse = (HttpServletResponse) response;
+
+    OAuthSession oauthSession = oauthSessionProvider.get();
+    if (request.getParameter("link") != null) {
+      oauthSession.setLinkMode(true);
+      oauthSession.setServiceProvider(null);
+    }
+
+    String provider = httpRequest.getParameter("provider");
+    OAuthServiceProvider service = ssoProvider == null
+        ? oauthSession.getServiceProvider()
+        : ssoProvider;
+
+    if (isGerritLogin(httpRequest) || oauthSession.isOAuthFinal(httpRequest)) {
+      if (service == null && Strings.isNullOrEmpty(provider)) {
+        selectProvider(httpRequest, httpResponse, null);
+        return;
+      } else {
+        if (service == null) {
+          service = findService(provider);
+        }
+        oauthSession.setServiceProvider(service);
+        oauthSession.login(httpRequest, httpResponse, service);
+      }
+    } else {
+      chain.doFilter(httpRequest, response);
+    }
+  }
+
+  private OAuthServiceProvider findService(String providerId)
+      throws ServletException {
+    Set<String> plugins = oauthServiceProviders.plugins();
+    for (String pluginName : plugins) {
+      Map<String, Provider<OAuthServiceProvider>> m =
+          oauthServiceProviders.byPlugin(pluginName);
+        for (Map.Entry<String, Provider<OAuthServiceProvider>> e
+            : m.entrySet()) {
+          if (providerId.equals(
+              String.format("%s_%s", pluginName, e.getKey()))) {
+            return e.getValue().get();
+          }
+        }
+    }
+    throw new ServletException("No provider found for: " + providerId);
+  }
+
+  private void selectProvider(HttpServletRequest req, HttpServletResponse res,
+      @Nullable String errorMessage)
+      throws IOException {
+    String self = req.getRequestURI();
+    String cancel = Objects.firstNonNull(
+        urlProvider != null ? urlProvider.get() : "/", "/");
+    cancel += LoginUrlToken.getToken(req);
+
+    Document doc = header.parse(OAuthWebFilter.class, "LoginForm.html");
+    HtmlDomUtil.find(doc, "hostName").setTextContent(req.getServerName());
+    HtmlDomUtil.find(doc, "login_form").setAttribute("action", self);
+    HtmlDomUtil.find(doc, "cancel_link").setAttribute("href", cancel);
+
+    Element emsg = HtmlDomUtil.find(doc, "error_message");
+    if (Strings.isNullOrEmpty(errorMessage)) {
+      emsg.getParentNode().removeChild(emsg);
+    } else {
+      emsg.setTextContent(errorMessage);
+    }
+
+    Element providers = HtmlDomUtil.find(doc, "providers");
+
+    Set<String> plugins = oauthServiceProviders.plugins();
+    for (String pluginName : plugins) {
+      Map<String, Provider<OAuthServiceProvider>> m =
+          oauthServiceProviders.byPlugin(pluginName);
+        for (Map.Entry<String, Provider<OAuthServiceProvider>> e
+            : m.entrySet()) {
+          addProvider(providers, pluginName, e.getKey(),
+              e.getValue().get().getName());
+        }
+    }
+
+    sendHtml(res, doc);
+  }
+
+  private static void addProvider(Element form, String pluginName,
+      String id, String serviceName) {
+    Element div = form.getOwnerDocument().createElement("div");
+    div.setAttribute("id", id);
+    Element hyperlink = form.getOwnerDocument().createElement("a");
+    hyperlink.setAttribute("href", String.format("?provider=%s_%s",
+        pluginName, id));
+    hyperlink.setTextContent(serviceName +
+        " (" + pluginName + " plugin)");
+    div.appendChild(hyperlink);
+    form.appendChild(div);
+  }
+
+  private static void sendHtml(HttpServletResponse res, Document doc)
+      throws IOException {
+    byte[] bin = HtmlDomUtil.toUTF8(doc);
+    res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+    res.setContentType("text/html");
+    res.setCharacterEncoding(StandardCharsets.UTF_8.name());
+    res.setContentLength(bin.length);
+    try (ServletOutputStream out = res.getOutputStream()) {
+      out.write(bin);
+    }
+  }
+
+  private void pickSSOServiceProvider()
+      throws ServletException {
+    SortedSet<String> plugins = oauthServiceProviders.plugins();
+    if (plugins.isEmpty()) {
+      throw new ServletException(
+          "OAuth service provider wasn't installed");
+    }
+    if (plugins.size() == 1) {
+      SortedMap<String, Provider<OAuthServiceProvider>> services =
+          oauthServiceProviders.byPlugin(Iterables.getOnlyElement(plugins));
+      if (services.size() == 1) {
+        ssoProvider = Iterables.getOnlyElement(services.values()).get();
+      }
+    }
+  }
+
+  private static boolean isGerritLogin(HttpServletRequest request) {
+    return request.getRequestURI().indexOf(GERRIT_LOGIN) >= 0;
+  }
+}
diff --git a/gerrit-oauth/src/main/resources/com/google/gerrit/httpd/auth/oauth/LoginForm.html b/gerrit-oauth/src/main/resources/com/google/gerrit/httpd/auth/oauth/LoginForm.html
new file mode 100644
index 0000000..f7814c0
--- /dev/null
+++ b/gerrit-oauth/src/main/resources/com/google/gerrit/httpd/auth/oauth/LoginForm.html
@@ -0,0 +1,58 @@
+<html>
+  <head>
+    <title>Gerrit Code Review - Sign In</title>
+    <style>
+      #error_message {
+        padding: 5px;
+        margin-left: 5px;
+        margin-bottom: 5px;
+        width: 20em;
+        background-color: rgb(255, 255, 116);
+        font-weight: bold;
+      }
+      #cancel_link {
+        margin-left: 45px;
+      }
+      #logo_box {
+        padding-left: 160px;
+      }
+      #logo_img {
+        width: 239px;
+        height: 240px;
+        background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAO8AAADwCAYAAADo8DP3AAAABmJLR0QA/wD/AP+gvaeTAAAgAElEQVR4nOxdd1gUx/t/7zh6FREEFEFEsSuI2AuiUVAUv6KYoFgQRFQUBESMXmyx9xISFbuxJbEX7F1jolERe6JGBQSRpoJw7+8PXH53t7N7u8dxd5j7PM/n8XGZ3Zmdmc/tzPvOvAOggw466KCDDjrooIMOOuiggw466KCDDjrooIMOOuiggw466KCDDjrooIMOOuiggw466KCDDjrooHkINF0AHSoPsVhsVlpaWqu0tNS6rKzMTCKROEgkEoNPnz7ZUWlEIlG2QCD4KBKJXurp6RWKRKK3IpHotVgs/qjJsuugPHTi1WKIxWKDDx8+tC0qKmpXUFDQqqCgwDU/P79WQUGB1du3b01zcnIM3759W6k2FAgEUKtWLYm1tXWxtbV1gYWFRa65uXmGiYnJKxMTkyeGhoYPTU1NT8+dO/elqt5LB9VAJ14tQUJCQqf8/Pyv3r592y4zM9P9xYsXti9evDAoKSnRdNFAIBBA3bp1Pzk7O7+xs7N7WLNmzauWlpZHFyxYcF7TZfsvQydeDUAsFtvk5OR8/ebNm77Pnz9vkZ6ebpubm1vt2sLCwgIbNGiQ5+rqert27doHatasmSIWi99qulz/FVS7DlMdIRaLTXJzc0e8evVq8MOHDz3u3btn/unTJ00XS+UwMDCApk2b5n8W834bG5tNYrE4W9Pl+lKhE28VYerUqa1fv34d8+TJE99bt27ZFRYWqrWuBQIBWFlZ0a4XFBRAaWmpWspgaGgIXl5eGe7u7r/a2trOnjdv3mu1ZPwfgU68KkRcXJzfs2fPYtPS0rzv3btniogqea6+vj44ODhA3bp1wcnJCerWrQu1a9cGGxsbsLGxATs7O6hVqxaYmpqCnp4eWFhYcHpuUVERvHz5EjIzM+Hly5eQkZEBr169gpcvX8KDBw/g/v37UFRUpJJ3MDY2Bi8vr1fu7u67HRwc5uq+yJWHTryVRHx8fLsXL15Mv3nzps/9+/eNK/MsQ0NDaN26NTRr1gwaNWoETZo0AXd3d3B2dgahUKiqInMGIsLz58/hwYMHkJ6eDnfv3oWrV6/CvXv3QCKRKP1cU1NT8Pb2ftasWbNFK1euXKPCIv+noBOvEjhy5IjhyZMnZ964cSPi4sWL1sp2ZGtra+jcuTN07NgROnToAG3atAFDQ0MVl1b1yM/Ph6tXr8LVq1fhypUrcOnSJSgoKFDqWU2bNi3u2rXrLy1btpwYERGh+xrrUDUQi8U2ISEhe5ycnEoAAJVhy5YtMTExES9evIilpaX4JaCkpATPnj2LiYmJ6OHhgQKBgHe9WFtbl/3vf/+7NHXqVC91tWd1h+7LywFJSUl1nzx5sv7kyZO+2dnZvMav+vr68NVXX0G/fv3Az88P6tSpU+nySCQSyMjIgH///Rdev34NhYWFUFBQAHl5eZCXlweFhYVQXFxckb+ZmZnMv7Vq1QI7Ozuwt7cHW1tbqFWrlkqH5ZmZmXD06FHYtWsXnDx5kpeBTCQSQbdu3Z56eHiEL1y48JTKCvUFQideFiQmJjZ8+PBhSmpqavv8/HxeddWmTRsYPnw4BAcHQ61atXjn/eHDB3j48CE8fPgQHjx4AA8ePIDHjx/DixcvIDMzU6UWYz09PbC1tYXGjRtD48aNoWnTptC4cWNo1qwZ2NjYVOrZWVlZsHv3btixYwdcvXoVkKMRTyQSgY+Pz6OWLVuOWbRo0blKFUKH/w42btxYa+TIkWcsLS0lwGPo5+joiAkJCZiWlsZr2FlWVoZ37tzBDRs24JgxY7Bly5aop6en1LBc1bS3t8fAwEBcuHAhXrhwAd+/f6/08Prx48eYkJCANjY2nPPX19fHfv36PZ07d253VbaxDl8YxGKxMCwsbK2jo+Mn4NHB27Vrhz///DN++vSJc0d+8uQJ/vDDDzho0CC0trbWuEi5Ul9fH728vDAhIQFTU1OVEvOHDx9w06ZN6OXlxTlfAwMD7Nev362kpKS6KmhqHb4kTJw4cULTpk0LgWNnEolEOGTIELxy5QqnDvvp0yc8ffo0Tpo0CV1dXTUuQlXRyMgIfX19cf78+Xjv3j3eQr527RoGBgZyNnLZ2tqWhYWFrROLxer3nemgXViyZElrPz+/F1w7j5GREUZHR+OzZ884CfbQoUM4bNiwavV1rQybNm2KM2fOxDt37vAS8V9//YWDBg1CoVDIKR8vL6/8WbNmBSvZ7DpUZ4jFYlFoaOgOKysrTvNaQ0NDHD9+PL58+VJhR7x9+zbGxMRg7dq1q1ws+vr6WKNGDRlWdZ5c6e7ujmKxGB8+fMhZxHfu3MHBgwdz+hLr6+vjgAEDronFYlslu4EO1Q2xsbFDmzdvXgAcOqCBgQFGRkbiixcvWDtdSUkJ7ty5E729vdUqkIEDB9LKIhKJaOn69+9PS2dgYKC2crZt2xaXL1+Or1+/5iTi69evY+fOnTk928XFpWTy5MkjlewOOlQH7N6923LYsGF/kDo3iQMHDsQnT56wdrKcnBycN28eOjo6auTr9r///Y9WJtLQc8CAAZxEXtXU09PD/v3747Fjx7CsrEyhiPfu3YsNGjRQ+FyRSIShoaFXdu/eXaklqjpoIZKSkoY2btz4A3DoYC1btsTTp0+zdqqsrCycOnUqmpuba0S0FAcNGkQrG2nIGRgYSEvH1x21evVqTEpKwhYtWqik7K6urrhw4ULMzs5mrevi4mJctGgRmpmZKXymh4dH0fz58/vw7yE6aB3EYrFRcHDwCS5DxFq1auEPP/zAumwxMzMTJ0+ejKampkp32gYNGuDQoUNVIoCgoCBaGUnpSMNrrsYhAEAbGxuZesnKysItW7ZgUFAQJ1Gx0dTUFGNjYxUOqZ8/f44DBgxQ+DwzMzPJ6NGjf1Kqw+igHYiPj2/XokULTnPbkJAQ1i9Abm4uJiQkoImJiVId1M7ODmfMmIG3bt2qeObUqVMrLd7BgwdzEi9peM1nDfLw4cMZ6+b9+/eYmpqK0dHR6ODgoPS76Ovr47BhwxQauPbv34/16tVT+Dx/f/+7YrHYRMnuo4OmEB8fP6dmzZoKLcl169bFQ4cOMXaUsrIyTE5ORnt7+0qJbMSIEbRnSyQSHDZsWKWeO2TIENozSelIw2s++SxfvpxVUBQ+ffqEp06dwkmTJmH9+vWVeicjIyOMj4/Hd+/eMeZTUFCAYWFhCp/l6elZsHjxYk8lu5EO6sSRI0cMQ0JCrioaEgoEAhw7dizm5eUxdpDU1FRs1KhRpcRFMSEhgZhHSUkJdu/eXennBgcHyzyvrKyMmI7r8JqNDRo0wMmTJ+Pp06c5ryZTVsAAgFZWVjh//nz88OED4/P37duHNWvWZH2Og4ND2bfffjtWuR6lg1qQmJjY0NvbOxsUdApHR0c8ceIEY4fIycnB8PBwXnNCRVyyZAlrfg0bNlTquUOHDpV5VmlpKTGd/PCa6QvNlXFxcQqFe//+fZXUXePGjVkNiP/++y/6+PiwPsPIyAh182AtxeTJk0MdHBxKQUFHGDx4MObk5BA7gUQiweTkZLSyslKZaClu3bqVtaPfu3dPqXxJ4iX96Min+/TpU6Xe58yZMzLP+/3333HdunUy/vCFCxeqtA6DgoIwMzOTWH9lZWU4a9YshRb0IUOGpPLqWDpULcLDw5caGhqyNpqNjQ3u27ePUTyvXr3iZMlUlqmpqTL5zZgxg1aGCxcuoKL3kKe/vz/tOXXq1KGli4+Pl0mTmZmp9LtYW1vThs2RkZEIUD4d8fDwwBkzZmDr1q1VXo92dna4c+dOxnY8f/68QvuEn59fmlgsFvHpYzpUASIjIzfr6+uzNpaHhwc+ffqUscF37NiBFhYWVSZcAMDbt2/L5GllZYXLli2jleXnn3/mZQVu0aIF7Rnx8fEyaYRCIV6/fl0mzdWrV5V+l9DQUJlnSSSSSlmZleFXX33F6Fp6/vy5wpVufn5+z7Zs2WLKsZvpoGqMHDnyrKKOPnr0aEaDR2FhYaWtvVyZkZFRkW9ZWRkKhULU19fHc+fO0cqVmJjI+blCoZDm4vrw4QN+++232LFjR+zduzcePXqUlsfcuXOVfpe9e/fKPOv69esqqaMJEyZgdHQ0Z1uDnZ0d8d0Qyxd2hIaGst7frVu3Nxs3buQfKUEH5SEWi0X9+vW7BSwNY2RkhOvXryc2LCLi33//jR4eHmoRrp6enswCh5ycnIq/de3alTYElUgkOHjwYM7P/+677xjfk4QPHz4Qh9ZcaGRkhAUFBTLPmz59eqXrqFmzZvjx40dELB8VuLu7c65bsVhMXGopkUhw5syZrCMZLy+vt2Kx2Jp779NBaYjFYiMfH59nwNKgTk5OeOPGDcbO+9tvv1X5MFmadnZ2MvlnZWXhrFmzWCNufPjwAdu3b8/p+cbGxqzvKw8u/lEm+vn50Z5X2eWSQqEQL126JPPMgoICTmuaKXbp0kVmdCONlJQUZJtatWvX7o1YLDbj0Q114IuVK1ca9uzZ8zmwNGLXrl0xKyuL2IhlZWWYkJCgVHTDypA0L+WCnJwczn5mS0tL3LRpE+vSzuzs7Eovy1y3bp3MM58+fVrp+iH5wPfs2cP7OXXq1MGrV68S3/3EiROsS1q7du2auWbNGp2AqwJnzpwR+fv7/w0sjRcVFYUlJSXExnv//j2GhIRw+gr4+vriDz/8gL///js+efIE09PTcfv27ThkyBClYkv5+vqyivTDhw944MABPHDgAO1v9+7d47VH18nJCSMiInDdunW4e/du3LJlC86fPx+DgoLQ2Ni4UiITCAS0/cwrVqyo1DObNm1aMVymkJmZySvOlTRNTU1xz549xHq+dOkSqzuuR48eL1JSUoz49EsdFEAsFgv9/f3vAEunWrBgAaM40tPTOc2hPD09aVZZedy9e5f3MPGbb76hPScvLw937Nghs7DfwMAAz549S0ujrrm5IrZp04b2Hj169FD6eUKhEM+fP0975vDhwxl/mLgsZhEIBDhz5kxi+/3xxx+sPwy9evV6pHMjqQiIKAgKCmIUrp6eHm7YsIFRbNevX0dbW1uFDR4UFMS6DE8aRUVFvIKnxcTEyNx/6NAhRn9uzZo1ceXKlejv78/b51vVnDVrlsx7vH37lnUuqYhTpkyh1e3evXsZhX7p0iUsKSlBsVjMaQQUHx+PEomElkdaWhqrLzgoKOiawo6pg2IMGjToAjBUsoGBAevCi1OnTnHab9uxY0dekR8RETMyMrBWrVqcOqn8qGDVqlUaF6Iy7NKlC65fv75ildOuXbuUfparqysWFhbS6nXJkiXEH4SxY8fKpDt79iynnUUhISHEqdTdu3dZv8C6pZSVxLBhw3YDQ+UaGxvj8ePHGcW1adMmTpEi9PX1aQs4CgsLcd68edi+fXv09vbGuXPnEpdUrlu3jlNH3bRpk8x93333ncaFWBkKhULs1KmT0sN5PT091qibd+/elVlk4ebmhkVFRTJp3r59y3lhSPfu3WnuLcTyWGNMgQH19fVx/Pjx8Qq6qA4kREZGfsc0NDIzMyMubqCwbNkyzhZl+dVCRUVFxNU5jRs3phlrioqKOG1EP3LkCCIi/vnnn5iUlIR169bVuAA1SflpBAnUVkxzc3OaGwkRcfTo0bzybNu2LfEH+Nq1a4yjM3Nzc8mUKVP6M3ZSHeiIi4vrxRTVUV9fn2iVpbBkyRJeriD5Z82ePZsxbd++fWn5+fn5Kcxj8ODBX1Ss5srQxcWF9hXMyclhHEW9evWKdu3kyZNKufu8vb0xNzeX9rwjR44wzt1dXV2Lk5KS6hG6qQ7ySExMbFKnTh3iiQUikQh//fVXRuF+++23vBtUvnOw7a0VCAS0eM3jxo3TuCCqC/X09PDy5cu0dhsyZAgClBsNFcWz4jNcJrFjx47EufbOnTsZl2V6eXnliMViA9CBGQsWLDBv06bNO2CoePkFAtJg+2KyUd7CrOhLKu/aoHbTaCP37NmDaWlplfbvqoqRkZG0dtu/f79Mmnr16tF2YElj7NixlS5Hr169aL5lRMQ5c+Yw3jNs2LBjoAMz/Pz80oCh8uS3tEljyZIlSjfk8+fPZZ61YMEC1vT379+XSd+vXz+1dHy+i0P69euHR44cwUWLFuHEiRN53SsQCLBOnToqX4nWsGFDmR+/nJwcostGIBDQjHyIyg+XSRw4cCBxNRrTj7FIJMLo6OhxoAMdYWFha4GhooODg4n+OsRyi29lGlR+zpufn8/ogujXr59M2uLi4ipbI62vr4+WlpYIUL5o//Tp0wrDvUiX88iRIxXB8r7//nvOAhYKhfjjjz9WDGWtrKywd+/eKj21MCgoCHNycirykKeTkxMtPFFlh8skhoeH0/oTW1yxWrVqlU2dOrW1fN/9TyMxMdGbyUDVvHlzzM/PJwp3+/btlQ5VM2zYMNpzHz16hG3atJFJ17NnT3z79q1MupSUFJWLViQSYWxsLLZs2VLmuqenJycBywuXIhcBywuXYu3atfHYsWMqDSzPtkiCZJBUxXCZxO+//56W1/v377Fjx47E9N7e3tm6FVifIRaLTZo1a0Y8na9mzZqMB3opE3WCRH19fcbdPVevXsXdu3cTd+zk5eWhi4uLSjuSubk5Hj58GD09PYl/VyRgJuFSZBMwk3Ap1q5dG0+cOMFrp48yHD16NK2ujxw5UmX5CQQCYmSOvLw8bNWqFfGeoUOH6ua/AADBwcHngaFSf/nlF6KoHj16pPTCdRI9PDyIFkgmfPr0CQcOHFipPOU3G1hYWLAKl2K7du3w3LlzNAErEi5FkoAVCZcik4AdHBxUImp1DZflaWhoiBcuXKC18z///IN2dna09MbGxjh9+vS+8n35P4W4uLhoprkUad0rYvmOE1V/8QAAe/fuTfQByuPdu3fo7+9fqbyMjIzw8OHDGBAQwEu4FNu3by8jYK7CpSgtYEq4wcHBnO6VF3CTJk3w0qVL6OTkVOk26NevH20llbqs+dbW1sRQSefOnSP6gFu1alX4n92BNG3aNDumE+i9vLyI61FLS0sVhvisDF1cXDAlJYXoRsjNzcU1a9Zw2uTAhcbGxnj48GEMDg7mJVyKlIC//vprXsKl+P3332N0dDQv4VKkBNy7d2+8fPmySoRL0d7eHn/77TdErNrhMokdOnTA4uJiWtszbXscOnToIfgvom/fvsQwNhYWFozB4uQDq1UVTU1N0c/PD8PDw3HkyJHYqVOnKtnhY2dnh1lZWTh58mSl7o+Li8OcnBylQtoIhUK8efMmbtq0Sam8u3fvjnl5edilSxeV14tAIMCIiAh0dnZWS3tLc8yYMcS+R4o+YmBggLGxsQPhv4SYmJghTJsGmLb3paamqjQIuqZJDZU7dOiAhw8f5u0vpobK3bt3J86B2SgUCjE5ORmHDh2Ky5cv5+0Hbty4MV6+fBm9vb3xzJkzVW7EUjd37NhB63/v378nRjVp2bJlvlgsFsJ/AWKx2KhRo0bEozYDAgKIwn39+jXRcKCN5OJztrCwwGPHjmG7du0Q4P+H0FwFLD/HlZ8Ds1F+jisQCHgJuHHjxjJz3Dp16vASsLas9mKjlZUV0cvx559/Eue/I0aM2AL/BYSEhPwChAozNzcnVlhZWVmlojWok507d8YdO3awbkWkhCsfXI6rgJmMU1wEzGSc4ipgeeFS5CrgqKgonD9/vsbbiQu7dOlCjEhJWj9fo0YNSWJiYkP4kjFr1qzWlpaWxMUYCxcuJH51ly1bpvGG5MNRo0bhzp07iQJmEi5FRQJWZFVmE7Aiq7IiAVNDZSbjlCIBR0VF4fLly9Ue/K8yJC3g+PjxIzZt2pSWtn///nfgS4aPj88/QKik5s2bEyNZPHz4UOnzcDVJkoAVCZcik4C5uoNIAqbmuIqsykwCViRcikwCro7CBSg3SMmfdIFY7j6STysSiTAmJiYIvkRMnjx5JKnxBAIBcWN9WVkZ4xK16kBpAXMVLkV5AfP140oLmKtwpdtDWsBchUtRXsDVVbgU27VrRxw+k4Lit27dOu+LNF61bt06DwiVQzp0GhGrzdyIjaNGjcK9e/fi8ePHOQuXorGxMR45cgRnz56tlB+XEvDmzZt5+3EpAc+ZM0cpPy4lYLFYXK2FS5F0ptS///5LjAMdHh6+GL4kTJw48VsgVIqpqSkxUsLTp0+rhVVSES0sLPDOnTuYmprKKZ6WPAcNGoRZWVk4aNAg3vcKhULcv38/3r9/n5cbiWKTJk3w1atXOGPGDKXePSkpCV+8eIFubm4abwdVtOO///5L66fTpk2jpW3UqNGH5ORkffgSIBaLRQ0bNiS6hsRiMfGrW5VHbVaGfNZTSw+VR40apdAKLc9+/frh0aNH0cbGBg8fPox9+/blfC9lnBoxYgQvNxJFaqhcr149XLFiBU6YMIFXPVFD5bp1634xfuCgoCBaP3337h0xIH5YWNg6+BIwevToH4BQGbVr1yZG9Dt06JDGG4rEGjVq4KVLl7BJkyYK05LmuHwETAmXGiqbmJhwFrC0cKlrHTp04Cxg+TmuQCDgJWD5OS4fAZuYmOCiRYu0dph94sQJWn+dOXMmLZ2zs3OJWCw2geqMNWvWmNWrV4+4fnn58uW0iiguLuZ8OpwmWLduXbx8+TKrgNmMU1wELC9cilwETBIuRUrATKFOAQAbNGiA58+fp81xuQqYyTjFRcAWFhZ4/Phx7Ny5s8bbmYmenp4041VOTg4xIENYWNhGqM4YOXLkZiBUgq2tLb5//54m3kWLFmm8gRTRycmJ8QtMCbdDhw6M97MJuG/fvkThUjQxMcEjR44QBcwmXIodOnTAs2fPEgXMJFyKigQcFRWFK1asYPxq1q1bF8+ePUuMolkdhEsxJSWF1m+nTJlCet9P1fbrKxaLhW5ubh+BUAFz5syhVUBubi7rIVDaRJKAuQiXIknAffv25WRVJgmYi3ApkgSsSLgUmQSsSLgUqS+wtIDNzc2rjXAByvcuy+/9fv78OXHZ5KhRo9ZDdcS4ceNmAuHlraysaBuuEcmWO20mJeDGjRvzEi5FaQFzFS5FSsD+/v68hEtRegjNVbgU5QXMVbgUpQVc3YRLkbTyihT3ys3N7WO19Pu2b98+GwgvHhsbS3txpnmDttPJyQmvXr2KFy5c4CVciqNGjcKzZ8/isWPHePtxKQEfO3aMl3ApdujQAa9fv66UH1cgEODKlStx9+7dvIRLsW7dunjhwgW8cOFCtRMuQPlWTvmgAb///jsxbXR0dAJUEarkVyE2NnbQlStXaspfNzIygri4OFr6BQsWQH5+flUUpUrx7t07+PjxI1haWkJubi7v+7OysqBmzZqQl5cHJSUlvO79+PEjZGRkgL29Pbx580apvA0MDEAgEEBhYSGvexERHj58CA0bNoQnT54AIvK6/927dyCRSMDMzAxevXrF615tQGZmJqxatUrmWps2baB9+/a0tFevXp2irnKpBL17934AhF8h0kbnN2/ecDrJTx3kc4i19FBZegjN9X5p49SoUaNw+/btnMOrCoVCXLNmDfr4+KC+vj62a9eOV9mNjIywffv2aGpqiq6urvjLL7+wWqHlSQ2VhUIhbz+w9FCZzYil7SR9fZkCG8TGxg6C6oB58+a5k4aAQqEQ7927RxMvyU+mKc6YMQMTEhIUpiPNcfkImGRV5ipgoVCI/fv3RyMjI5W9t0AgwO7du1fEiWaj/ByXjx+YNMflI+Bu3bpxah91cfXq1TJ9ubCwkFiH/v7+96A64HNYTNoL+Pr60oRbWFjI6xe/qikQCHD16tUYHR3NmIbNOMVFwGzuIEUCFgqFMkEJrKysMDAwEL///ntMSUnB3bt3c+LWrVtx2bJlOHLkSJn5rqGhIevcm8k4Rc2B2QTMZpziImBFbjRN0NXVleb3JQXMMzExwenTp7uBNkMsFhs5ODiUAuFFd+/eTRPvjz/+qPEGkCebgLlYlZ2cnPDcuXPEBQlcOuCoUaNw48aNtJA/AoGgwh1hY2ODP/74I+2cJWUgkUjwxIkT6OXlVZEX6aseFRWFK1euZDROsQmYi1WZTcDaKFyK1PGtFC5dukRM9/XXXx8AbUZERMR8IBS8Vq1atKh8EomE01JDTZAkYHNzc87uIJKA+XTA0aNH44YNGyoELBQK0cDAAAHKh45ZWVmVFq08ysrKcOrUqRVlkN4Yoki40vUmL2A+7iCSgP39/bVWuADlIYPl+zXpuJzPax60F23bts0BwgtOmjSJ1lkOHz6s8Ypno0AgwDVr1mB0dHSFcPnsL5YWsDJfDkrAIpGoYqjcrVs3YlhaVUIsFiNA+Q+GhYUFZ+FK1xslYKre+LiDpAWs7cKl3lf+8LnY2Fhi2kmTJg0DbURcXJwfUwPfuXOH1km4HEqtaQoEAvzxxx8xPT1dqcAATk5OeOfOHTx37pxSHTAsLKwiLGytWrUUnl2rKlBB5T08PPDHH3/k7ccVCAT4ww8/4L1795Ty41a23tTNuLg4mfq7fv06MV3fvn3/AhVCZX7eBw8ezEWCv69t27bQrFkzmWsZGRlw4sQJVWVdZTAzMwMnJydIT08HT09P3ve3aNECsrOzQSgUgoODA697hUIheHh4QEpKCgAAzJgxA2rWpLnOqwTLly8HPT09+PPPP6GsrAxq1KjB634zMzNwdnaGe/fuQatWrXjn37x5c8jOzgaBQAD29va871c3fv75Z5BIJBX/9/LyggYNGtDSXbp0qblYLLZQZ9kUQiwWW1hbWxMDy61Zs4b2y14dNiBID5WpITSf+MbSQz42IxaJVOiaXr16VZSF6aTEqgL19XVwcGDczMBWb507d64YQo8fP17peqsufuCTJ0/K1B/TAQFjx479HrQJERERq4FQUAMDA9qxmBKJBBs2bKjRilbkSyXNcfkImDRX4ypgSrgjR46suBYaGqpG2ZZj586dFfmz7Z29WlwAACAASURBVEYi1Zv0UFkgEOCqVas4CZip3qqDgENCQmTq78aNG8R0nTt3fg3aBCZDVa9evWidgmk+oE4uX76ccS01m3GKi4DZjCyKBEwSLgDgr7/+qi7NVqCgoEDG4tyxY0dWAbMZp7gI2N/fn3GNtyIBC4VCHDNmjEb7lLm5OW3FVf369WnpDAwMMCkpyRG0AdOmTWvBdAzJ2rVraZ2Cb0iVqmDnzp3x+PHjtGWZXKzKbALmYh1lEjCTcE1NTYl7n9WBwMBAmbIwCZiLVZlNwGzCla43koCFQiH+9NNPrAtr1EXqgDQKUVFRxHRaE6QuNDR0JxAKKBQKMSMjQ+ZlSktLtebYEnkB83EHCQQCXLt2rYyA+bg15AXMJFwAwMDAQHXqVQY7duyglUdewHzcQSQB+/r64uHDhznXm7SAKeFOmjRJ4/0JgD69OXr0KDGdr6/vE9AGeHt7E7f+dejQgdYZLl68qPEKliYlYHt7e95+XGkBK+OPpATs5ubGKFwAwC1btqhJqnTID50pUgJ2cnLi7ceVFjAf4UrX29mzZ9HNzU2rhAtQHiGmtLS0ov4+fPhAfLcaNWpIxGKxAWgSYrHYgmmBPGnDsjYMbeT51VdfYXZ2Nvbs2ZP3vQKBAA8fPoz37t1Tyh/p7OyMr169klnZJE19fX1OB35XJZgiefbs2RNzcnJ4RbWUrrfffvsN09LSKlVvc+bM0Xj/kaf8AQJfffUVMd2kSZPCoJKolJ83Nzc35ONH8qqvQYPou6AOHNCu5Z3m5uYwefJkmDlzJkyZMgXMzc153e/n5wdCoRAuXrwIo0eP5nWvUCiExMREWLp0KfTp0wdcXV1pabp06QJWVla8nqtqBAXRT/AwNzeH2NhYmDFjBkyZMoW3H7hHjx5gYGAAFy5cgFGjRvG6VygUQlJSEvz000/QqVMnYr1pEocPH5b5v7+/PzHd8+fP+b24qsG0g6hJkya0X/AHDx5o/FdRmtRcrVOnTghQfiocyYjFROmhMjWE5mqMEwqF+MMPP+CoUaMQ4P+H0PLGGPktZ5pAfn6+zNCZqjfqUG1qCM11PzE1VDY1Na1YQ87VDyw/x9VGN5Knp6dM/T158oSY7nMsc82hU6dOr0kFI4W6WbFihcYrlqK8cClyFTBpjstVwPLCpUgJWDpeMilKvybQv39/BCh3c0gLlyJXAUsLV7reuAiYyTilbQIWCoW0qQ6pbAKBAKdOneoFmsDu3bv1mFZVHTx4kNYBlJkbVQWZhEtRkYDZ3BqKBExZleWFS9HJyanCvdC2bVt1aVMhtm3bhgCANWvWpAmXYqdOnVgF7Ovri0eOHCGe76NIwIqsykwjF03xwIEDMvUXHh5OTBcWFvYTaAKxsbEDSAUSiUS0pXylpaVaEWBOkXApMgmYiz+SScCKhEvR0NAQAQDnzZunTn2yIj8/n1PkDiYBswlXut5IAqaiYyqyKmuTgOU3KmzevJmYrlevXo9AExg5cuQmUoHatGlDa/ybN29qrCKp84W4CpeivIC5CJeiQCDAdevWVQhYIBBwEq4009PT1aVNTggICOBUbnkBcxGudL1JC5ircCnKC5hLnlVBb29vmbpjsvc4OzvzizqoKgQEBPxBKhBpvrtq1SqNiXfevHnYt29fXsKlSAk4ICCAtz9SWsCkOS4b3d3d1aVJzti6dSvn8lMCHjBgAGfhStfb6tWrccKECbyES5EScPfu3XHWrFka6XMGBgYy+64lEgnjIXVTp05tDeqGt7f3G1Jh9uzZQ2v4b775RiOVCFC+KyY7O5vXjiBpTp48Gd+8eYO2tra87xUKhZiWllYxZ+TKxMREdeqSE7gOnSlOmjQJc3Jy0NHRkXe96enpYVpaGu7bt0+pNuvSpQu+e/dOo4ez37x5U6b+mEYuERERC0FJKO3nffHiBdEB2bo1/Yfk6tWrymZTKYhEIti4cSN88803EBAQAD4+Przu9/X1BV9fXxgxYgRs376dlx9YIBDA2rVrYenSpVBQUADjx4/nfO+AAQN4lVMdMDc3h549e3JK26NHD+jVqxcMGTIEUlJSwMKC+xZWoVAI69atgw0bNkBGRgavegMAcHV1hTlz5oCfnx/MmzcPzMzMeN2vKty6dUvm/6SYzgAA2dnZHdVRngpMmzbNjhRdwdLSEiUSicwvTkFBgcaObIyJialYumdhYYGnTp1CHx8fTvdSczVqqNyzZ09MTU3l5AemIklQQ2VqCM3Fn1m3bl1aHWoLtmzZorD8PXr0kBkqd+rUCU+cOMHJYEnNcanoIdQmEK5+YFdXVzx//nxFDKl69erhvHnzNNL3Jk6cKFN3586dI6b7vLxYfZg8efLXpIJ07tyZ1uCXL1/WSOUJBAL09vaWucZVwExGFi4Clheu9HUuAo6KilKXFnnj3bt3FdZwEuWFS5GLgOWFK11vXAQsL1yKTk5OrGWuKnbq1Emm7pg+YjVr1iwDdSIsLGwtqcCkjqep8K5MRhJKwN27d+clXIpsAmYSrvTfFQk4NTVVXVpUCkz+eibhUmQTMJNwpeuNTcBMwqWoygD1XGltbU2ruzp16hDTJiYmNgF1YciQISdJhSAdmK2soagqySRgrm4NkoAVCZeiUCjElJQUYke0trbGkpISdWhQaZB8loqES5EkYEXCla7fNWvW0PbIurq64rlz5xiFq0m+e/dOpu569OhBTDdhwoRJoC74+/vfJhWCtLKqd+/eGq9EEuUFTFq6x0ZpAXMVLkV5AVOdediwYerSoNLIzc2VGYZyFS5FaQFzFS5FeQFrs3ABAP/44w+ZuiOdpgAAGBoaugPUha5du/5LKkRaWhqtsTUdr4qNlICnTJnCS7gUKQFv2LCBlx8X4P8FPGPGjIqN7fv27VOXBisFKjhdmzZtePtxAcoFnJqaips2beIsXIqUgGfOnMk6VNYG7t27V6belixZQkwXEBDwJ6gLLVu2zJcvgFAopAUELysr04ixgA/79++P2dnZ2KdPH973CgQCPHToEKalpSl10mGDBg3w0KFDCFB+no18DCRtBXUanqOjo1L1JhQK8cCBA/jgwQOlls26urriy5cv8dtvv9V4/2HjokWLZOrt4MGDxHSfN/ioB/Xq1SuRL4CdnR2tkV+8eKHxCmQjNce1t7dnNWKRSA2VR48ezcuNRLF+/fp4+fLliq9u//791aW9SiM3N7fi+JWlS5dit27dOL+39FC5c+fOnN1I0vV27tw5dHZ2xrVr1zLGidIGjhs3Tqbe0tPTiekaN278HtQF0jLB5s2b0xr5ypUrGq9AJsobpywtLTkLmLIajx49uuIaHwFTwnVzc6u4tmnTJnVpTyWgTrzQ19fHQ4cOcRIwaY5LhSLiImBKuNRQmdoEoq0CHjRokEydFRcX0w6QAygPiwPqwLRp0+xJBfXx8aE18G+//aaRSvP29mZdGMJkVeYiYJJwKXIRMEm4IpEIc3Jy1KU7lSAlJaWi/KampgoFzGac4iJgeeFKtwcXAbdr107t/bBbt260emMKnTtt2jQ7qGpMmzatFSnzoUOH0gqanJysEfF6enpicnIyUcCK3EFsAmYTLsWePXviiRMniAImCRcAsHv37urQm0ohPXQGBQKmznxiM06xDaGZhCv9fDYBjx07lrXNqorNmjWj1Zt821OMi4vrClWNz5nQMpdfDoaIOHv2bI2IF6B8Q4G8gLn6cUkC5iJciqQvMJNwAQBXrlypDr2pHPLGKpKAuQiXIknAioQrnQ9JwGPHjsXVq1drZImuvb09rc7kV/1J9ddQqGpMmTKlPynz6dOn0wqq6bCc0gLms68U5ATMR7gUe/XqVSFgNuFqU7gbvti4cSPtfaQFzEe4FKUFzFW40nUpLWBNChegPLCCPCg3mzyjoqKmQ1Xj8xmjtMxJkR/CwsI0Kl74LOBDhw4p5Y+kBHzgwAGlhl29evXCixcv4rVr1xiHS6TgBdUF8kNnipSADx48yNuPC58FfP78ebx48SI6OzvzupcS8M6dOzUqXIry7r9hw4YR04WFha0GnuC9JVAikRD3d5mYmNCuFRUV8X28ynHnzh2oX78+vH79Gt6/52eRz8/Ph3/++Qfc3Nzg6dOnvPN+/PgxWFtbw6dPn+D1a7IrTxu3/3GFlZUVcZvl+/fvISsrC1xdXeHmzZu8n/vy5UuwsLAAiUQCb9++5XUvIsLt27ehWbNmcP/+fUDCsbPqRHFxscz/mY5pLS0t5X30J2/xlpWVETMxNTWlXSssLOT7eJXC19cXYmJiwMvLC+7evQvr1q0DgUDA6V5qP+7ly5ehXbt2MH36dOjevTvnvOvXrw/btm2D/v37w6xZs+CXX34h7i0NDAzk/ExthHxcZ4FAAMnJyXDnzh3w8vKCuLg46Nq1K+fnubi4QEpKCgwYMACSkpJgz549vPYDR0REQPPmzaFly5bQpEkTiIqK4nxvVaC0tFTm/0wxrsvKyoyqvDCRkZHfAeGzv3XrVtqwimkhtjpIWnM7efJk/OGHHxQOpUhzXD5+YNIct1evXnjixAk0MzOruNaoUSP1jG+rEG/fvq0YOlNz3JiYGNoQumvXrgrrzcXFpWIBBnWN6VA4EiMiInDNmjUV7Uu1oyb9wPL2jLlz5xLTff3111V/IkFUVNQ0UuYk8XLd+K5qdujQgXGOq0jAVIOT5utcBMxmnKIELBKJEAAwISFBXRqrUlBHevTu3VtGuBS5CJgkXIpcBCwvXPn21JSA//77b5m6YgoOMGTIkFSoaowfP34KKXPSgViaEu/mzZtZjVMxMTFEAbMJlyIlYJI/08XFhVG4FH18fFBfXx8BAK9cuaImeVUt1q9fX1E3TO/NJmAXFxe8dOkS68HjbAJmEq58uyqz/ryyfPTokUxdff/990ziPQlVjYkTJ0aRMict79OUeLlEeZQXMBfhUiQJmItwpVmnTh2tDXfDF9nZ2RU/SGw0NTXFw4cPywiYi3ApkuJpKxIuRYFAIDNlURcfPHggU1fz588nphs6dKjsIUdVgejo6DGkzFNSUmiNqinxcmVMTAwuX74chUIhZ+FSlBYwX+ECAEZGRqpJWupBr169OL03JeAuXbrwEi5FaQGHhoZyEq4m+ezZM5l6YhLvN9988xvwhIjvDQKBgHgsYFkZPRSPoaEh38erFUuXLoXY2Fi4desWrFq1CtavX8/53ry8PBg4cCAcPXoULCwsIDAwEB494h4Avzq7iEgICgqCEydOKExXVFQEgwcPhkOHDoGNjQ0EBgbC48ePOedz/vx5AAC4fPkyXLlyBSIiIjTuDmKDsbGxzP+ZvB1CoZB3AHberiKhUEgUL8ktRPL9ahMEAgE0aNAA0tPToWnTppzdSBSsra1BT08PCgoKwNHRkfN9NWrU4OV2qg4IDAwEkYjbt8DW1haMjY0hOzsbHBwceOfl4uICjx8/hnr16mkstCtXyIuXCUKhsFhxKrl7+N6gr6+fSbqen59Pu8a14JoA5cf9448/YMiQIfD8+XNYtmwZZwG7uLjA9u3bISQkBHr37g0zZsyAbt26cbrXz88P9PX1K1F67UPNmjU5/SC5uLjAtm3bICQkBPr27QtTpkyBdu3acc4nNDQU2rZtCwMHDoS5c+fC3r17eZ+rrE4YGcm6b0k6AQAwMDDIqfLCJCUlOQJhzC5/uBIiYkREhEbmGbVq1WL9u0AgwGXLltHmuDExMbhs2TKFcyjSHJfNCi1P0qkSXwIURQolzXFNTU3xwIEDnLbskea4XI9llT5jWF3U19en1RFTHCu1rG0GACCtZx07diytoFOmTNGIeBcsWMAYZlMgEODSpUsZjVOKBMxmnOIiYGNjYywoKFCHltSON2/eVPiwSfXGZJziImA245QiAZuZmeG0adPU3g+trKxodRQcHExM+9kQXPWwsbEpk898yJAhtIIuXLhQI+K1t7fHc+fO0QSsSLgUKQHLX6eEyxZUz9LSEk+fPs0o4H79+qlDRxqDr68vo3DZrPFsAuZiVWYSsJmZGZ44cYKXJ0BVbNCgAa1+qAUt8vy8W6/q4erq+lE+865du9IKSgUq0wQbNmwoI2CuwqUoL2AuwqVIEjC1gGHjxo1qkJDmIB+AwdnZWaFwKZIEzMcd5OvrK7OPmhIuk2Cqmh06dKDVj5eXFzHt9OnT3WhCqwq0bt06Tz5z0jrdw4cPa0y8ICfgpUuX4pgxY3jdTwmYj3ApSguYGiqJRCLMzs5Wh4Y0Bumhs6OjI168eJHXV09awMr4calACLVr19aocAHIQQVJ0wZDQ0MUi8VKH/rHC507d34pXwDS+P7333/XqHgByn9UXrx4gXFxcUrdP2fOHHz16pVS8actLS3xwYMHFUeckkYnXyKoDSkTJ05Uqt5MTU3x5s2bePDgQWLANkUMCAjAnJwcHDBggEb7Xnh4OK1uqEPHpeng4PAJlIBSaq9RowZtc+q7d+/gw4cPMtecnZ2VebzKIBAIICIiApKTk8Hf35+XLxag3K3h4+MDmzdvhrFjx/LO/5tvvoEbN27A77//DgDVf/sfV1DbBLdt2waRkZG8/eeDBg2C27dvAwBA27Zted1rZmYG48ePh8WLF0NUVJRG3UhOTk4y/3/37h3k5ubS0llbWxPXTlQJQkJC9gDhl+b+/fu0XxpNLAYH+P85LjVUpobQXA97lh8qx8bG4tKlSznnP27cOExJSan4cggEAvznn3/U8eHTOKSHzoMGDeLkfqMYGhqKGzduRKFQWDGEZor7JE/5OS41hNbEQWMA9J12f/zxBzHd5xNI1INx48aJSYU4dOgQrSGbNm2qkYpbuHAhbY7bqFEjPHv2rEIBM81xuQpYXrgAgB4eHurSjlZAel17eHg4Ll26VKGApYVLXeMqYCbjVM+ePXHgwIEa6YNnz56VqZNdu3YR0w0cOPAKTWRVhSlTpvQjFYIUBZHpSMiqJlODKRKwIuOUIgGThAsAOGvWLHXpRiuwbt06mfdXJGCScCkqErAiqzLbVsWq5IsXL2TqhGk74MiRIzfLa6zKIBaLrUmNMHnyZFojamqhBhuZBMzVqswkYCbhAgDeuXNHXbrRCmRlZdEWbERERBDrjU24FC0sLPDYsWM0AZuZmeHx48c1alVmKq/8lk8mb8f48eMny2usSmFra0tbqEFagEA6z1UbKC9gvu4geQGzCbdhw4bq0oxWgRRxRF7AXIRLUV7A2ipcAMD27dvT6oO0gAUAcOrUqV40gVUl2rZtmy1fCBcXF1qBb9y4ofGKZCIl4Pbt2/P248JnAS9ZsoRVuACAU6ZMUYdWtA5r164l1gclYD7CpUgJuFu3bhr347Jx1KhRtPogTdWsrKzUc06RNAIDA6/IF0QgEGBeXp5MgYuKilBPT0/jlclEHx8fzMvLw86dOyt1/759+/Du3busHfDSpUvq0otWISMjg7HtU1JSFNYbE+3t7TE7Oxujo6M13n+YKG//efnyJTGdh4cH3XfEEUqv6rCxsbkhfw0R4e7duzLXTExMoFGjRspmU6VwdnaGOXPmwODBg2H27Nm8/cCRkZGQn58PmzZtgkWLFhHT2Nvb89ry9iXBzs4OOnfuTLs+fPhwQERYs2YNY70xwczMDDZv3gzh4eHQp08f8Pb2VlVxVYo2bdrI/P+PP/4gpnNwcHimjvLIgMninJycTPsFHjlypMZ/CeXp7OwsM1Tm6kaiGBkZKTNUpobQ8ukiIiLU85nTUqxZs0amPoYPH44bN26s+CJHR0dz9p9TVuXevXsjwP8Podu2bavx/iRNkUiE79+/l6mH7777jph2xIgRW0j6qlKIxWIDUqC3sLAwWgNq6rRAJlLCbdSokcx1rgKWFy5FkoBJvm9lkZ2djU+ePKlyvnz5UmVlfvnyZUU99e/fH1NSUmhDaS4CpoxTlHApaqOAW7VqRasHJpdpbGzsQLLCqhgtWrQokC9My5YtaQW/ffu2xiuUIpNwKVICdnBw4CVcirGxsRUbrs3NzfHjx48qEwKXwOWqoKWlpUrL3alTJwQADAwMZJwDR0dHE0cuAMzCpahtAo6JiaHVAemDYGpqKhGLxQagCfTv359mtNLT08PCwkKZgpeWlmrMUS5NRcKlyCTgyMhI3LRpk0IjS+vWrREAcPDgwSoTQHZ2tloNf6ocMaxYsQIBQOEKK5KAFQmXooWFBR49elQrBLx//36Z968KY1WlMW7cuJmkQskvC0NEje3woI6H5CpcivICpoTLR0C7du1SmQBIx2kysXfv3njw4EE0NjZGgUCAP/zwA8bGxnK6lxLY8OHDVVb2169fc643aQFzFS5F6gvcvHlzjfQ1gPLQN/n5+TLvv23bNmLazx4bzWDSpEn2pEYhHfdJ/fqqmyKRCCMiIngJlyIl4KlTp/IWrpGRkUrD3fD98aMEvH79es7ClaaVlZVKh85dunThnHd0dDSuXLmSl3ApWlpaamwtMwB5A/6IESOIaSMjI6eBJtG8efNC+UJ1796d9gLp6ekaq9Bp06ZhfHy8UveKxWJ8/fo11q1bl9d9/v7+Kuv4RUVFnE6BkKZAIMDDhw9jWlqa0sHXVDl0XrVqFed8TU1NMT09Hfft26dUuZXxHauKixYtknlviURCtJ983oBvA5rEgAEDrskXzMDAgDbvlUgknN0wqqa+vj6mpKQwHmzMRGqo3KRJEzxz5gyjEYvE9evXq6zjHzhwgFe5pYfK0kNorvdTPxQjR45U2TtwHTqbmppWfHEnTZrEaMRiop2dncaECwCYnp4u8953794lpvPw8HgHmkZkZORsIBTuxIkTtAYcO3asxipVKBTi5s2bOQtYfo6ryAotTT09PczMzFRZx6eOGrW1teUlXOpanz59OAvY0dER58yZgwDlQ+fi4mKVvYeiVWzSwqWu8RFwq1atkBTZVF0khYJauXIlMW1QUNA50DSSkpLqkg6Zio+Pp73IiRMnNFax8FlUXATMZJziKuDOnTurrMOXlpZWiHbmzJmsc0dKuKSdXH369MEDBw6wCtjR0RHPnDkjE2fp2LFjKnsXpo4MDMKlyEXAXl5e6O/vr9H+Rdr2GRAQQEw7adKkUaAN8Pb2fgNyhSOFvSwuLkYLCwuNC3jLli2MAlZkVW7UqBHjEJqaay1dulRlHf7SpUsVz7e0tMQzZ84QBcwmXIpsAqaEKx8sbsyYMSp7l9evXxPno8bGxnjw4EFW4xSbgL28vHDnzp0a7VcCgYB2nGdeXh4xioetrW2Z2gLOKUJoaOhOILyQ/PgfEXHo0KEarWSQEnBISAgv4VIkCdjQ0LDCzSLfiJVBYmKiTN5WVlZ48uRJmfCoXIRLkTSEZhIulHc0LC0tVdn7tG/fnrdwKU6aNAkXL15ME+6xY8c0FuqGIsnKvGPHDmLaXr16PQRtQXx8fDuSA17e8oaIePDgQY2LFwgCjoyMxM2bN3N2B0kL2MjIqEJkTk5OKuvoiIju7u60vKUFLBAIcPny5byCHkgL2MHBgVG4FFNTU1X2PklJSQhQ7krjI1yK0gKmhKsNC4B++ukn2rv+73//I6aNiIhYCNqEJk2aFIFcITt27Eh7oZKSEoVnCamLlIBTUlJ4CZdio0aN8Ny5c3j06NGKeSIpVq+yePToEWPelIB//vlnpaKV9OnTB1NTU/HcuXMK4ypHRUWp7J1+/fVXBAB0cHDA1NRU3n5c+Czgbdu2aY1wLS0tad6VgoIConvP0tJSMmHCBAtQAVQ27m7evDlttcjly5fh6dOnMtf09fUhODhYVdlWCmVlZXDt2jXo0qULnDp1injGMBuePXsGJSUlYG9vDxkZGQBQflqeqvDrr78y/i0vLw8ePXoEHh4ecPHiRd7P/uuvv8DW1hZKS0vh33/Zgxfu3buXd90wgaqf4uJiqFWrVkWIVz64dOkSeHp6wqNHjyAvL08l5aoMQkNDwdTUVObagQMH4P3797S0HTt2TF+1ahX5qECeUJl4HR0dk+WvISL8/PPPtLTDhw9XVbaVQmRkJLRt2xYaN24Mvr6+EBISwvleIyMj2LlzJyxcuBCGDBlScVh2aWmpysp36NAh4nWBQADLli2DJ0+eQNu2bWHOnDm89gw7ODjA9u3bISgoCBYvXgy7du1iPY41MzMTLl++zLv8JODng7ALCwshKCgItm3bxuuMXi8vL5g9eza0a9cOnjx5ovFDygUCATGm9759+4jpXV1daTrRCjRq1OgDyA0T3NzcaIG4EJnPbFEXXVxcZIbKTEYsEo2MjPDXX3/Fnj17VlyjFge0bdtWJcPLN2/eEIfxpDkuyYjFRGqOKx3yh4sbaeLEiSp5L/ljQN3c3PD06dOc/OekOa6mDVWklXSZmZlEf7OTk1OJ1liZ5REcHHwQCC/4+++/015wy5YtGq10APoyOi4CJglX/u+qWNNM2ojAZpziImCScCkqEnCdOnWwrKys0u9FCszARcDaZJyS5rlz52jvyLQ3OTAwkP/8Rl1ITExsQlqwMWLECNoLlpSUaGy5JBvZBKxIuBTXrVtX6U4u71LhYlVmEzCbcCkqEvDRo0cr9U75+floZmZGfDabgNu0aaOVwu3UqRPtHSUSCbq6uhLTx8XF+YE2o0OHDlkgV2gTExN8+/Yt7UVnzJih8QYgkVqJJS1grsIFKHcXyW8L44P9+/fzFi5FkoAdHBzw9OnTnKJjkgRMWdLbtm1bqa+vosPe3Nzc8NSpUzIC1lbhAgDu3buX9o7nz58npv3sjdFuRERELAJC4Uk+38zMTI3PWZgoLWA+wqU4cuRI4lxfEV6+fIn29vYIUO74V8aPKy1gPsKlKC3gkJAQmYURpHbkgsuXL9OCsJMoLWBtFq6rqyvxh4xpEdLo0aN/Am3HhAkTLD7HopUpfIMGDYgrdbRhxRUT9fT0cPv27Xjjxg1ewqUYHx/PS8DPnz/HFi1aVNwfHR2NN27cR3hFhQAAHCxJREFUUMqPa2VlhRcuXMAbN24odcymn58f/vnnn7h9+3YZw5lQKCQuSGDD7du3sWbNmpzzdnNzw99//x3PnDmjlcIFAFyyZAntPXNycogfIxsbmzJV+XaloXLL16pVq/K7dOlyU/7648ePYc+ePbT0kyZNUnURVAZ9fX0wNTWF3NxcsLW15X3/woULwdfXFx49esSaDhFh165d4OXlVeH3FAgE4OzsDBkZGUrlbWJiAogIJSUlYG1tzft+a2tryM3NBQsLCzAw+P8wSxKJBMaMGQMRERHE4yqlUVJSAqtWrYJ27dpBTk4O57wtLS3hw4cPIBQKaf5TbYC5uTmMHj2adn3t2rXw8SP9tM7u3btfVJVvt8oxdepUT5LhqnHjxsShBtMyMk1SeqhMGbGoQ7K5sF69ehXDRD09PRwwYABu27YNHzx4gNnZ2fj69Wu8desWrly5UuZrC1A+x122bFnFFzcuLg4XLlzIOW/poTIfNxLFkJAQ3LJlC+rp6aGfnx+jEcvS0hLHjRuHhw4dwqdPn+Lbt2/xxYsXeOXKFZw9e7ZSBknpoTJpDqwNJAWYy8/PR2tra2I/SkxMdIfqhK5duz4DwosfPHiQ9uJ//fWXRqMfyNPIyAh/+eUXmaEyHwF7e3vjyZMnlTqbWF64FOPi4nDBggUK7yfNcfkIWFq41DU2AXPhoEGDOPnPSXNcNzc3nD9/PufzfauaQqEQHz9+TOvDTGGefH192Ydd2ojJkyd/DYSXady4MX769In28qGhoRpvGIru7u7Yo0cP2nUuAqaEW6NGDdrfDAwMWH+kmIRLUZGA2YxTXARMEi7FyghYT08Pt23bxrqPms04pS3CBQDs27cvre9++PABa9euTSx3bGysZpeAKYtWrVrlAaECUlJSaBXw6NEjJA21tY1sAmYTLkV3d3c8cOAArbEp4SpypzAJmItVmRIw6ZxbNuFSpAQsb5QxNjbGX3/9lfEUPKremASszVZleZLcQ0yRPT09Pd9CdcXnrU+0l6pTpw4WFRXRKoHp/FJtI0nAXIRL0d3dHc+cOVPhEuIqXIryAra3t+fsDiIJmItwKfr5+eH+/fsrBGxsbIwHDhzAPn36cKo3eQFXJ+HWqVMHS0pKZPrsp0+fGOt94sSJUVBdIRaLhU2bNqVtFQQoD+kijxcvXig9r1I3pQXs7e2Np06d4iRcipSAHRwceAmXIiVgPsKlKC1gPsKlSH2Ba9SowVm4FPX19XHPnj04bNiwaiVcALKPe/Xq1cS0nTp1eg3VHVFRUdOA8HKmpqb46tUrWmXMnj1b443ElXp6enjkyBG8e/cuL+FSdHd3x+fPnyv9zrNnz8Znz54p5ce1srLCu3fv4tGjR5U6iSEwMBAzMjKwf//+vO/V19fHkydP4u3bt6uNcG1sbGjH1+bn5xP3pguFQrXMdat8h8OaNWvmeXl50RyCRUVFMHPmTFr6uLg4cHevHpb1Nm3agKGhIdy9exf69OnD+/6IiAjYtWsXdOzYEezt7Xnda29vDx07doRdu3bBqFH8Y5n17dsX0tLSwMDAgHYcpSIYGxvDyJEjYd26dTB69GgwMjLidX/Lli1BIpHA06dPoV+/frzu1RTEYjFYWMius1i+fDm8efOGlrZ79+7/LFmy5Dd1la1KMXbs2DFA+DUTiUR479492tf3woULWmVhJFF6jksNob/++mvO90sPld3d3fH06dMVc2BFlB8qx8fH4/z58znnHRISglu3bkU9PT2sUaMGoxGLRGqO6+fnhwDlW+Kk58CK2KZNGzx+/DhaWVlVDKG5uJE0yebNm9NWB+bk5KCVlRUtrUgkwqioqB7wJaF9+/avgVAxXbt2JS4h5BsgXZ2sV68ebY7LR8CkOS41Bya5HKTJNMeNj49HsVisMG9p4VLXuApYXrgUAwMD8eDBgwoF7OnpWSFc6pqhoSHRLadNPHz4MK1/Mh0h06tXr3vwpSE2NnYg08J00lrZzMxM4ooVbSFpszUXAbMZp6gvMJOAFRmn4uPjcebMmYx5k4RLUZGAmYRLceDAgawC9vT0xGPHjhG/VtrMPn360Prm3bt3iW5Nc3NzydSpU1vDlwjS0SgA5ae7vXjxglZJ8lEXqgPZBMzFqswkYEq4ig5LYxJwSEgIbtu2jdU4xSRgRcKlyCTg6ipcKysrWr8sKytjjAIzfPhwesynLwVisdjG0dHxEzA0vDwkEgn269dP443IlyQB83EHyQuYq3ApyguYi3Ap1qhRA0+dOlUhYK7ClW5HaQGThsrVhZs3b6b1yXXr1hHTtmjRokAsFovgS0Z4ePhSYKisX375hVZZ2dnZvE/o0wZKC1gZPy4l4JYtW/ISLkVKwHyES5EScJcuXXgJlyIl4Pbt21db4ZJC+L569Yro2tLT08OYmBju0QurMQStW7fOAUKF1atXjxj/6fz585w2cmsb9fT08N69e7h9+3al7u/cuTO+ffsWO3bsqNT9O3fuxLS0NKWWndauXRvfvHmDkydPVirvuLg4zMrKqtSpfW5ubjhhwgRcu3YtJicn47Rp0/Crr75Syi/Nh/b29piRkUHrh0zn7H711Vd34b+CKVOm9Gc6zS0iIoJWaYjVa/EGxfnz52NCQgJu2bKFd9ABe3t7PHXqFPbp04fViMXEb775Bvfs2YOJiYm8ww0ZGRnh/v37cfDgwbzcSBSpofLXX39NDDiniI0bN8YTJ04wBjL4+++/sW/fvlXSZiKRiBhUjgoWL8+aNWuWTZ8+3RX+SwgODj4CDBW4Z88eWuVVx/kvNe+jhtBcBVy7dm2ZobIiK7Q8KeFSX9z4+HjOAqaESw2V+fqBPT09eZ8HLM2QkBD8+PEjUbTy/aEqYqCtWLGCltfLly/RxsaGmD48PHwZ/NcgFotFXl5eb4FQIWZmZnj//n1aJb569UrrNmZzpVAoxI0bNyoUcO3atfHUqVO0OS5XAcsLlyIXARsZGRHnuPJGLCZ6eHhgbGys0nuzg4ODeQe4GzdunMraKDAwkPa1Ly0tZTxXuHfv3unwX8XUqVM9a9SoQYt3BVD+C076Bb5x4waampqqXXyBgYG4aNEipTbYS7N///44YMAAXsKlqEjATMKlyCZgSrhMZ9wqErCjo2Ol5rfOzs60iJvFxcW4c+dOTExMxDVr1uCbN29o/aGwsBDr1atX6fZt0aIFMeIn08q1+vXrF4vFYv7xib4khIaGJgNDhSYkJBB/bX/77Te1Rt6wsLDA169fI2L5srjo6OhK5S8SidDZ2ZmXcCm6u7vj8ePHacM4RcKlSBKwIuFSpATctm1bmeuqaIvk5GSZNs7IyMDWrVvLpDExMcHffvuN1h+4RBhhY+3atfHZs2fED4WhoSGx/SIiIkaCDgBdu3Z9AYRKFQqFePz4caKAf/rpJ7WJd+7cubT8+axjVkRjY2OcM2cO591B8gLmKlyKCQkJFQLmKlyKFhYWGBwcTOzUytLAwIC2v5upPHp6enj37l2ZtGynKSqimZkZ3rx5k9a+b968YfyiBwUFnQMdypGUlOTo6upaDISKsrW1xadPnxIFHBkZWeXCbdy4MW0D9q1bt6rcXaGIlIDHjh3LS7gUExIS8LvvvsP9+/dzFm5VUf58p9evX7NuTBk8eLBM+rKyMqXD8/z666+0flVaWsoYFeTzYgx+26i+dCQkJHSytrYmzn/r16+PmZmZtEr+9OmTUue78uHJkydl8pRIJNipUyeNdnaKcXFxmJGRwXk3kjSNjIwwLS0Nt27dqvH3kI8L9eTJE9b0DRs2pPUFCwsLXnkKBAJcv3498aPA5NuuXbt26bRp01qADnSEhoZ+x/SL2717dywuLqZVdGFhIXbp0qVKOhVplc3evXuJaU1NTXHDhg0KD6pWFT08PDApKQnd3Nxw3759jK4MEo2MjDA5ORk7deqEfn5+2KFDB42Kt3fv3jJ1XFJSwmoYlBdvUVER75HQggULiMLdvXs38asvEokwPDy8+oa1UQeCgoLOAEOFBwUFEV0JBQUFKv8ampmZ0Ral5+XlMVp6Fy9ejIjlo4Hk5GReYlIF9fX1ORmOtHGfdL169WhtOmvWLMb006dPl0l79uxZXvkxGUKPHz/OOP34ojcdqApisdjgc/wfYiWKxWJixRcVFaGPj4/KOtR3331Hy+P27dvo5ORES+vu7k4bFRw+fFjjotAkGzZsiPPnz8dnz55x+irKG40+fvxIXI7YrVs3mnGLzyquGTNmEPvPnTt3GEMZ+fj4/A06cMOIESOsmjdv/g4IFSkQCHDLli2MAu7evXulO16jRo2IQ3TE8iHdihUrKo6qFAgEeOHCBZk0paWl6OnpqXEBaYLU6jBqhPT+/XtO9w0fPpxY3+fOncMJEyZgeHg4/vzzz7SRV3p6OmfLN5Nwnz9/jnXq1CHe4+7uXjR58mT+58X8lzF9+nS3+vXrEy3QIpGIuIQSEfHdu3c0PyRfnjhxgvhsafz77784fPhw/Oabb2h/S05O1riINEVPT0+ZusjKyuJ0n0AgwNOnTyusd2kUFBTQfMFMnDJlCvEZWVlZ2KRJE+I9tra2ZQkJCZ0YO6kOzJg6dWprBwcH4v5far0wCXl5efjVV18p1fn8/f1pz7t9+zZ++PCBmJf89dzcXLS1tdW4iDTFDh06yNTH06dPOd9rbW2Nf/zxBxfdct5tJRAIiH56xPJQw0y+dSsrK8mUKVP6s3RPHRQhJiZmiKWlJdGFpK+vj/v27SM2TGlpKSYkJPDqeKamprSVNu/evUM7Ozt0cHBg/LGQxtixY5Xq9BYWFrhixQr08/PTyPJPLjQ3N8eRI0cyxmmuUaMGzpkzR6Y+7ty5wysPY2NjXLJkCePmBIlEgnv27OG0xl1fXx+3bdtGfM7Lly/R3d2d8b6oqKhEDt1TB0WIiYkJsra2LgNCRevp6eHWrVsZxbRw4ULOy/dmzZpFu1/+3CAfHx9MT08n5nXt2jWllwoGBARUPOfjx4946tQpNDEx0bhghUIh+vr64tatWysMRe/fv69YrC8SidDf3x93795NHJ1cuXJFqXxtbW1x/PjxuGnTJty9ezdu374dZ8yYwTkogbGxMXEpJSLi48ePiYZH6n3/kzuFqhKKBMz0C4tY7gLg4sSXD3vy8OFDojHEzMyM9oWu7OKNNWvWyDwvPT1d48IFKB92btiwgVanubm5uGrVqoo130w4efKk2stcr149/Ouvv4jlefToEWtkFp1LqIoQExMTYmVlxTiEJh1gRuHatWucViJ17ty5ouGZQpIGBwfTnr9hw4ZKdbiHDx/KPG/lypWV7sTu7u4oFovx2LFjeOvWLXzy5AneuHEDk5OTsVevXpx9vkwC5gKmTexVxZYtW+Lz58+JZXny5AnrDqSAgIA/OXZFHZRBTExMCNM2QgDApKQkxggMr1+/5hQj2MDAAAcPHkz8G2lezLZ4gwtdXFxoZa1M4AFjY2P86aefFO6NPX78ONasWZPTMw0MDPDo0aOMz3r48CF+++23OHbsWJnr27ZtU5twAwICiGGUEBEvX77MumUxICDgT7FYXOUniPznERcX19XFxaUEGBpiwIABxFMIEcuHt8uXLyfGXeZC0mFT48ePr1SnCwsLk3meouWBbNTT02PciUXClStXOMcHMzExwfPnz9OecePGjYpnyC8pVYfbzNDQkLalUBqrVq1ifcegoKCz3HufDpXGtGnTmjVp0oR4AiFA+fCJFAuawvXr19HFxYVXJ2nevDntYPCbN29WeofRrl27ZJ554cIFlf0QcEFYWBjn55uYmNAWpSAi7tixAwUCAQYFBclcX7x4cZUK19XVldG9VFJSguHh4az3fw7HpIO6ERYWVqdly5bEUDrwuWFJ4XQoZGRksB4ILc+BAwfSoi1069atUp1PKBRidna2zDMrE5vp2rVrMs8qKirCiIgItLS0RGtra5wwYQJti+OlS5d45WFpaYl//vknrT7nzp1LW7Ty3XffVZlw+/XrR4yugVi+gCMgIEDR/ad5dTgdVAuxWGzVtWvXf4Glo5FOM6dQVlaGS5Ys4exbtba2xhUrVmBZWRlu2bKl0h2wTZs2tDK1b99eqWcZGxvThDl37lxauqVLl9LqgO/mekdHR9o+a4lEQttCGR8fr3LRWlhYsBrQ/v77b2zRogXj/QKBAIcMGZLKq6PpUHUIDQ3dyrYhfdiwYYzzYMRyYxafw818fHzQ0dGx0h0xMTFRphzv3r1TOkZ1nTp1aO9FMtD5+vrS0rm6uiqV3z///MNYp4iqDRAHUD6nJsVUprBt2zZWe4GJiQlGRETM59O3dFADhg8fPtXCwoLREu3l5YUPHjxg7Ww7d+6sVCA1Ng4aNAiHDx8uszooNTVVJv/9+/cr/XyS1Zq0QYKUTtn14G5ubqy+3uHDh6uk7qysrHDdunWMnoSPHz/ihAkTWN1fVlZWZREREWN5disd1IUxY8Z0rV+//ntgaEBTU1NWyyRi+TwxISFB5aFupOejd+/exeXLl9NWJVXGcm1vb097F1LY0lq1atHSVWY7ZYsWLTAnJ4dYl//73/8qVWdCoRAnTpyIb9++ZWyvW7dusQ6TAQCdnJyKv7hzc79ETJs2zd7Hx+cZsDRmv379GJ35FC5dulTpHUoUra2taQc0M3V2ZZdYWlhY0J5HChNkbGxMS1fZgPbdunXD9+/fyzyzuLi4UqvOGjZsSJtDS+PTp084e/ZshW6/Fi1a5CcmJror1Zl00AyCg4NXm5iYMA6jTU1Ncf78+TQjjzQkEglu3bqVcb8nVw4aNEihcClkZWXhzp07cdSoUbziU+np6dGeRfryCQQC2gKO4ODgSv9A9ejRAz98+ICpqak4fPhw3nGlKNrZ2eGaNWtY2+XmzZvYpk0bhc/y9fW9PmzYMFPle5EOGkNCQkKHVq1a5QFLA9evX594+rk0SkpKcPPmzVi/fn2lO3f9+vUxPDwcDx48yLjFUB6jR4/mlYf8c5nmnPIuL775MLEyK82srKxw+fLljIEQEMtXs4WHhytc2lmjRg1JZGTkbCW7jQ7aArFYbBEQEPCHogb/+uuv8dWrV6xiKioqwsWLFysVsVGaxsbG6Ovri/Pnz8cbN24wGmKYdr8wUd5nzGTtlTcyRUdHq0S8ytDCwgLj4+NZrciI5cY8Lj+ejRo1eh8XF9dLud6ig1YiPDw8sl69esToHCDVkRYvXqzwsKuysjI8cOCA0j5ZedaqVQuDgoIwOTm5Yi7+4MED3s+Rd90wnQv86NEjmXTTpk1Tu2gdHR1x+fLljOuRKaSlpWHPnj05PbN79+5/BQQEmCvTP3TQcojFYpv+/fvfUGRJdnZ2xu3btzN+ESlIJBI8fvw4BgQEqMw6LRQKsU2bNkpFA0lLS5Mpn1gsJqa7deuWTDrSYo6qYpMmTXD16tVYWFjIWrf//PMPhoeHc/J729raloWHhy9WslvoUJ0QFhYWUr9+/Q+goFN4enri/v37FYqY6myJiYlV5ifmwuvXr8uUadGiRcR0c+fOxe3bt2NycjIuXLiwyk9N0NfXx8H/1979xTR1RgEAP2tLHKW39o/9R3pBKowgrV1mWLWSqJjIHtBAQvog2YxkahPAsDRxEcz2ZWoWQpb4MPfAw4wP/mG+zUR50sxUtDQTCIQAFcE7RTtCm7YXqKXw7WEJcWFCLy1F6/klJ+Gh97uH+91Tei+333E46L1791Y9js+ePaNOpzPhL4/s27dvqKGhIX+NpwJ6HxFC5A6H424irTKsVivt7OxMqAXl/Pw8vXXrFnU4HEs9etMVubm5lGVZqlQq09qMbaXj1t7evuq9BEopHRoaoseOHUu4aI1G43xDQ0PL2mYfZYTTp0/vt9vtf0MCJ0xRURG9ePEiDYVCq56MlP674sTly5fpoUOH0l7IGxV5eXn0zJkzyxqCvY3b7aY1NTUJv9mIRCJaVVXV19LSolvjlKNMU1dX951er3/r94TfDIZhaGNj47JrzJWEw2F648YNWldXRzUazYYXWapCLBZTu91OL1y4QB8/fpzQJQbP87Sjo4NarVZB+7LZbJNOp7MqmXlGGYoQoq2trb0vZAXH8vJyeuXKlWVPGK1kYWGBer1eeu7cOVpRUbG0iPv7ECKRiJrNZnrixAl67dq1Zf+eWkl3dzd1Op1UoVAI2qfFYok0NTWdSsEUo0x3/Pjx0v379/cLuYOsUChofX09vXPnzopPB/2feDxOe3t76aVLl+jRo0ep1WoV3LZzvYJlWVpZWUnPnj1Lb9++TYPBoKDf7cmTJ4L6D78ZW7dujR45cuQHAPgodbObGfCArMLlclV3d3d3PHz4UCNkO5VKBdXV1VBTUwMHDhyA7OxswfuOxWIwODgIAwMD4PP5YGxsbCkCgYDg8d5GLBaDTqcDlmUhNzcXWJaF7du3g9lshtLSUlAoFILH5DgObt68CZ2dneD1egVvbzKZYnv37r2an5/fSAiZFTzABwCLN0FNTU2nPB7P9z09PYL71uTk5MDBgwfh8OHDUFlZCQaDIel8YrEYTE9P/yei0SjwPA/hcBgWFhaWbSOTyUAul4NcLofNmzeDXC4HnU4Her0exGJxUvm8fv0a3G43dHV1QVdXFwwODq5pHIvFMmO3238xGAwthJB4Ukkh9Kbm5ub68vLyl8m0yiwuLqYnT56k169fpy9evBD0EfRdMTMzQ+/evUsJIbSioiKpBeJFIhHdtWvXVGNj4zfpmcXMgH9518jlctX29/e3PXjwwDQ3N5fUWEajEWw2G9hsNigrKwOLxQJqtTpFmabG2NgYPHr0CDweD3g8Hujt7YX5+fmkxjQYDAt79uzp3rZtW0tbW5s7Ral+MLB4k9Ta2prPcVy71+utGh4eFn5h+xZarRZKS0uhpKQEiouLwWQyQUFBAZhMpjVdP68mHo/Dy5cvgeM44DgORkZGYGRkBEZHR8Hn80EkEknJfrKysmD37t2TZrO5Q6vV/kgIiaVk4A8QFm8KNTc3f+nz+b7t6ekpmZqaWrcFvA0GA6jValCpVKBSqZZ+3rRpE8hkMmAYBiQSybLtotEoBAIBCAaDEAgEloLjOHj16hUsLi6uS74Mw9CdO3c+Lyws/F2v1/90/vz58XXZ0QcGi3cdEEIkgUDga47j6vv6+j6dmJjI2uic0s1oNMZ37NgxWlBQcFWj0fxMCAlvdE6ZBos3DVwuV/Xk5OTx8fHxzwcGBrbMzMxsdEopp9PpFs1mM5eXl/eHVqv9ta2t7f5G55TpsHjTjBAiDQQCX/n9/tqnT59+Njw8rIhEIu/VPEilUigsLORZlp3QarV/btmy5bf29nbsPJBm79VJk4kIIaK5ubnycDj8xfT0tM3v93/CcZzu+fPnWcnezU2WRCIBlmXnDQZDQKPR/KVWqx8rlco7DMN0EUKiG5ocwuJ9VxFCJNFo1Do7O1s2Oztr5nm+KBQKGXmeV/I8L41EIh+HQqGsYDAoiseFP8sglUpBqVTGVSpVlGGYGZlMFpbJZNPZ2dl+hmEGGYZx5+Tk3Menm95dWLwZoLW1NT8Wi21d6TUSicQvFotfSySSKUIIn6bUEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCGW4fwAomeYY2rBtxwAAAABJRU5ErkJggg==') no-repeat 0px 0px;
+      }
+    </style>
+    <style id="gerrit_sitecss" type="text/css"></style>
+  </head>
+  <body>
+    <div id="gerrit_topmenu" style="height:45px;" class="gerritTopMenu"></div>
+    <div id="gerrit_header"></div>
+    <div id="gerrit_body" class="gerritBody">
+      <h1>Sign In to Gerrit Code Review at <span id="hostName">example.com</span></h1>
+      <form method="POST" action="#" id="login_form">
+        <input type="hidden" name="link" id="f_link" value="1" />
+        <div id="logo_box"><div id="logo_img"></div></div>
+        <div id="error_message">Invalid OAuth provider.</div>
+
+        <div>Available OAuth providers:</div>
+
+        <div id="providers">
+        </div>
+
+        <div>
+          <a href="../" id="cancel_link">Cancel</a>
+        </div>
+
+        <div style="margin-top: 25px;">
+          <h2>What is OAuth protocol?</h2>
+          <p>OAuth is an open standard for authorization. OAuth provides client applications a 'secure delegated access'</p>
+          <p>to server resources on behalf of a resource owner. It specifies a process for resource owners to authorize</p>
+          <p>third-party access to their server resources without sharing their credentials.</p>
+        </div>
+      </form>
+    </div>
+    <div style="clear: both; margin-top: 15px; padding-top: 2px; margin-bottom: 15px;">
+      <div id="gerrit_footer"></div>
+    </div>
+  </body>
+</html>
diff --git a/gerrit-openid/BUCK b/gerrit-openid/BUCK
index 90d0d48..78abce8 100644
--- a/gerrit-openid/BUCK
+++ b/gerrit-openid/BUCK
@@ -1,4 +1,4 @@
-java_library2(
+java_library(
   name = 'openid',
   srcs = glob(['src/main/java/**/*.java']),
   resources = glob(['src/main/resources/**/*']),
@@ -12,12 +12,13 @@
     '//gerrit-server:server',
     '//lib:guava',
     '//lib:gwtorm',
+    '//lib/commons:codec',
     '//lib/guice:guice',
     '//lib/guice:guice-servlet',
     '//lib/jgit:jgit',
     '//lib/log:api',
     '//lib/openid:consumer',
   ],
-  compile_deps = ['//lib:servlet-api-3_1'],
+  provided_deps = ['//lib:servlet-api-3_1'],
   visibility = ['PUBLIC'],
 )
diff --git a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
index c19d74b..aea816e 100644
--- a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
+++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
@@ -22,10 +22,14 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.auth.openid.OpenIdUrls;
+import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
+import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.httpd.HtmlDomUtil;
+import com.google.gerrit.httpd.LoginUrlToken;
 import com.google.gerrit.httpd.template.SiteHeaderFooter;
 import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -55,15 +59,18 @@
 class LoginForm extends HttpServlet {
   private static final Logger log = LoggerFactory.getLogger(LoginForm.class);
   private static final ImmutableMap<String, String> ALL_PROVIDERS = ImmutableMap.of(
-      "google", OpenIdUrls.URL_GOOGLE,
+      "launchpad", OpenIdUrls.URL_LAUNCHPAD,
       "yahoo", OpenIdUrls.URL_YAHOO);
 
   private final ImmutableSet<String> suggestProviders;
   private final Provider<String> urlProvider;
+  private final Provider<OAuthSessionOverOpenID> oauthSessionProvider;
   private final OpenIdServiceImpl impl;
   private final int maxRedirectUrlLength;
   private final String ssoUrl;
   private final SiteHeaderFooter header;
+  private final Provider<CurrentUser> currentUserProvider;
+  private final DynamicMap<OAuthServiceProvider> oauthServiceProviders;
 
   @Inject
   LoginForm(
@@ -71,13 +78,19 @@
       @GerritServerConfig Config config,
       AuthConfig authConfig,
       OpenIdServiceImpl impl,
-      SiteHeaderFooter header) {
+      SiteHeaderFooter header,
+      Provider<OAuthSessionOverOpenID> oauthSessionProvider,
+      Provider<CurrentUser> currentUserProvider,
+      DynamicMap<OAuthServiceProvider> oauthServiceProviders) {
     this.urlProvider = urlProvider;
     this.impl = impl;
     this.header = header;
     this.maxRedirectUrlLength = config.getInt(
         "openid", "maxRedirectUrlLength",
         10);
+    this.oauthSessionProvider = oauthSessionProvider;
+    this.currentUserProvider = currentUserProvider;
+    this.oauthServiceProviders = oauthServiceProviders;
 
     if (urlProvider == null || Strings.isNullOrEmpty(urlProvider.get())) {
       log.error("gerrit.canonicalWebUrl must be set in gerrit.config");
@@ -102,7 +115,7 @@
   protected void doGet(HttpServletRequest req, HttpServletResponse res)
       throws IOException {
     if (ssoUrl != null) {
-      String token = getToken(req);
+      String token = LoginUrlToken.getToken(req);
       SignInMode mode;
       if (PageLinks.REGISTER.equals(token)) {
         mode = SignInMode.REGISTER;
@@ -140,7 +153,7 @@
     }
 
     boolean remember = "1".equals(req.getParameter("rememberme"));
-    String token = getToken(req);
+    String token = LoginUrlToken.getToken(req);
     SignInMode mode;
     if (link) {
       mode = SignInMode.LINK_IDENTIY;
@@ -151,7 +164,23 @@
       mode = SignInMode.SIGN_IN;
     }
 
-    discover(req, res, link, id, remember, token, mode);
+    OAuthServiceProvider oauthProvider = lookupOAuthServiceProvider(id);
+
+    if (oauthProvider == null) {
+      discover(req, res, link, id, remember, token, mode);
+    } else {
+      OAuthSessionOverOpenID oauthSession = oauthSessionProvider.get();
+      if (!currentUserProvider.get().isIdentifiedUser()
+          && oauthSession.isLoggedIn()) {
+        oauthSession.logout();
+      }
+      if ((isGerritLogin(req)
+          || oauthSession.isOAuthFinal(req))) {
+        oauthSession.setServiceProvider(oauthProvider);
+        oauthSession.setLinkMode(link);
+        oauthSession.login(req, res, oauthProvider);
+      }
+    }
   }
 
   private void discover(HttpServletRequest req, HttpServletResponse res,
@@ -216,24 +245,11 @@
     sendHtml(res, doc);
   }
 
-  private static String getToken(HttpServletRequest req) {
-    String token = req.getPathInfo();
-    if (token == null || token.isEmpty()) {
-      token = PageLinks.MINE;
-    } else if (!token.startsWith("/")) {
-      token = "/" + token;
-    }
-    return token;
-  }
-
   private void sendForm(HttpServletRequest req, HttpServletResponse res,
       boolean link, @Nullable String errorMessage) throws IOException {
     String self = req.getRequestURI();
     String cancel = Objects.firstNonNull(urlProvider != null ? urlProvider.get() : "/", "/");
-    String token = getToken(req);
-    if (!token.equals("/")) {
-      cancel += "#" + token;
-    }
+    cancel += LoginUrlToken.getToken(req);
 
     Document doc = header.parse(LoginForm.class, "LoginForm.html");
     HtmlDomUtil.find(doc, "hostName").setTextContent(req.getServerName());
@@ -278,6 +294,20 @@
       }
       a.setAttribute("href", u.toString());
     }
+
+    // OAuth: Add plugin based providers
+    Element providers = HtmlDomUtil.find(doc, "providers");
+    Set<String> plugins = oauthServiceProviders.plugins();
+    for (String pluginName : plugins) {
+      Map<String, Provider<OAuthServiceProvider>> m =
+          oauthServiceProviders.byPlugin(pluginName);
+        for (Map.Entry<String, Provider<OAuthServiceProvider>> e
+            : m.entrySet()) {
+          addProvider(providers, link, pluginName, e.getKey(),
+              e.getValue().get().getName());
+        }
+    }
+
     sendHtml(res, doc);
   }
 
@@ -296,6 +326,43 @@
     }
   }
 
+  private static void addProvider(Element form, boolean link,
+      String pluginName, String id, String serviceName) {
+    Element div = form.getOwnerDocument().createElement("div");
+    div.setAttribute("id", id);
+    Element hyperlink = form.getOwnerDocument().createElement("a");
+    StringBuilder u = new StringBuilder(String.format("?id=%s_%s",
+        pluginName, id));
+    if (link) {
+      u.append("&link");
+    }
+    hyperlink.setAttribute("href", u.toString());
+
+    hyperlink.setTextContent(serviceName +
+        " (" + pluginName + " plugin)");
+    div.appendChild(hyperlink);
+    form.appendChild(div);
+  }
+
+  private OAuthServiceProvider lookupOAuthServiceProvider(String providerId) {
+    if (providerId.startsWith("http://")) {
+      providerId = providerId.substring("http://".length());
+    }
+    Set<String> plugins = oauthServiceProviders.plugins();
+    for (String pluginName : plugins) {
+      Map<String, Provider<OAuthServiceProvider>> m =
+          oauthServiceProviders.byPlugin(pluginName);
+        for (Map.Entry<String, Provider<OAuthServiceProvider>> e
+            : m.entrySet()) {
+          if (providerId.equals(
+              String.format("%s_%s", pluginName, e.getKey()))) {
+            return e.getValue().get();
+          }
+        }
+    }
+    return null;
+  }
+
   private static String getLastId(HttpServletRequest req) {
     Cookie[] cookies = req.getCookies();
     if (cookies != null) {
@@ -307,4 +374,9 @@
     }
     return null;
   }
+
+  private static boolean isGerritLogin(HttpServletRequest request) {
+    return request.getRequestURI().indexOf(
+        OAuthSessionOverOpenID.GERRIT_LOGIN) >= 0;
+  }
 }
diff --git a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthOverOpenIDLogoutServlet.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthOverOpenIDLogoutServlet.java
new file mode 100644
index 0000000..8fad0ad
--- /dev/null
+++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthOverOpenIDLogoutServlet.java
@@ -0,0 +1,59 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd.auth.openid;
+
+import com.google.gerrit.audit.AuditService;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.httpd.HttpLogoutServlet;
+import com.google.gerrit.httpd.WebSession;
+import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import java.io.IOException;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@Singleton
+class OAuthOverOpenIDLogoutServlet extends HttpLogoutServlet {
+  private static final long serialVersionUID = 1L;
+
+  private final Provider<OAuthSessionOverOpenID> oauthSession;
+
+  @Inject
+  OAuthOverOpenIDLogoutServlet(AuthConfig authConfig,
+      DynamicItem<WebSession> webSession,
+      AccountManager accountManager,
+      @CanonicalWebUrl @Nullable Provider<String> urlProvider,
+      AuditService audit,
+      Provider<OAuthSessionOverOpenID> oauthSession) {
+    super(authConfig, webSession, urlProvider, accountManager, audit);
+    this.oauthSession = oauthSession;
+  }
+
+  @Override
+  protected void doLogout(HttpServletRequest req, HttpServletResponse rsp)
+      throws IOException {
+    super.doLogout(req, rsp);
+    if (req.getSession(false) != null) {
+      oauthSession.get().logout();
+    }
+  }
+}
diff --git a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java
new file mode 100644
index 0000000..6d129bf
--- /dev/null
+++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java
@@ -0,0 +1,238 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd.auth.openid;
+
+import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
+import com.google.gerrit.extensions.auth.oauth.OAuthToken;
+import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo;
+import com.google.gerrit.extensions.auth.oauth.OAuthVerifier;
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.httpd.CanonicalWebUrl;
+import com.google.gerrit.httpd.LoginUrlToken;
+import com.google.gerrit.httpd.WebSession;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountException;
+import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.account.AuthResult;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.servlet.SessionScoped;
+
+import org.apache.commons.codec.binary.Base64;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+
+import javax.servlet.ServletRequest;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/** OAuth protocol implementation */
+@SessionScoped
+class OAuthSessionOverOpenID {
+  static final String GERRIT_LOGIN = "/login";
+  private static final Logger log = LoggerFactory.getLogger(
+      OAuthSessionOverOpenID.class);
+  private static final SecureRandom randomState = newRandomGenerator();
+  private final String state;
+  private final DynamicItem<WebSession> webSession;
+  private final Provider<IdentifiedUser> identifiedUser;
+  private final AccountManager accountManager;
+  private final CanonicalWebUrl urlProvider;
+  private OAuthServiceProvider serviceProvider;
+  private OAuthToken token;
+  private OAuthUserInfo user;
+  private String redirectToken;
+  private boolean linkMode;
+
+  @Inject
+  OAuthSessionOverOpenID(DynamicItem<WebSession> webSession,
+      Provider<IdentifiedUser> identifiedUser,
+      AccountManager accountManager,
+      CanonicalWebUrl urlProvider) {
+    this.state = generateRandomState();
+    this.webSession = webSession;
+    this.identifiedUser = identifiedUser;
+    this.accountManager = accountManager;
+    this.urlProvider = urlProvider;
+  }
+
+  boolean isLoggedIn() {
+    return token != null && user != null;
+  }
+
+  boolean isOAuthFinal(HttpServletRequest request) {
+    return Strings.emptyToNull(request.getParameter("code")) != null;
+  }
+
+  boolean login(HttpServletRequest request, HttpServletResponse response,
+      OAuthServiceProvider oauth) throws IOException {
+    log.debug("Login " + this);
+
+    if (isOAuthFinal(request)) {
+      if (!checkState(request)) {
+        response.sendError(HttpServletResponse.SC_NOT_FOUND);
+        return false;
+      }
+
+      log.debug("Login-Retrieve-User " + this);
+      token = oauth.getAccessToken(new OAuthVerifier(request.getParameter("code")));
+      user = oauth.getUserInfo(token);
+
+      if (isLoggedIn()) {
+        log.debug("Login-SUCCESS " + this);
+        authenticateAndRedirect(request, response);
+        return true;
+      } else {
+        response.sendError(SC_UNAUTHORIZED);
+        return false;
+      }
+    } else {
+      log.debug("Login-PHASE1 " + this);
+      redirectToken = LoginUrlToken.getToken(request);
+      response.sendRedirect(oauth.getAuthorizationUrl() +
+          "&state=" + state);
+      return false;
+    }
+  }
+
+  private void authenticateAndRedirect(HttpServletRequest req,
+      HttpServletResponse rsp) throws IOException {
+    com.google.gerrit.server.account.AuthRequest areq =
+        new com.google.gerrit.server.account.AuthRequest(user.getExternalId());
+    AuthResult arsp = null;
+    try {
+      String claimedIdentifier = user.getClaimedIdentity();
+      Account.Id actualId = accountManager.lookup(user.getExternalId());
+      // Use case 1: claimed identity was provided during handshake phase
+      if (!Strings.isNullOrEmpty(claimedIdentifier)) {
+        Account.Id claimedId = accountManager.lookup(claimedIdentifier);
+        if (claimedId != null && actualId != null) {
+          if (claimedId.equals(actualId)) {
+            // Both link to the same account, that's what we expected.
+          } else {
+            // This is (for now) a fatal error. There are two records
+            // for what might be the same user.
+            //
+            log.error("OAuth accounts disagree over user identity:\n"
+                + "  Claimed ID: " + claimedId + " is " + claimedIdentifier
+                + "\n" + "  Delgate ID: " + actualId + " is "
+                + user.getExternalId());
+            rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
+            return;
+          }
+        } else if (claimedId != null && actualId == null) {
+          // Claimed account already exists: link to it.
+          //
+          try {
+            accountManager.link(claimedId, areq);
+          } catch (OrmException e) {
+            log.error("Cannot link: " +  user.getExternalId()
+                + " to user identity:\n"
+                + "  Claimed ID: " + claimedId + " is " + claimedIdentifier);
+            rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
+            return;
+          }
+        }
+      } else if (linkMode) {
+        // Use case 2: link mode activated from the UI
+        try {
+          accountManager.link(identifiedUser.get().getAccountId(), areq);
+        } catch (OrmException e) {
+          log.error("Cannot link: " + user.getExternalId()
+              + " to user identity: " + identifiedUser.get().getAccountId());
+          rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
+          return;
+        } finally {
+          linkMode = false;
+        }
+      }
+      areq.setUserName(user.getUserName());
+      areq.setEmailAddress(user.getEmailAddress());
+      areq.setDisplayName(user.getDisplayName());
+      arsp = accountManager.authenticate(areq);
+    } catch (AccountException e) {
+      log.error("Unable to authenticate user \"" + user + "\"", e);
+      rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
+      return;
+    }
+
+    webSession.get().login(arsp, true);
+    StringBuilder rdr = new StringBuilder(urlProvider.get(req));
+    rdr.append(Url.decode(redirectToken));
+    rsp.sendRedirect(rdr.toString());
+  }
+
+  void logout() {
+    token = null;
+    user = null;
+    redirectToken = null;
+    serviceProvider = null;
+  }
+
+  private boolean checkState(ServletRequest request) {
+    String s = Strings.nullToEmpty(request.getParameter("state"));
+    if (!s.equals(state)) {
+      log.error("Illegal request state '" + s + "' on OAuthProtocol " + this);
+      return false;
+    }
+    return true;
+  }
+
+  private static SecureRandom newRandomGenerator() {
+    try {
+      return SecureRandom.getInstance("SHA1PRNG");
+    } catch (NoSuchAlgorithmException e) {
+      throw new IllegalArgumentException(
+          "No SecureRandom available for GitHub authentication", e);
+    }
+  }
+
+  private static String generateRandomState() {
+    byte[] state = new byte[32];
+    randomState.nextBytes(state);
+    return Base64.encodeBase64URLSafeString(state);
+  }
+
+  @Override
+  public String toString() {
+    return "OAuthSession [token=" + token + ", user=" + user + "]";
+  }
+
+  public void setServiceProvider(OAuthServiceProvider provider) {
+    this.serviceProvider = provider;
+  }
+
+  public OAuthServiceProvider getServiceProvider() {
+    return serviceProvider;
+  }
+
+  public void setLinkMode(boolean linkMode) {
+    this.linkMode = linkMode;
+  }
+
+  public boolean isLinkMode() {
+    return linkMode;
+  }
+}
diff --git a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthWebFilterOverOpenID.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthWebFilterOverOpenID.java
new file mode 100644
index 0000000..ff02419
--- /dev/null
+++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthWebFilterOverOpenID.java
@@ -0,0 +1,100 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd.auth.openid;
+
+import com.google.common.collect.Iterables;
+import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import java.io.IOException;
+import java.util.SortedMap;
+import java.util.SortedSet;
+
+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;
+
+
+/** OAuth web filter uses active OAuth session to perform OAuth requests */
+@Singleton
+class OAuthWebFilterOverOpenID implements Filter {
+  static final String GERRIT_LOGIN = "/login";
+
+  private final Provider<OAuthSessionOverOpenID> oauthSessionProvider;
+  private final DynamicMap<OAuthServiceProvider> oauthServiceProviders;
+  private OAuthServiceProvider ssoProvider;
+
+  @Inject
+  OAuthWebFilterOverOpenID(DynamicMap<OAuthServiceProvider> oauthServiceProviders,
+      Provider<OAuthSessionOverOpenID> oauthSessionProvider) {
+    this.oauthServiceProviders = oauthServiceProviders;
+    this.oauthSessionProvider = oauthSessionProvider;
+  }
+
+  @Override
+  public void init(FilterConfig filterConfig) throws ServletException {
+    pickSSOServiceProvider();
+  }
+
+  @Override
+  public void destroy() {
+  }
+
+  @Override
+  public void doFilter(ServletRequest request, ServletResponse response,
+      FilterChain chain) throws IOException, ServletException {
+    HttpServletRequest httpRequest = (HttpServletRequest) request;
+    HttpServletResponse httpResponse = (HttpServletResponse) response;
+
+    OAuthSessionOverOpenID oauthSession = oauthSessionProvider.get();
+    OAuthServiceProvider service = ssoProvider == null
+        ? oauthSession.getServiceProvider()
+        : ssoProvider;
+
+    if (isGerritLogin(httpRequest) || oauthSession.isOAuthFinal(httpRequest)) {
+        if (service == null) {
+          throw new IllegalStateException("service is unknown");
+        }
+        oauthSession.setServiceProvider(service);
+        oauthSession.login(httpRequest, httpResponse, service);
+    } else {
+      chain.doFilter(httpRequest, response);
+    }
+  }
+
+  private void pickSSOServiceProvider()
+      throws ServletException {
+    SortedSet<String> plugins = oauthServiceProviders.plugins();
+    if (plugins.size() == 1) {
+      SortedMap<String, Provider<OAuthServiceProvider>> services =
+          oauthServiceProviders.byPlugin(Iterables.getOnlyElement(plugins));
+      if (services.size() == 1) {
+        ssoProvider = Iterables.getOnlyElement(services.values()).get();
+      }
+    }
+  }
+
+  private static boolean isGerritLogin(HttpServletRequest request) {
+    return request.getRequestURI().indexOf(GERRIT_LOGIN) >= 0;
+  }
+}
diff --git a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdModule.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdModule.java
index c87a0cf..ace0c53 100644
--- a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdModule.java
+++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdModule.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.httpd.auth.openid;
 
+import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
+import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.inject.servlet.ServletModule;
 
 /** Servlets related to OpenID authentication. */
@@ -21,9 +23,12 @@
   @Override
   protected void configureServlets() {
     serve("/login", "/login/*").with(LoginForm.class);
+    serve("/logout").with(OAuthOverOpenIDLogoutServlet.class);
+    filter("/oauth").through(OAuthWebFilterOverOpenID.class);
     serve("/" + OpenIdServiceImpl.RETURN_URL).with(OpenIdLoginServlet.class);
     serve("/" + XrdsServlet.LOCATION).with(XrdsServlet.class);
     filter("/").through(XrdsFilter.class);
     bind(OpenIdServiceImpl.class);
+    DynamicMap.mapOf(binder(), OAuthServiceProvider.class);
   }
 }
diff --git a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
index 5817a55..44b58c3 100644
--- a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
+++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
@@ -16,6 +16,8 @@
 
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.auth.openid.OpenIdUrls;
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.httpd.CanonicalWebUrl;
 import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.reviewdb.client.Account;
@@ -90,7 +92,7 @@
   private static final String SCHEMA_LASTNAME =
       "http://schema.openid.net/namePerson/last";
 
-  private final Provider<WebSession> webSession;
+  private final DynamicItem<WebSession> webSession;
   private final Provider<IdentifiedUser> identifiedUser;
   private final CanonicalWebUrl urlProvider;
   private final AccountManager accountManager;
@@ -102,7 +104,7 @@
   private final int papeMaxAuthAge;
 
   @Inject
-  OpenIdServiceImpl(final Provider<WebSession> cf,
+  OpenIdServiceImpl(final DynamicItem<WebSession> cf,
       final Provider<IdentifiedUser> iu,
       CanonicalWebUrl up,
       @GerritServerConfig final Config config, final AuthConfig ac,
@@ -155,6 +157,7 @@
     final AuthRequest aReq;
     try {
       aReq = manager.authenticate(state.discovered, state.retTo.toString());
+      log.debug("OpenID: openid-realm={}", state.contextUrl);
       aReq.setRealm(state.contextUrl);
 
       if (requestRegistration(aReq)) {
@@ -482,11 +485,10 @@
 
     final StringBuilder rdr = new StringBuilder();
     rdr.append(urlProvider.get(req));
-    rdr.append('#');
     if (isNew && !token.startsWith(PageLinks.REGISTER + "/")) {
-      rdr.append(PageLinks.REGISTER);
+      rdr.append('#' + PageLinks.REGISTER);
     }
-    rdr.append(token);
+    rdr.append(Url.decode(token));
     rsp.sendRedirect(rdr.toString());
   }
 
diff --git a/gerrit-openid/src/main/resources/com/google/gerrit/httpd/auth/openid/LoginForm.html b/gerrit-openid/src/main/resources/com/google/gerrit/httpd/auth/openid/LoginForm.html
index f5734ffe..07e09f5 100644
--- a/gerrit-openid/src/main/resources/com/google/gerrit/httpd/auth/openid/LoginForm.html
+++ b/gerrit-openid/src/main/resources/com/google/gerrit/httpd/auth/openid/LoginForm.html
@@ -16,9 +16,19 @@
       #logo_box {
         padding-left: 160px;
       }
-      #logo_img {
+      #logo_oauth {
+        width: 96px;
+        height: 96px;
+        display: inline-block;
+        margin-bottom: 20px;
+        background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3wQQBh8CrfzmBQAAIABJREFUeNrtfXlYlOX6/z0rszADDAwgywDDvskmu8KwDJugICguaImmImpUWlp6tNJT3zxWv7pOfrs8nTrZbnY0LZdSK81jpn5dKtwV0UgRBWQbGPj8/vj6PmdeBxTQOp7v6b6u57rged95l+e5n/u578+9vES/0+/0O/1Ov9N/KonutwdatmwZffXVV1b9r7zyilitVktNJpO0ublZ4uzsbOPs7CzTarUytVptIxaLJT09PeLIyEhxbm6uqKKiQrBp06aeW69TVVVF+/btu2/eV3A/PMRDDz1Ea9as4fVNnTrVbc+ePa5CodC9rq7OXqvVhgDwbmhoUN64ccNGoVDYS6VSO6FQKOvu7m7r6Oi43tXV1azRaDrVanVjT09PbUtLy0mNRlPf1NR0oaysrH7lypVXLO/x5JNP0h//+Mf/3OX34IMP8v43GAzxwcHBS+zs7Nbb2toekMlkF+VyOYho0E2hUJglEskZlUr1nYuLy/s6nW7e3LlzI35fAf8UOSF/+9vfZjY2No5rbGy0FQqF8u7ubiYanZ2dKSoqisLDw8nPz490Oh0NGTKEnJycSCqVkkDwz1doamqiCxcu0Pnz5+mHH36gQ4cO0d69e6mrq4udIxQKuwQCQYejo+N5f3//1d9+++1q7tjChQvp+eef/7/H6cuWLaPY2Fj2f1xcnDI6OjrNz8/vfwQCAeNWuVwOBwcH5Obm4u2338a1a9dgSV1dXWhra0NzczMaGxtx7do1XL9+HY2NjWhqakJraytMJhNupePHj+Oll15CUlISHBwcYGNjw+7p4uLSExoaujwlJUWbn58v5p7xqaee+r+3AiorKx22bNmS3dLSMqepqSnZZDIREVFQUBDFxsZSYWEhFRUVkUAgILPZTCdPnqQLFy7QhQsX6MyZM1RTU0N1dXVUX19Pra2t1NraSiKRiGxsbEilUpFWqyVXV1fy9/cnf39/8vT0JHd3d/L19SWR6H8XVX19PW3YsIG2bNlC+/fvp0uXLhERkVqtNqvV6pfc3Nw+2r9//wEiory8PPr888//fTn/scceY38nJCSMsbOz+0omkzHuGz58ON58802cPXsWANDd3Y2NGzeisrISqamp8Pb2hkQiGbT8VyqVCAwMRFZWFubPn4/t27fzVsbhw4exatUq+Pj4gIggEAigUCjqPDw8/rxmzRpn7tkfeeSRf7/Bz8jIICKiy5cvy/V6/QaRSGTmBsbX1xdbtmxBW1sbAKCurg4zZ86Eq6sr5HI5LMVSb620tBSzZs2CRqMBEaGwsBDl5eXs/96aUCiEQqGAi4sL5s2bh+PHj7OJuH79Ov785z9DqVRy5/bI5fLa5OTk4n9b7k9JSRGkpKSMcnR07OG4y8HBAa+88grj9urqaowdO7bXgbod5+/atQu1tbUIDAwEEeHLL79EXV0dQkJCeOd5enrCYDDAwcEBCoXCamLj4uKwdetWNDc3AwAaGhpQUlIChUIBIoJMJkNYWNgLiYmJKu69lixZcv8OemJiIhERlZWVuep0utfFYjHbXKdPn46rV68CAL799ltMnTq118EdM2YMtm/fjscee6zPlfDll1/i7NmzCAgIABFh27ZtqKmpQXBwMO+8sWPHorOzE2azGZs3b8a8efMwYsQICIVC3nmRkZF4/fXX2fNt3rwZCQkJ7LiHh8f+6OhoprqWlpbef4OfnJwsICIaOXJkrKOj42GRSAQiQnh4OJO9ly9fxsyZM6HVam/L3dwKGTNmTK/n7Ny5E6dPn4a/vz+ICFu2bOl1AhwdHREXF4cLFy7wxE1f946JicHbb7/Nzlu0aBE7plar68LDw8fdlypmZGQkERHl5+dPEovFN7iHfvTRR9HU1AQA2LRpE3Q63R3le3V1NZqbm7Fjxw50d3cjNDTU6pzNmzfjypUrCAsLAxHhxIkTOHHiBHx9fa3OlUgkAIB9+/bB3d0dI0aMuO39xWIxjEYjGhoaAADbtm2DxUrujo2NnUhElJ6efn8MvoWWM00ikXTe5BYm69va2vDCCy/0W3NpamrC8ePHkZGRgfb2djQ1NcHZ2Zl3zkMPPQQA+PHHH7F//34AwPr168GtOss2efJkAMCf/vSnPu/5xBNP4JlnnuExh5OTE3bu3AkAqK2thaenJxOnw4YNm8S9d15e3r9uAjjODw8Pn65QKLqICIGBgdi8eTMA4Pz58zwx4uHhgZycHBQWFlqJC26jbm9vR3d3N65fv45Dhw4BAE6ePMnTcEQiEVasWIF//OMf+P7777F27Vp4e3v3Orjbtm1DV1dXn5yfmJgIAKiurmaDzDWpVIoXX3wRAPDLL7+wa9jY2CAkJGQmNw7Tpk377Qc/Li5OQEQ0fPjwSrlc3klESElJwYkTJwAAe/fuRXh4OHuZlJQUHDhwgMniixcvYtSoUTyuGzJkCEwmE44ePYqkpCS4uLigsrIS5eXlVquAW2mOjo59crZcLkdtbS2am5t7Pe7g4IDDhw+ju7sb2dnZICKkp6fjlVdegZOTEztv6tSpAID6+noYDAZOQ+oKCQmZ/i/h/ISEBCIiMhqNU0QiURcRITMzE3V1dUzeWr6As7Mz9u3bh/r6emRlZWHixIkMPpBKpbxN0Gw2Y+HChTy1dLCGmEAggL29PY8RLNtzzz3HGGLv3r2IiYlhqy4lJYV3nczMTLS3t6OzsxORkZEc0HcjOjo66VZx/JtQWlparFQqvU5EiIiIQG1tLQDgwIEDVjr80KFDAQBlZWWsb8GCBQCAuLg4HkcPZIClUikzngbahg0bhpaWFhw9ehSPPfYYD0N68803e/3N2LFj0dzcjM7OTmaDODk5XU5OTtZYGp+/OhmNxiEajeYwEUGn0+Gnn34CAOzYsYPH0VwLDw+32ggXL14MAEyX76vJZDJIpVI4OztDpVLxtJuCggLMnDkT7u7uvN8EBARAqVTCwcGhz5VhMBhw8OBBxMbGgohQXFzMgLvbGYEzZsyA2WzGuXPn2LPrdLqvAYiJiBYvXvyr6vpEROTj4/MWNwjff/89AODrr7+Gi4tLrw/t6OiIPXv2AADWrl2L1atXo62tDdu3b+9Vc7H8ncFggEgkglarRXFxMRNJRqMRvr6+kEqlSElJQVBQEIgIBoMBMTExEIvFCA8PR1RUFG/ztry+SqWCUCiEjY0Ndu/eDbPZfEc1lYhQUVEBAPjuu+/YO+v1+td+Va5/9NFHuUmYwG2cH374IdN2uCXZVwsPD8ePP/7IlvnmzZvh5ubW67n29vaYMmUKcnJyGCxwU+Zi5syZyMvLY0YYxwgJCQmYMmWK1YqKj49HRkaGFeRh2ZydnXH69Gm8/PLL/dpzBAIBli5dCgD4+OOPOc2oOT4+vuimeL63g19QUEBERP/93//twMnpqVOnor29HSaTCSUlJf2WvTqdzkpzEQqFvBdXKBQoLy9Hamqq1YAUFxfjscces7puSEgIHnnkESuxI5PJsGLFCkRERNz2nhx62t/3UKvV2Lp1KwAwpUGtVu8ZM2aM86+2Ctzd3T+/Ve4PxMjqq2m1WqZZ2NnZwWg0wsnJCXFxcUwkSKVSZGVlwdfXFzKZDCUlJWxPCAsLw/Dhw2FjY4OCggLodDoGxk2YMIEBb9wkyGQyxMfH81bXYDfyX375BQDYc/r6+s4lIpoxY8a9GfTU1FTO2BorFos7LNW3Xbt23VaG97eJxWIkJibCYDAgNzeXp8IGBQVhzJgxSEtLg16v5+0PeXl5yMjIQHR0NE8zMhgMMBqNyMzMZDCCUChETEwMEhISUFBQAFdX17t+bs6K5nwLCoUCKpWqZdasWS73lPNnzZrlYGdnt+3mZgMAaG9v7xWnGWxzcHDA0qVLeYNpqXlMmTLFqj8nJwfPPvusVb+dnR1WrFhhhQ3Z2NjgqaeeQnp6+j17biLCsWPH0NPTg0cffZRjmjfv6Srw9vbO56ITvvzySwDAc889N2juVygUyMvLY1yoUqmQnZ0NmUyG4uJiHq5fXFwMPz8/BAYGYvjw4eyeERERTNuZN28e67e1tcXcuXNBRBg5ciTbb+RyOUaNGgWJRILk5GSe/ZGUlAQ/P79BT0BwcDAzQF1dXSGRSLBo0SJfIqKHH3548ANfUlJCHh4eYk9Pz12WuMm5c+fuqPX0ZxPLyspCYGAg0tLSMGTIEHYsMzMTw4YNYzKf609ISEBCQgJCQ0N56qJCoUBRURFCQkIwbtw4HsePHDkSfn5+SE1N5WldsbGxiImJwYgRIxATE3PXq+C1114DAIZ9ubm5fXpPuL+wsDCWM6527dqFnp4evPjii4OCBW7t0+v1WLFiBRITE600lIULFyI3N9fqNzNnzsSkSZOs+j09PbF06VKescatrmXLliEjI8PKiq6oqMD48eP7fL6BNH9/f7S1teGLL76AWCyGVCq9bjQaIwdtnM2aNYuIiPR6/eccmHbt2jWYTKbbAmB9NVtbW5666ujoiPz8fCiVSuTk5PA4vaioCN7e3jAajTxxZG9vzzZTo9GIrKws1gwGA7RaLTuHWwEPPPAAZDIZRowYwbvHiBEjEBgYiISEBCQnJ7N+g8HAtKiBNIlEgldffRUA4OPjA4FAADc3txfvCudfvny5B6dFvPzyywCANWvWDJpLnJycMHnyZKhUKmRlZcHOzo6nYnLWrqU8LiwsZOIuOTkZP//8M25HL730EqRSKRQKBQoLC8FFYUgkEqZhJScnMwiCu25cXByioqKsVuNAWnZ2Njo7O5mEUKvV/5Obm6sb8AQEBwdzkMNfORyfQwlvxV0Gs1SfffZZqz3E2dkZS5cu5XEjJxamTJmC+Pj4Ow4+R2vWrIG/v7+Vz0GlUmHhwoVMTgsEAt49pk+fflfoq0KhwL59+3Dt2jW2An19fW/rsRH21lldXU2vvfaa8+XLl0cSEYWEhFBUVBR98cUXLJCpv+Tm5kaWYYb+/v709ttvk6enJykUCiIiEolEFB4eTuvWrSOFQkG+vr5ERASAjEYjHT58mAwGAw0ZMqRf95w0aRLJ5XLy8fEhqVRKREQSiYSioqJow4YNdOXKFRo6dCgBIACUnJxMZ86coSNHjlB0dDS7jkQiIbFY3O93bWtro+3bt5ODgwMZjUYymUzU0tKSl5SUJBmw+AkMDJwmEonaxWIxVq1aBQDIy8sbMFdoNBpMnDgRtra2GDVqFBMJTk5ODJ8pKipiwVEikQhZWVnw8PCA0WiEv78/1Go1vvrqK/SXurq68Pjjj4OIUFlZyRxC3KoTi8UYMWIEvLy8kJKSwouASEhIwPDhw6FWq5Gbm8sMuf620NBQdHZ2Yu3atSAiuLq6XtfpdMoBB3c5ODi8JRQKIZfLcebMGTQ0NPBUxYG0wMBALFq0yArv12q1ePXVV63EkUQiQWVlJXOKeHl5YaD0xRdfsMH+r//6L94gc/eoqKjoVdMyGo1YvHhxr9B6f9rZs2dx8eJFy70hakAiaPbs2c7d3d3+PT09pNfrSa/X044dO6i5uXnA+4mDgwN5enrSgQMHKDQ0lPXL5XKKjIykt99+m3x9fUmtVrNjISEhdOrUKbKxsSG1Wk0jR44c8H39/f0pLCyM4uPj6ZNPPiGtVkt2dnbseGBgIFVXV5NIJCJvb2/Wr9PpSK1W05dffkmhoaG86Ov+0kcffUQajYZSUlKIiKi2tvahgW7CEUql8iIRYcmSJQDQKwLZH5AtPz+fOdSDg4NRVFTENAYvLy8mjnJyciAUCpGVlcXgCDs7O0ilUhw8eBCDoQcffJCtWnt7e4wZMwYCgQAJCQlITEyEQCCARCKBwWBAWFgYvLy8kJWVxTg/Ojqapy31tyUkJAAAFixYwBll9TeDk/s3Ab6+vjm2trYgIuzfvx8mkwkFBQUDeojExESUl5dbLWMnJye88MILVuJMrVZj+fLlVviNi4sLBktr166FZYKHWCzGc889Z2X5ikQi5OTkoLy83MobFhsbywZyIA0A/va3vzFta8OGDa79FkENDQ12bW1tREQUGxtLdXV1VFNT0+8VJJVKyWQykY2NDWm1Wt6x4cOH09atWyksLIzX7+fnRzt27KDExEQSCv/5WFOnTh20FZ+VlUX29vbs/4iICPr0008pLi6Ohatz4tDNzY2uXLlCjo6OrF8sFlNQUBDt37+fPDw8BnTvH374gfz9/cnJyYkA0OrVq439+uFTTz0lDg4OfoHbPAHg4MGDPIj4Tk0kEjFwrKSkBCEhIbCxsUF2djaL3xk6dChSU1NBRIiKimKbpEajQVFREezt7UFE+OGHH3A3NHLkSOaR4+4hlUpRUlICpVIJT09PlJaWQigUQiwWIy8vDz4+PhCLxSgoKGAOnoGGyb/xxhu4ePEic5W6ubn1L/WmoKDARq/Xv2fpqN65c+ddGV/JycmYMWMGPDw8eP3e3t5Yvny5lRbk6OiItLQ0JCYmor29/a4mYMOGDVAqlVZBuZbuTcvB5VTUqqoqZqkPpi1YsABms5lNekBAwD/6JYIOHTokuHLliowTC0REV65cuStAT6PRUFdXF9nY2PD6Q0ND6bPPPqOkpCSr38hkMkpJSRmQIdQbjR49moRCITk5OVFPzz+zVtVqNV25coX0ej0vh8xsNlNMTAwdPnyY3N3dB33f06dPk0gkYiKwvr5+CBHR448/fsfBksvl8q8t8Z+XX355QLPv4+PDNtkxY8YwI6usrIxBGRyszCGZ+fn5TFuZNWsWiAhffPEF7gU99NBDcHZ2Rnp6OoRCIVQqFSZNmsTE5Ny5c5nY5OAIkUgEo9EId3d3CAQCBAQEDEgMx8fHAwBziQoEgp/7NXNeXl5KBweHw0SEd999Fz09PViyZMmAU4Py8/NRVlZmhSyOHj0aY8eOtdJEgoKCMHHiRJSWljKL8syZM/dkAr7//nt2j6KiIpSWlvKcSWKxGGVlZRg/fjwPzub2hNGjRw9YHfXz8wMAVFZWQiAQQCQS1W/cuNH9jiJIKpUKJBKJmojI1taWAFBLS8uAll9XVxf19PSQSqWi1tZWIiJm0LS0tJBSqeRpOkRE3d3dpFAoqLu7m2ksPj4+98SnER4eTl5eXmQ2m0mhUJBQKGT34cSOSCQisVhMZrOZ1y8Wi0mhUFBHR8eAx4AzRIVCIQkEAmF1dbX6jhNwc6AEHBgFgCcj70QCgYBiY2Pp2LFjtHr1asrPzydnZ2cCQKNGjaJz587RW2+9RSqVigIDA4mISKvVUlhYGL3xxhu0d+9eMhgMlJCQMCgrtDcSi8U0efJkam1tpXXr1tGWLVtowoQJ7PjDDz9M77//Pq1bt44mTpzI3iMjI4MOHz5MH374IclkMrYn9ocAMECPG5qmpiZpf4wwpZOT03G6mQhhNpsxZ86cu9KCSktLMWrUKKaSWfaHhIRgxowZVloQF2V9r+hWTc7d3R1ZWVlMRFhC4PPmzUNMTAwLlRlM0+l0AIBnnnkGIpEIYrH42tNPPx16xxVgMplgNptbiYg6OjpIIBBYaS8DpTNnzpC3tzddu3aN1797926aM2cOffPNN7x+vV5PAQEB9zSyw9fXl+UzEBFdvXqVgoOD6cyZM4xbOc49dOgQZWdn06lTpwZ9P07EWog6aDQa0x0noLm5uae9vf0yEdH169dJKBSSUqkcMBCm0WiIiCgzM5NMJhO98cYbFBcXR05OTszpExwcTLNnzyaNRsNC3omIxo8ff8+DynQ6HcXExPDU07feeouqq6tpzJgxrD8nJ4d++eUXWrVqFaWnp5OtrS2bQEsw707EMW1TUxPnd+hOT09vvOMPw8LCbLRa7XoiwvLlywEAr7zyyoCWn6OjI1JTUzFu3DimgnKtrKwM4eHhyM/P5xlGI0aMYO5ALlvxXtPatWshk8kwb948WCaMq9VqTJgwAenp6TyPn1wuxwMPPICIiAiWwNHflpKSAgB44IEHuECDS/0yxEJCQmBnZ9d8E0YlIiJXV9cBcVtDQwPJZDJSq9XsGhxt2LCBqqqqaO/evTzD6ODBgzR06FBKTEzk4TH3ktLT0yksLIx27drF02paWlqooaGB/Pz86Oef/6mut7e309GjR2nGjBm0bdu2Ad3L09OTjcVN+8rUW4SE1QSkpaV1E9EpzprjtJSBkKOjI/3888+0Y8cOyszMJJVKxURTQUEBTZs2jWJjY9nE2tnZUXp6Or3++utUXl7+q8W2urm5kUajIVtbW95+kJiYSNeuXaPt27fTiBEjWP/QoUNJrVbTnDlzWA70QMQwEVFjYyM3AT/fDHToF3RQIhAI4OrqCgA4dOjQbXN7e4sB4sSLq6srjEYjhg8fzvKrOGMtNzcXPj4+KC4uZqBXfX09fk36+9//zpKzo6OjMXLkSF5Iu4+PD4qKihAWFobk5ORBO+nXr1+P+vp6DB06lAPjXuj37AUFBWWoVCozEaGzsxM1NTVW4d0DjYRYvHgxOB+D5SS88847DLYwGo3o7OzEr02cj2Lu3Lk8prDcj3qLOR1Iu3TpEg4ePMj2lLS0tAf77Q9oaWm5ZDKZzhIRfffdd+Tm5kZeXl6DBuKGDx9Of/nLXyg5OZlFKahUKiouLqaysjJKSUkhuVxOBQUFfYJvZrOZampq6OTJk7x24sQJq75z587d9pkmT55MAQEBtH37dpLJZBQSEsITO/b29rRq1SrKycnh+Q36SwqFgtzc3Oj8+fMMyCwqKvq23xeoqqpyUqlU3xARli1bBgCYP3/+gEPOAwICWOgfBz9nZWXB09MTubm5DHcRCAQIDg5mSde90cGDB5mPwBJS7gsga2xs7PNa+/fv552blpaG6OhoREZGMh8Fhx3Fx8cPmPvHjx/Py53QarUmIuJZ37ddAS+//PJVpVJ5ioho06ZNzDM2EHtAoVBQeno6ffDBB6zv/PnzdOrUKaqoqKBTp07RjRs3mPGj0Wh4zvFb6ejRo2xDs/R4VVVV9Xr+unXr+rxWZGQkW9ECgYB27dpFQUFBFB0dTbt372bnHT9+nKcV9ZfKysqoo6ODVWeUy+UbuUCAOxLnOPb29p4pFAo7lEolmpubcfny5QEnNfj7+/My5eVyOSoqKuDg4IDs7GxeSlBVVVWfHNvW1oaqqipeWIuvry8MBgNcXV0xffp03n0nTZqE3NzcPq/X3d2NJ554glctJSMjA7Gxsaz+BBdUMNA0WLlcjhs3buDy5ctsb8vJyZky4FmcM2dOmEQiaZBIJHj33XcBwCrCuL+w7MiRI2Fra8uMkptaAbKysqBQKKDRaLB+/fo+B6y+vh5ubm4YPXo0dDodNBoNiouLGaTMxZSKRCLmSwgODsbp06f7vObXX3/N0pssA85ycnIQHBwMrVYLg8HAqy/XnzZ16lSYzWbs2rWL+TeUSqWyqKho4Buoo6PjYY6juIIYg9EIYmNj8fTTT1u5+AICArB48WLExMSgurq6z8Hau3cv+828efNYJoplCwsLw5/+9Cc2YFKp9LaT+uOPPyIzMxOFhYVWcaKTJk1CZWXlgBPHiYgl7nFJ6UOGDNkcHR1tM6BMes5tFh8f/wTHTVzNnYHYA1yLiIjAsGHDkJaWxluqaWlpGDp0KLy9vXHs2LE+B4tDYyMiIpCUlISoqCheEC/nQI+IiGBxR3SzXE5fau1PP/0EPz8/pKSk8OyAgIAAGAwG6PX6AecMJCUlob6+Hh0dHRCJRJBIJPD19X2go6NjcLg6AJGtrS0EAgHef//9QbknuQHixBE3QBkZGSxjxTK2/lbq6emBSCSCXq9HSkoKu1ZsbCyzTSoqKhjnOzk5MXjby8vLqvQlRx999BF7vqysLPj7+0On0yEtLW3A8aCWIZBciPxNLe18QEBA6KAGf9GiRZyL8h26WSehpaUF7e3tuJtqtoGBgXjyySetIiH0ej0OHjzIOLanpwcmkwlZWVlITExEWVmZVU7aqFGjsHTp0l4j8p555hnodDpMnToVbW1t6OnpYYG7p0+ftirwV1paiilTpgw6HtTDw4PVy+CMLwcHh3cBCG83zn1aGXv27OEArNNnzpypOHbsGJczRt3d3b0W2O4PhYSEML9AXV0dASCBQEAymYzeeecdqq+vp9raWtq5cydNnz6d6urqKCgoiBoaGqi9vZ2prmq1mlxcXOjq1askl8vpl19+YWplZGQkNTY2kkwmo40bN9LWrVupubmZTp48Se+99x7Nnj2bRCIRccFn7u7uZG9vT01NTXTp0qUBeQA5Gjt2LJWVldG7775L77zzDgmFwrawsLCnZ8yYcXz69Ol06NChgQ/WuHHjyN/fX6zT6bZwSxUATpw40WeBpNs1oVDIYoMCAgKYVpWUlIRhw4Yx3CUwMBASiQShoaEsW97GxgY5OTlwcXGBSCRCYWEh29SHDRvGkvYKCwtZho1er2dqsI+PDztfLpcjOzsbDg4O8PPzQ0ZGBmQyGSQSyaAzP2tqamA2mxm04ejoePSeoYje3t7ZXFb5p59+ykvPv5vm6emJJUuW8HRvupnJUl5ejrS0NKsBKSkpwaxZs3otibNgwQIry9jBwQFPPPGEVXi6SqXCtGnTkJ2dPSiZb9kKCwsBAJ988gnzMxQWFmbfhCBu7znrzwSMHz/+HxKJ5HOifybvPf744wNyUvdG7u7uVF1dbRV3KZPJmFfJwqlNYrGY1Go1Xb16leczEIlEJJfLyWQyMejbEpevqamx8mkoFApqamoiJycnXiTEYOi1116jGzdu0BtvvEEdHR3k5eW1Z8OGDdvy8vLo73//+91xf2ZmJme+j5ZIJO1EhIULFwIAPvzww7uqZsXVaOOsWu7vUaNGMcd5dnY21Go1bG1tUVRUxDjfaDRiyJAhEAgESElJYWVp8vLy2N9RUVEsycPb25t5tTw8PJCZmcnEzt1w/9NPPw0AWLduHVeHoruoqCiUfg3S6XTrOVVv3759ANCrUTSY5u3tjenTpyMpKYkZRFLXAAAI+klEQVQndpycnJCXl4fCwkIenM3VhJg0aZKV2zMiIgKTJ0/m1QriRN60adOQlpbWqyanUCh49SjuZAckJCSgoaEBzc3NTKvS6/XLOJ9zv5z3/TmppKSEiIhWrFgx1d7evvvq1au0YsUKam1tpeXLl5PBYLjryb127RpdvHiR3NzceEFTV69epZCQEOrq6uIFc3V1dZGjoyOpVCqmGVmSWq22gpJlMhk1NTXRiRMnqL29nXdswYIFdOnSJTpy5AjV1tZSRkYGL1riVrK3t6eVK1eSRqOhsWPH0rVr10ilUh25mVlKGzduvLfcP3/+fE4tzeA2rZdeeomVfBxI+qq9vT0SEhJ6Xf7e3t6sjoNIJEJFRQXT7bOzsyGXyyESiZCWlsa0ndLSUla5KjIykuV9WSaA29jYWEVnc+3hhx9mnr/du3fDZDLh8uXLfRb7EwqFrJwllxMslUpbIyIixt9EEH4dnypXLVav1/8/bnlyAbSbNm3qN3bCpT1lZWXddo+Ij4/niSM3NzdkZ2cjMzPTSrxkZWUhNzeXqaPc8/Un8bqzsxPff/89+w03IX1peosWLWIYFQfN6HS61+m3ovz8fDsnJ6ednIzevXs3AOCdd97plyXMZTFycrO0tNRKjtNtUp96qynNoaX91eO5wba1tQUAfPXVV7ykcQC9WtlcfaArV66wyDlPT8+dx44dkxARzZw587eZhFGjRulVKtVlznPEQb9btmy57YtzGfebN2+GXC5npV4++OCDfkEcN8P8ehULdxp8jUaDVatWMbHGtVOnTsFkMuHJJ59EaGgoduzYgdbWVuTk5PDOq6ysREdHBwCwleXs7HwxJibGnuh/069+E+I23ZKSkqEymayNbmbBXLx4kalkloFPdEu1wZ9//hkmkwl1dXU4e/YsWlpamOpJtxRYsrW1HbR1KpFIeGKRqyM9e/ZsKyi7paWF5wBauXIlb3InTJgAADCbzcjMzOSCChoiIyNj6F9BXJxMRkaGQalUXiYipKamoqamhnG4ZS1mGxsb7N+/H21tbfDy8kJiYiIDryyRScv2yCOPYO/evbf1y9rb28PLy8tq/xGLxZg7dy5++uknFBcXY+HChTh58qRVAVnLYNonn3wSK1as4MHmUqkUjz/+OPvAA7cq5HJ5W2ho6L+2nD2328fExKSoVKpaTgf/7rvvAABHjhxh5cHEYjHi4uJQXFzMsJmTJ0+ipaWlTx/Dpk2bAKBX3EkgEKCiogJ79uzBiRMnsHv3buTn5/N096ysLLS3t6Onpwdnz57FRx99BAD9TrnVarV477332GdWOOxKoVAgPDx88q1u3H8ppaamhnN7gqurKz7//HMAQEtLC/7whz/wBk4gELDCfxMnTuwTJ/rpp59w4cKFXo+npqaiubkZV65cwV/+8he0tbWhtrbWCmaOi4tjAOLKlSut6kLfTlngKkOeOXOG+cPlcjlSU1MzufeeMmXKv37wuSS7+fPnu3G5BRKJBH/961/ZprVx40Z4eXkxDrWxscG0adN6ldsKhQI5OTkwmUxYtWpVrwNUXl7OKxc5f/58AOi1lFpaWhrzBQC4bdy/RCJh3yfgsiw5lFahUDQlJiaOvImP0X1FXOFqAFJnZ+e3ua8mTZ06lX2x6OrVq6isrOyzxDHdLEl25MgR9imT8vLyXrWjadOmAQDLNZs4cSIA8FyVlm3cuHFoa2tDS0tLnwaZwWBgn1u5ePEiqqqqLCOoTw4dOjSd7mfiluOFCxdEAQEBU9RqdTNn3T777LPMK7Vv3z7MnTu3V6wlKSkJa9asYQl69fX12LRpk5UmxA04t8dwOc29VT/hNBmDwYDExESra8XExOCDDz5gmtCaNWsYRH7Th/FxdHQ0yxj5zXT9u6UxY8a4e3t7f8vJ/sDAQFb20mw249KlSzwus9Q8VCoV9Ho95syZg9WrV/fK0ZZVCkePHn3bPaW3FhISgq1bt+LGjRsMUklOTmb+ZblcjoCAgEeMRqP8VlftfU+WRUsjIyNn29raNlvK5AMHDrAseJPJhOeffx5+fn596v23rpagoCAsWbKE5Z05OTkhLS3ttlEb3MRmZ2czEdfZ2Ynz58/jwQcftFwtZjs7uxMZGRmJ9O9OnKpaVFTkp9Vq35XL5Y1kURhp7dq1vIS87777Dn/4wx+Qm5uLkJCQQcXm3ApRxMXFYdKkSVizZg2Lkrh06RLWr1/PEqk5K1uhUBzX6XRPA5D/Fmrmb/oxTwBCHx+f4V1dXbMbGhpKuSwVf39/ioiIoLS0NCooKGDZJRcvXqRz587RpUuX6OTJk3T27Fmqq6uj69evU3NzM5lMJgJAIpGIVCoV2dvbk4uLCw0ZMoT8/f3Jy8uLvLy8SK/Xk1KppJaWFvrss89o06ZNdOTIEfrxxx8Z5KzVan+xsbFZFRIS8sn27dvPcu7Eu/Zo3S9kqbZlZ2dLR4wYEe3r6/ulpWiRSqWws7NDQEAAqqqq8Pnnn6O1tZUXI9TZ2cm0mebmZjQ3N+PGjRtobW1FR0cHuru7eed/8803WLx4MYYNGwY7OzseRCIQCODs7Hw2OTl5hkajUSxYsEBI9CvU/79fVgBHI0eOpM8++4yIiFauXOnz2muvPd7Q0DDuxo0bspsfdBbc6lyJioqi4OBg0ul05OzsTBqNhuRyOQmFQjKbzdTQ0ECXL1+mmpoaOn36NB07dsyqwqNQKOwhog6RSNSm1+v/4eXl9fz27dv3EhHl5ubSli1b6D+KLPPBAIhiYmKy/f39X1UoFFsVCkW1QqFovJsgMKVSCalUekmlUh21s7P7XKfTPZ+QkJDPfe/lfiDB/fAQJSUl9PHHH1tOhiQ5Odn76tWrQ1pbW51bW1sdtVptaGdn55DGxkZZW1ubXC6XO+CmAO/s7Gw1m82t9vb2Zjs7uxaRSFTX0NBwVKPRNLa3t19MTExsWLdu3UWBQMDCH+bOnUuvvvoq/U63UG8JFwBo8eLFkoKCArmfn59CqVQq3d3dVW5ubip3d3eVvb290sbGRhEeHi6fMGGC7I9//GOvhVL/bQyo3+l3+p1+p9/pP4L+P8YI+Lh+azEYAAAAAElFTkSuQmCC') no-repeat 0px 0px;
+      }
+      #logo_openid {
         width: 200px;
         height: 80px;
+        display: inline-block;
+        margin-left: 100px;
+        margin-bottom: 28px;
         background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAABQCAYAAABcbTqwAAANbUlEQVR42uxdeXBV1Rm/URYpuNDRFjtuZcaOZTpd7D+O07pi65QqFMQEE7KzhF2kQEUr1gUkIBQiIKDUBYRgUBCCIHEBBLKR5GUlKwlZSAKJtvUPZzr8+r73u80dfHnv3iyveeF9v5lvQsi53zvv3O93z/mWc66hUCgUCoVCoVAoFAqFQqFQKBQKhUKhUCgUCoVCoVAoFAoFQaClAtgQDhSmw1AoFBbwxRZgzgggwgBcShCFwgM0FAGrHgaiDEqcWwoPKEEUChxIBmZ+H5gkxBBRgigUBqozgeX3AU8YQLRb4kWUIAqFgT1LgSlDSI54ESWIQmH5G7EGMFkIoQRRKC4B6l3A1EFArBJEoeiEIIXA1MFKEIVCCaJQKEEUCiWIQqEEUSiUIApFcOdBlCAKhS9cPOtyRJCLWs2rCAVcuHBhakNDA2pqalBWVYOqI3twccogIM4XQfizdv8mlFXXea6T60WPoVD0d5w7dw6VlZUoLCxEbm4ucnJykJWV5ZHMUwUoPrTTEUHKd69DZn5Rx7XZ2dkefS6Xy6O/qalJZxhF/0BjYyNKSko8BmwatBBDRP6vQ7LzC1HySaojglR8kIJsF3Wa0qFTyCKfIf9XVFSEs2fPdoksLS0tOe5ZSQmmCBza2tpGnz59mqTgE96LEJReIoi3eJGlrKwM58+fX274gcw+J0+elKVbjwmCtjogOxVIXw58uJSy9wXgxLtAQ7ESMBQhBiizhRimLSm8CcIlVncIYi8dfZL+WUSxZrm8vLyOGe7MmTPoNjFKPwXWjgeShrJk/09uecSUsQa3DCcOBJbfD2TvuuyJguNvA4nXAnNvAM7XAuVHgZnXUmZcDSQN85YZbll4G/Dyb4B3ZgJZO/v/OMkT2jRCRwbLJZHlgwhBSg/vckSQyg9fcxOkVD7La4ZyShTpryynSktLuezj9T0iCLbNYQRuglmyv2gksH4i8O4sYPtcYHMUsPSXQLzZJtItKROA9st3SYcvNgHhBpBwBdBaDc8DJNqgJIQBiVd6S3wYEGWO0TiDD5qFtwOHU/rfONXV1dHwnBHDy0eQ5Uxzc3Ob6Pq2OtdRmPfb3D34n78gxlxcXIxTp06RLM774UXonhAEKx8CHvfMENwNWbAPPttWnQRem0AjeMwtz7lJc6EWlyVBjmwRA+c2htYaoOwz3t/EAcCpD4DGEqC+0JKGYuBMLpC3Fzi4Clg3Hpj+PWCiQaKt/gPwTdtooz/gO09ff9LRTpY4sqTpUaKw6ONOr5folfTJ/Dyny68eEwSbnhBycNZIewaOr/v8dSBhEG/+S3eFDkFiDGDKQOBcORzpaCoDNkbIA4XjvOqh4B+r/Px8J0/rjjZVVVX4f5aaVFdXW58fQILgy7doAJPckrqg6zPP0TeA6DA+HdMWI5QIIve7a9uxnwciTZJ88GxwjlV7e/tIy/Dso0jl5eXoy1osiaZJXwJGkIU/5g17+e7uO/ZbJlPHtKuAxlIoQXwD68bJw4RO/oW64Bsrifb4NziSR9qJj9DXxYrinwSKIO6QLW9+TFiP6sOEFJg2hCR535pFRCfKj+DSZdkm4PVIYMUDQPKDwJo/AmlLgLo8Z31uqQIOrABSJgLJD4gervPTV3icaK/2/2pNhSsdKP7E6ldzBbB7CbB6DHWsHA1sjoX0LeAEqcuTKCBJcvDV4CKIZKrF+O3IIUbZ19W8sqSziW71nCDrHzOd7J/bt7fXRV/kmVEgaUr4XZ++Ax1kWfgTCRuTSDEGxynCjPQkDga2z/PbDzEozBgOjDfY7xiK+3Opd9YNwOcbqcMiKX2rGdeAUamNwJSh/Mwosw+RnutptItHARXHEBiCEG5SMsK19pEgIQgNTozH1rjECe/Lcvf6+npPX06cOCH96bLIdeK/OOrvkyNoaDvm95wgR9+kIU0ZALRUAs0V/K5L7wRy0oCYK2ioshzL2QWcLZCID/8m5DIjYlgzBp3qf3+RkIlGvPxe4NhWoDYPkALRY//gbBRu+lJ7n4dFkHR+7pzrgf3L2GbezUyAVp4Amko9swu2z5FQLsn75I1A21kEhCD8LvyuC24KHoLYPY3l71Jj1Zf7QVpbW1PFuCV0LLNAN0TIwUSiDXAmlwYRZfRKIsttrEIO6svdLWFfhkLn/wiYeyOQdL3fQ7vx2UYglgYqy59L/pa9E4g0jf8932TGjqekDclYkmHNXHEG+xLnlmX3AO318JEUZJuJdKIDRBAubSOpUx4mwZIE9OuQy89Q2jAF137e6IQrgZos9IrOWdfRwD9ZI0bIsYg2TNKkwcGTldcnXQ20WFFD97KNT9x1Y+11bHicy5fl99DAiw7JmLMP824EvmqAjRPN61+5D+bM2PsEcaVTR/wVXM71NYQAdkur2traUCIIw7O88b1WW4Uld9A/2PeSZNdlLLgsWvmgI/34pn2ke8YhGT56ASaRadwJgxwZJOqLgISBQFwYPMGD019wzCeZOm2AjBSSdMEt/HxZvvUyQYQUMnuLHvl+Rp+Ba3pb30Oy2KG25RZHNsMiSBF6ReezPzMJ8vKlBMlYB8c63p5Gh/nV39NAd86nQ736Yec6XrmXs4As2yTrH+t8pkTeXvosScMCR5DK4+xPTF+/CoN5BL/LKzPXEXoEyU0TQ6DfUJffK/1xO8Jcv3+cLASh7jjDY1RdcPZJqgU300DXjCHp9ix1ruO9uSys3D5PwsxiiCwg/Lf9xjSpckC0tL86UAShXxRrcJYr+7RvbUEcb39LLJlduH8ixAhSeVzWwDSGvL097Q9zC9MG05hObJMkmDjGlHrHMxQNNIYGKpEkLP2VLLHo7C8Z5ZY7/Iv4K3NvYLQqZRxQ6NYXS30OjTfgBJEqaEymDi5v+7akxDZ6JdGjECMIE2izruV6+6MXe04Q3nRWstaeEie7OwTh+jzezMrX5kqY2NTrUOIMVtTGDgY2hgcnQQ6s4LjPHg7xu4KdIAyLhhRBCKy4nw5x8gM9J8hbU4GJVmxf8gsWQQq7OYPUsVI4QnIbfwOaykSXvTQU8edXjTLW1BdMBEkZz3F/8S4EQ/Zcl1g+4PYVaHwJA+39EPvoEwmyNQFmZbMYEY0t/6Ou+iBWFGnV7+i071/Wnf4xURgdPATBV03ArOGcQdKeDv4ciPytoqIiNAkifsJMLrOk5L27eiR8igiWfUhY9ZKxCKeD3fUoFqNW2DabUay1j6JLIeyUOHgSfyWHg2oGcZOCYxUvUbXsvrcDOfQgMzPTX5JQChNDkCCEO2NMI54cBmTt6PoNr84Cpg/j7LF+ArzGIpL1Wd3Og2Sn0kCnDwXO1zjT8+dbgDEGNyxVHg8WgjBBmHgVx2pjuHVtf0gUCpFCjyAEnv81b1rSdUC+44gWy1WeupUz0LwRUk3rTZAYJumk9sn+6bqYumZ8J5O+8Fb2b/Nkex2SxY8wdXzd3MYtsgEiSKPjuj2W0iSZs/X8mzhWQbRz0K7UhMnC0CQIK28X3MYnd8IAIO0vQFs9/N/wDfIGX5NYw4HSDHiNxZRBMh40iOgwSF2T330X8QNpQCw27IBU6CLCrLpNXehbR8E+YOowJgl3LQIN/uOAEUQeEH511eQAh1YDL97Nvj8m5LiN5SXBdgKigzJ3VvKGHkGsPMay34qB8kbO+yGwNRE4vI57rwv2S2aahzcsvl0Mlu2eHgVUfolOxyJxgBgafYHZ11P32rHAyW1ShetVzctl2kR02r83Yq0981K5e/wdqyJYiiPfjJPEG8mx8iHvat6koTBs4d0eR7cCk5hQJUE+tUr1F41k3mXJTztEfpcSf8y8RkLe/E7jDV7z90eB5nIYwQhxxB2UuzOrHsKnu8vJG1h0O594Yz03lwbyhEmIR82S8zk/AHYtBv7JAyt8EIRyvpoHGTx3p+ig0Mioa5wY4BDODn4g+RruB/GhIzYM2BzNQxGsGcEqHbGFd3scE4KYM4Z1aIMI20V1KrzP04aQNG9NB0qYMQ9qFBQUONowJZGvUH/9AfL3AbufATZEcMfdygf59N8xnzPC1+dgMxad5kGQlcqzopJHczfg+nAJ4TquB0NrFXBoDfB6FGeSZFPHh38FqjNtdhR6w6492us5exYd5O/uchUPiVxuKfQhBelA+TEuU/sX6LA7cdolfyL71/X9IDZwRhA9ibE/nchuOuaODoaTDUhKECVISJKEyy37Pd4S4ZLdekoQJUjogJW+QgBHx4ya7STrzuLGHr1hKl0JougHsE4PEXFCFLblsUDizEuCseMFOP+pzfdPkBgzGuQKAYLU5UskjFKXrwTp7687kHN2vY4htSeL9b4QVzHKMt73fXh1FAki8XzJ8BqXOSSCIxW+Iu5/K0H6P3g2rnl2VteP/Mxzdfb6A6vU4tlfAK59aiiK/g85rV2y6vQ9rFmlKy/QEeE5UVdJQk2Jobg8IdErc0+J17s8fBIkyiTHK/fLoQFKDkUogCekiGMuuxStdxaSNJmyxDq4AxeFHLNHABmvKTEUuhST87Qk9FtUVoHTGWm4uCVWjt1UcigUCoVCoVAoFAqFQqFQKBQKhUKhUCgUCsV/24MDEgAAAABB/1+3I1ABAGAilVZ2IKvvzEMAAAAASUVORK5CYII=') no-repeat 0px 0px;
       }
       #f_openid {
@@ -36,7 +46,7 @@
       <h1>Sign In to Gerrit Code Review at <span id="hostName">example.com</span></h1>
       <form method="POST" action="#" id="login_form">
         <input type="hidden" name="link" id="f_link" value="1" />
-        <div id="logo_box"><div id="logo_img"></div></div>
+        <div id="logo_box"><div id="logo_oauth"></div><div id="logo_openid"></div></div>
         <div id="error_message">Invalid OpenID identifier.</div>
         <div>
           <input type="text"
@@ -57,10 +67,14 @@
           <a href="../" id="cancel_link">Cancel</a>
         </div>
 
-        <div id="provider_google">
-          <img height="16" width="16" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABsUlEQVR42rWSy0sbYRRH88fYhW5KQXThIkUQN6ISJFBBW6FFBN2UqlC78LVKFlHIoqWGakF8gUZ8gajVzKSKZpQmAWtK0pBmhkxmSgPuiuaYmRKhSEwU/ODyDZfzO8ydOxbLQx1NB0H4y8FBlmMpy9LyOavrKUoKB4IXvBsIIopRPJ441dY/2Fr8fAtFSxO0dkRy4eA1PDp+TkVlmGhMKS5IJOFpbYgj6fQalhV4UhVm0esvLojELimv1JiYOPwPflS2gM93WNoIXZ1nPC4/IRDQzYDTqTA0PM2dtuCZ8vGmV6SvP5srL/da5c+4TnNLFlvTMYmEVrpkazuM2x1naOQ7Dc1fqLOmaayXkaSz4hLHewm7fY+Z2R0iP+JmYGpym/q6DDb76u0CwR+g9+0FHz4u3wAdLh9VNWu5UX4VlqysfaXRDoODNz+aw7nDs9ZPpNPpwgJV1WhvF6m1/qb79T6fpwXm5kVGRnd5/mIdQSzwH2iahq7/27nxiq5xLy9fbdDWsUtPzyausRVC4VMMzmAMNv9syTdkWTZLVVUymYx5J5NJs5dKpVAUxaw8Z/SM7BV+fz9Uc4yYRAAAAABJRU5ErkJggg==" />
-          <a href="?id=https://www.google.com/accounts/o8/id" id="id_google">Sign in with a Google Account</a>
+        <div id="providers">
         </div>
+
+        <div id="provider_launchpad">
+          <img height="16" width="16" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAHKSURBVCiRjZI9SFtRGIafc5tiVFCkhUxKBAcLYoPWwE0GEUPBwc0f1MHFoWLBiKABobWtBh2kacFYwUUQRXBoB3EJCVUvyBVrIB1UskTawUGFQOoP5h6H3ki8deg7vuc853zvxyuklORLDUc9QH9nZeAL0AGM+1v0cywSOVANR8uBaaALOOysDLwCYsAp8AaY97fo2RyoqOFooRqOvgUOTMiqJ8AsEA9tuH13IFkcQD1QZHpbQE+bPXEADAK5MSsA1/p+1SMAoTbF9oAh2WYUAcWbz5tThiQE1EsI7VxVzMWvHe2lGWP1RaqgWyKe1g5vjyhAHfBdrCmuWG1zqSHRAS9gFxBQC451/+PdhbqUPSYR00JSAmDLy+IE7ICwZHRkbBQKqMo3lQeW8V/KB5PAMSAtd06Kb7iQf8/vgXuA9/2V9nN7dCKjCNyABlxKmPqx7HQvDjX0HX29bBLIUSlIA4gNT9Bpw/gMtJqPaYD/2evJX5FgTQcwDpQBaeCD8+XZx8aZZFaxYZyYv/4xQS+wFAnWVAOfTAgzRrxxJpkFUHza2IVPG3sHVAMrD+zhFBgAXL0JPZIzhbXkEe+kB+j/nf52V/LexL8lvwVTCpkwGXEEfAAAAABJRU5ErkJggg=="/>
+          <a href="?id=https://login.launchpad.net/%2Bopenid" id="id_launchpad">Sign in with a Launchpad ID</a>
+        </div>
+
         <div id="provider_yahoo">
           <img height="16" width="16" src="data:image/gif;base64,R0lGODlhEAAQAPECAAAAAP8AAP///8zMzCH5BAEAAAMALAAAAAAQABAAAAIqnI+py30BY3AgAjCkfJDjiIAQlgUkNxqWkqrm0honKk7KhZOzw/f+fygAADs=" />
           <a href="?id=https://me.yahoo.com" id="id_yahoo">Sign in with a Yahoo! ID</a>
diff --git a/gerrit-patch-jgit/BUCK b/gerrit-patch-jgit/BUCK
index 00b8228..e621722 100644
--- a/gerrit-patch-jgit/BUCK
+++ b/gerrit-patch-jgit/BUCK
@@ -6,8 +6,8 @@
     SRC + 'diff/Edit_JsonSerializer.java',
     SRC + 'diff/ReplaceEdit.java',
   ],
-  gwtxml = SRC + 'JGit.gwt.xml',
-  compile_deps = [
+  gwt_xml = SRC + 'JGit.gwt.xml',
+  deps = [
     '//lib:gwtjsonrpc',
     '//lib/gwt:user',
     '//lib/jgit:jgit',
diff --git a/gerrit-pgm/BUCK b/gerrit-pgm/BUCK
index b7162ed..8b5fdc1 100644
--- a/gerrit-pgm/BUCK
+++ b/gerrit-pgm/BUCK
@@ -4,7 +4,6 @@
   'init/AllProjectsConfig.java',
   'init/AllProjectsNameOnInitProvider.java',
   'util/ConsoleUI.java',
-  'util/Die.java',
   'init/InitFlags.java',
   'init/InitStep.java',
   'init/InitStep.java',
@@ -51,7 +50,7 @@
 
 INIT_BASE_RSRCS = ['src/main/resources/com/google/gerrit/pgm/libraries.config']
 
-java_library2(
+java_library(
   name = 'init-base',
   srcs = INIT_BASE_SRCS,
   resources = INIT_BASE_RSRCS,
@@ -74,14 +73,14 @@
     '//lib:gwtorm',
     '//lib/log:api',
   ],
-  compile_deps = ['//gerrit-launcher:launcher'],
+  provided_deps = ['//gerrit-launcher:launcher'],
   visibility = [
     '//gerrit-war:',
     '//gerrit-acceptance-tests/...',
   ],
 )
 
-java_library2(
+java_library(
   name = 'pgm',
   srcs = glob(
     ['src/main/java/**/*.java'],
@@ -100,6 +99,7 @@
     '//gerrit-gwtexpui:server',
     '//gerrit-httpd:httpd',
     '//gerrit-lucene:lucene',
+    '//gerrit-oauth:oauth',
     '//gerrit-openid:openid',
     '//gerrit-reviewdb:server',
     '//gerrit-server:server',
@@ -113,16 +113,18 @@
     '//lib:h2',
     '//lib:servlet-api-3_1',
     '//lib/guice:guice',
+    '//lib/guice:guice-assistedinject',
     '//lib/guice:guice-servlet',
     '//lib/jetty:server',
     '//lib/jetty:servlet',
+    '//lib/jetty:jmx',
     '//lib/jgit:jgit',
     '//lib/log:api',
     '//lib/log:log4j',
     '//lib/lucene:core',
     '//lib/prolog:prolog-cafe',
   ],
-  compile_deps = ['//gerrit-launcher:launcher'],
+  provided_deps = ['//gerrit-launcher:launcher'],
   visibility = [
     '//:',
     '//gerrit-acceptance-tests/...',
@@ -140,7 +142,7 @@
     ':pgm',
     '//gerrit-server:server',
     '//lib:junit',
-    '//lib:easymock',
+    '//lib/easymock:easymock',
     '//lib/guice:guice',
     '//lib/jgit:jgit',
     '//lib/jgit:junit',
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/BaseInit.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/BaseInit.java
index 5a8cb4f..9638db6 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/BaseInit.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/BaseInit.java
@@ -19,13 +19,13 @@
 
 import com.google.common.base.Objects;
 import com.google.common.collect.Lists;
+import com.google.gerrit.common.Die;
 import com.google.gerrit.pgm.init.InitFlags;
 import com.google.gerrit.pgm.init.InitModule;
 import com.google.gerrit.pgm.init.InstallPlugins;
 import com.google.gerrit.pgm.init.PluginsDistribution;
 import com.google.gerrit.pgm.init.SitePathInitializer;
 import com.google.gerrit.pgm.util.ConsoleUI;
-import com.google.gerrit.pgm.util.Die;
 import com.google.gerrit.pgm.util.SiteProgram;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.config.SitePath;
@@ -187,7 +187,7 @@
   private SiteInit createSiteInit() {
     final ConsoleUI ui = getConsoleUI();
     final File sitePath = getSitePath();
-    final List<Module> m = new ArrayList<Module>();
+    final List<Module> m = new ArrayList<>();
 
     m.add(new InitModule(standalone, initDb));
     m.add(new AbstractModule() {
@@ -252,7 +252,7 @@
     }
 
     void upgradeSchema() throws OrmException {
-      final List<String> pruneList = new ArrayList<String>();
+      final List<String> pruneList = new ArrayList<>();
       schemaUpdater.update(new UpdateUI() {
         @Override
         public void message(String msg) {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
index e446fdb..1c667e3 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
@@ -20,13 +20,14 @@
 import com.google.common.base.Objects;
 import com.google.gerrit.common.ChangeHookRunner;
 import com.google.gerrit.httpd.AllRequestFilter;
-import com.google.gerrit.httpd.CacheBasedWebSession;
 import com.google.gerrit.httpd.GerritUiOptions;
 import com.google.gerrit.httpd.GitOverHttpModule;
+import com.google.gerrit.httpd.H2CacheBasedWebSession;
 import com.google.gerrit.httpd.HttpCanonicalWebUrlProvider;
 import com.google.gerrit.httpd.RequestContextFilter;
 import com.google.gerrit.httpd.WebModule;
 import com.google.gerrit.httpd.WebSshGlueModule;
+import com.google.gerrit.httpd.auth.oauth.OAuthModule;
 import com.google.gerrit.httpd.auth.openid.OpenIdModule;
 import com.google.gerrit.httpd.plugins.HttpPluginModule;
 import com.google.gerrit.lifecycle.LifecycleManager;
@@ -52,7 +53,9 @@
 import com.google.gerrit.server.config.GerritGlobalModule;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.MasterNodeStartup;
+import com.google.gerrit.server.config.RestCacheAdminModule;
 import com.google.gerrit.server.contact.HttpContactStoreConnection;
+import com.google.gerrit.server.git.GarbageCollectionRunner;
 import com.google.gerrit.server.git.ReceiveCommitsExecutorModule;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.index.DummyIndexModule;
@@ -60,7 +63,7 @@
 import com.google.gerrit.server.index.IndexModule.IndexType;
 import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
 import com.google.gerrit.server.mail.SmtpEmailSender;
-import com.google.gerrit.server.patch.IntraLineWorkerPool;
+import com.google.gerrit.server.patch.DiffExecutorModule;
 import com.google.gerrit.server.plugins.PluginGuiceEnvironment;
 import com.google.gerrit.server.plugins.PluginRestApiModule;
 import com.google.gerrit.server.schema.DataSourceProvider;
@@ -304,26 +307,27 @@
   }
 
   private Injector createCfgInjector() {
-    final List<Module> modules = new ArrayList<Module>();
+    final List<Module> modules = new ArrayList<>();
     modules.add(new AuthConfigModule());
     return dbInjector.createChildInjector(modules);
   }
 
   private Injector createSysInjector() {
-    final List<Module> modules = new ArrayList<Module>();
+    final List<Module> modules = new ArrayList<>();
     modules.add(SchemaVersionCheck.module());
     modules.add(new LogFileCompressor.Module());
     modules.add(new WorkQueue.Module());
     modules.add(new ChangeHookRunner.Module());
     modules.add(new ReceiveCommitsExecutorModule());
     modules.add(new MergeabilityChecksExecutorModule());
-    modules.add(new IntraLineWorkerPool.Module());
+    modules.add(new DiffExecutorModule());
     modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
     modules.add(new InternalAccountDirectory.Module());
     modules.add(new DefaultCacheFactory.Module());
     modules.add(new SmtpEmailSender.Module());
     modules.add(new SignedTokenEmailTokenVerifier.Module());
     modules.add(new PluginRestApiModule());
+    modules.add(new RestCacheAdminModule());
     modules.add(createIndexModule());
     if (Objects.firstNonNull(httpd, true)) {
       modules.add(new CanonicalWebUrlModule() {
@@ -354,6 +358,7 @@
         bind(GerritUiOptions.class).toInstance(new GerritUiOptions(headless));
       }
     });
+    modules.add(GarbageCollectionRunner.module());
     return cfgInjector.createChildInjector(modules);
   }
 
@@ -380,7 +385,7 @@
   }
 
   private Injector createSshInjector() {
-    final List<Module> modules = new ArrayList<Module>();
+    final List<Module> modules = new ArrayList<>();
     modules.add(sysInjector.getInstance(SshModule.class));
     if (!test) {
       modules.add(new SshHostKeyModule());
@@ -405,13 +410,13 @@
   }
 
   private Injector createWebInjector() {
-    final List<Module> modules = new ArrayList<Module>();
+    final List<Module> modules = new ArrayList<>();
     if (sshd) {
       modules.add(new ProjectQoSFilter.Module());
     }
     modules.add(RequestContextFilter.module());
     modules.add(AllRequestFilter.module());
-    modules.add(CacheBasedWebSession.module());
+    modules.add(H2CacheBasedWebSession.module());
     modules.add(HttpContactStoreConnection.module());
     modules.add(sysInjector.getInstance(GitOverHttpModule.class));
     modules.add(sysInjector.getInstance(WebModule.class));
@@ -426,6 +431,8 @@
     if (authConfig.getAuthType() == AuthType.OPENID ||
         authConfig.getAuthType() == AuthType.OPENID_SSO) {
       modules.add(new OpenIdModule());
+    } else if (authConfig.getAuthType() == AuthType.OAUTH) {
+      modules.add(new OAuthModule());
     }
     modules.add(sysInjector.getInstance(GetUserFilter.Module.class));
 
@@ -433,7 +440,7 @@
   }
 
   private Injector createHttpdInjector() {
-    final List<Module> modules = new ArrayList<Module>();
+    final List<Module> modules = new ArrayList<>();
     modules.add(new JettyModule(new JettyEnv(webInjector)));
     return webInjector.createChildInjector(modules);
   }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
index fa77732..c30507f 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
@@ -18,9 +18,9 @@
 import com.google.common.base.Joiner;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.common.PluginData;
 import com.google.gerrit.pgm.init.Browser;
 import com.google.gerrit.pgm.init.InitPlugins;
-import com.google.gerrit.pgm.init.InitPlugins.PluginData;
 import com.google.gerrit.pgm.util.ConsoleUI;
 import com.google.gerrit.pgm.util.ErrorLogFile;
 import com.google.gerrit.pgm.util.IoUtil;
@@ -36,8 +36,8 @@
 
 import java.io.File;
 import java.io.IOException;
-import java.util.List;
 import java.util.ArrayList;
+import java.util.List;
 
 /** Initialize a new Gerrit installation. */
 public class Init extends BaseInit {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java
index 323d7f28..d208a3c 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java
@@ -70,7 +70,7 @@
       db.close();
     }
 
-    final List<Worker> workers = new ArrayList<Worker>(threads);
+    final List<Worker> workers = new ArrayList<>(threads);
     for (int tid = 0; tid < threads; tid++) {
       Worker t = new Worker();
       t.start();
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/PrologShell.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/PrologShell.java
index 4c66f0b..fa434a6 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/PrologShell.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/PrologShell.java
@@ -33,7 +33,7 @@
 
 public class PrologShell extends AbstractProgram {
   @Option(name = "-s", metaVar = "FILE.pl", usage = "file to load")
-  private List<String> fileName = new ArrayList<String>();
+  private List<String> fileName = new ArrayList<>();
 
   @Override
   public int run() {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java
index 7f5186e..161efc1 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java
@@ -17,18 +17,21 @@
 import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
 import static com.google.inject.Scopes.SINGLETON;
 
+import com.google.common.cache.Cache;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.Die;
 import com.google.gerrit.common.DisabledChangeHooks;
 import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
 import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.lucene.LuceneIndexModule;
-import com.google.gerrit.pgm.util.Die;
 import com.google.gerrit.pgm.util.SiteProgram;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -42,7 +45,7 @@
 import com.google.gerrit.server.account.GroupIncludeCacheImpl;
 import com.google.gerrit.server.cache.CacheRemovalListener;
 import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
-import com.google.gerrit.server.change.ChangeKindCache;
+import com.google.gerrit.server.change.ChangeKindCacheImpl;
 import com.google.gerrit.server.change.MergeabilityChecker;
 import com.google.gerrit.server.change.MergeabilityChecksExecutor;
 import com.google.gerrit.server.change.MergeabilityChecksExecutor.Priority;
@@ -51,6 +54,8 @@
 import com.google.gerrit.server.config.CanonicalWebUrlProvider;
 import com.google.gerrit.server.config.FactoryModule;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.GitReceivePackGroups;
+import com.google.gerrit.server.config.GitUploadPackGroups;
 import com.google.gerrit.server.git.GitModule;
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.git.WorkQueue;
@@ -65,11 +70,13 @@
 import com.google.gerrit.server.index.IndexModule.IndexType;
 import com.google.gerrit.server.mail.ReplacePatchSetSender;
 import com.google.gerrit.server.notedb.NoteDbModule;
+import com.google.gerrit.server.patch.DiffExecutorModule;
 import com.google.gerrit.server.patch.PatchListCacheImpl;
-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.ProjectCacheImpl;
+import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.SectionSortCache;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -185,6 +192,7 @@
 
   private Injector createSysInjector() {
     List<Module> modules = Lists.newArrayList();
+    modules.add(new DiffExecutorModule());
     modules.add(PatchListCacheImpl.module());
     AbstractModule changeIndexModule;
     switch (IndexModule.getIndexType(dbInjector)) {
@@ -207,6 +215,8 @@
         // once, so don't worry about cache removal.
         bind(new TypeLiteral<DynamicSet<CacheRemovalListener>>() {})
             .toInstance(DynamicSet.<CacheRemovalListener> emptySet());
+        bind(new TypeLiteral<DynamicMap<Cache<?, ?>>>() {})
+            .toInstance(DynamicMap.<Cache<?, ?>> emptyMap());
         bind(new TypeLiteral<List<CommentLinkInfo>>() {})
             .toProvider(CommentLinkProvider.class).in(SINGLETON);
         bind(String.class).annotatedWith(CanonicalWebUrl.class)
@@ -214,7 +224,16 @@
         bind(IdentifiedUser.class)
           .toProvider(Providers. <IdentifiedUser>of(null));
         bind(CurrentUser.class).to(IdentifiedUser.class);
-        install(new AccessControlModule());
+
+        bind(new TypeLiteral<Set<AccountGroup.UUID>>() {})
+            .annotatedWith(GitUploadPackGroups.class)
+            .toInstance(Collections.<AccountGroup.UUID> emptySet());
+        bind(new TypeLiteral<Set<AccountGroup.UUID>>() {})
+            .annotatedWith(GitReceivePackGroups.class)
+            .toInstance(Collections.<AccountGroup.UUID> emptySet());
+        factory(ChangeControl.AssistedFactory.class);
+        factory(ProjectControl.AssistedFactory.class);
+
         install(new DefaultCacheFactory.Module());
         install(new GroupModule());
         install(new PrologModule());
@@ -224,6 +243,7 @@
         install(GroupIncludeCacheImpl.module());
         install(ProjectCacheImpl.module());
         install(SectionSortCache.module());
+        install(ChangeKindCacheImpl.module());
         factory(CapabilityControl.Factory.class);
         factory(ChangeData.Factory.class);
         factory(ProjectState.Factory.class);
@@ -256,7 +276,7 @@
           Key.get(new TypeLiteral<SchemaFactory<ReviewDb>>() {}));
       final List<ReviewDb> dbs = Collections.synchronizedList(
           Lists.<ReviewDb> newArrayListWithCapacity(threads + 1));
-      final ThreadLocal<ReviewDb> localDb = new ThreadLocal<ReviewDb>();
+      final ThreadLocal<ReviewDb> localDb = new ThreadLocal<>();
 
       bind(ReviewDb.class).toProvider(new Provider<ReviewDb>() {
         @Override
@@ -302,9 +322,6 @@
       DynamicSet.setOf(binder(), GitReferenceUpdatedListener.class);
       DynamicSet.setOf(binder(), CommitValidationListener.class);
       factory(CommitValidators.Factory.class);
-
-      install(ChangeKindCache.module());
-
       install(new GitModule());
       install(new NoteDbModule());
     }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Rulec.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Rulec.java
index cabdc64..17a54d4 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Rulec.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Rulec.java
@@ -47,7 +47,7 @@
   private boolean quiet;
 
   @Argument(index = 0, multiValued = true, metaVar = "PROJECT", usage = "project to compile rules for")
-  private List<String> projectNames = new ArrayList<String>();
+  private List<String> projectNames = new ArrayList<>();
 
   private Injector dbInjector;
 
@@ -71,7 +71,7 @@
       }
     }).injectMembers(this);
 
-    LinkedHashSet<Project.NameKey> names = new LinkedHashSet<Project.NameKey>();
+    LinkedHashSet<Project.NameKey> names = new LinkedHashSet<>();
     for (String name : projectNames) {
       names.add(new Project.NameKey(name));
     }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HttpLog.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HttpLog.java
index 03b5a55..ba97d4a 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HttpLog.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HttpLog.java
@@ -16,31 +16,28 @@
 
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.util.LogUtil;
+import com.google.gerrit.server.util.SystemLog;
 import com.google.gerrit.server.util.TimeUtil;
+import com.google.inject.Inject;
 
-import org.apache.log4j.Appender;
 import org.apache.log4j.AsyncAppender;
-import org.apache.log4j.DailyRollingFileAppender;
 import org.apache.log4j.Level;
 import org.apache.log4j.Logger;
-import org.apache.log4j.spi.ErrorHandler;
 import org.apache.log4j.spi.LoggingEvent;
 import org.eclipse.jetty.server.Request;
 import org.eclipse.jetty.server.RequestLog;
 import org.eclipse.jetty.server.Response;
 import org.eclipse.jetty.util.component.AbstractLifeCycle;
-import org.eclipse.jgit.lib.Config;
-
-import java.io.File;
-import java.io.IOException;
 
 /** Writes the {@code httpd_log} file with per-request data. */
 class HttpLog extends AbstractLifeCycle implements RequestLog {
   private static final Logger log = Logger.getLogger(HttpLog.class);
   private static final String LOG_NAME = "httpd_log";
 
+  interface HttpLogFactory {
+    HttpLog get();
+  }
+
   protected static final String P_HOST = "Host";
   protected static final String P_USER = "User";
   protected static final String P_METHOD = "Method";
@@ -53,35 +50,9 @@
 
   private final AsyncAppender async;
 
-  HttpLog(final SitePaths site, final Config config) {
-    async = new AsyncAppender();
-    async.setBlocking(true);
-    async.setBufferSize(config.getInt("core", "asyncLoggingBufferSize", 64));
-    async.setLocationInfo(false);
-    if (LogUtil.shouldConfigureLogSystem()) {
-      final DailyRollingFileAppender dst = new DailyRollingFileAppender();
-      dst.setName(LOG_NAME);
-      dst.setLayout(new HttpLogLayout());
-      dst.setEncoding("UTF-8");
-      dst.setFile(new File(resolve(site.logs_dir), LOG_NAME).getPath());
-      dst.setImmediateFlush(true);
-      dst.setAppend(true);
-      dst.setThreshold(Level.INFO);
-      dst.setErrorHandler(new DieErrorHandler());
-      dst.activateOptions();
-      dst.setErrorHandler(new LogLogHandler());
-      async.addAppender(dst);
-    } else {
-      Appender appender = log.getAppender(LOG_NAME);
-      if (appender != null) {
-        async.addAppender(appender);
-      } else {
-        log.warn("No appender with the name: "
-            + LOG_NAME
-            + " was found. HTTPD logging is disabled");
-      }
-    }
-    async.activateOptions();
+  @Inject
+  HttpLog(final SystemLog systemLog) {
+    async = systemLog.createAsyncAppender(LOG_NAME, new HttpLogLayout());
   }
 
   @Override
@@ -151,80 +122,4 @@
       event.setProperty(key, String.valueOf(val));
     }
   }
-
-  private static File resolve(final File logs_dir) {
-    try {
-      return logs_dir.getCanonicalFile();
-    } catch (IOException e) {
-      return logs_dir.getAbsoluteFile();
-    }
-  }
-
-  private static final class DieErrorHandler implements ErrorHandler {
-    @Override
-    public void error(String message, Exception e, int errorCode,
-        LoggingEvent event) {
-      error(e != null ? e.getMessage() : message);
-    }
-
-    @Override
-    public void error(String message, Exception e, int errorCode) {
-      error(e != null ? e.getMessage() : message);
-    }
-
-    @Override
-    public void error(String message) {
-      throw new RuntimeException("Cannot open log file: " + message);
-    }
-
-    @Override
-    public void activateOptions() {
-    }
-
-    @Override
-    public void setAppender(Appender appender) {
-    }
-
-    @Override
-    public void setBackupAppender(Appender appender) {
-    }
-
-    @Override
-    public void setLogger(Logger logger) {
-    }
-  }
-
-  private static final class LogLogHandler implements ErrorHandler {
-    @Override
-    public void error(String message, Exception e, int errorCode,
-        LoggingEvent event) {
-      log.error(message, e);
-    }
-
-    @Override
-    public void error(String message, Exception e, int errorCode) {
-      log.error(message, e);
-    }
-
-    @Override
-    public void error(String message) {
-      log.error(message);
-    }
-
-    @Override
-    public void activateOptions() {
-    }
-
-    @Override
-    public void setAppender(Appender appender) {
-    }
-
-    @Override
-    public void setBackupAppender(Appender appender) {
-    }
-
-    @Override
-    public void setLogger(Logger logger) {
-    }
-  }
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyModule.java
index b563349..d356d96 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyModule.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyModule.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.pgm.http.jetty;
 
 import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.pgm.http.jetty.HttpLog.HttpLogFactory;
+import com.google.inject.assistedinject.FactoryModuleBuilder;
 
 public class JettyModule extends LifecycleModule {
   private final JettyEnv env;
@@ -28,5 +30,6 @@
     bind(JettyEnv.class).toInstance(env);
     bind(JettyServer.class);
     listener().to(JettyServer.Lifecycle.class);
+    install(new FactoryModuleBuilder().build(HttpLogFactory.class));
   }
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
index 067eeab..1d6d2bb 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
@@ -25,6 +25,7 @@
 import com.google.common.io.ByteStreams;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.launcher.GerritLauncher;
+import com.google.gerrit.pgm.http.jetty.HttpLog.HttpLogFactory;
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -39,6 +40,7 @@
 import com.google.inject.servlet.GuiceServletContextListener;
 
 import org.eclipse.jetty.http.HttpScheme;
+import org.eclipse.jetty.jmx.MBeanContainer;
 import org.eclipse.jetty.server.Connector;
 import org.eclipse.jetty.server.ForwardedRequestCustomizer;
 import org.eclipse.jetty.server.Handler;
@@ -58,6 +60,7 @@
 import org.eclipse.jetty.servlet.ServletContextHandler;
 import org.eclipse.jetty.servlet.ServletHolder;
 import org.eclipse.jetty.util.BlockingArrayQueue;
+import org.eclipse.jetty.util.log.Log;
 import org.eclipse.jetty.util.resource.Resource;
 import org.eclipse.jetty.util.ssl.SslContextFactory;
 import org.eclipse.jetty.util.thread.QueuedThreadPool;
@@ -75,6 +78,7 @@
 import java.io.InputStream;
 import java.io.InterruptedIOException;
 import java.io.PrintWriter;
+import java.lang.management.ManagementFactory;
 import java.net.MalformedURLException;
 import java.net.URI;
 import java.net.URISyntaxException;
@@ -157,7 +161,7 @@
 
   @Inject
   JettyServer(@GerritServerConfig final Config cfg, final SitePaths site,
-      final JettyEnv env)
+      final JettyEnv env, final HttpLogFactory httpLogFactory)
       throws MalformedURLException, IOException {
     this.site = site;
 
@@ -167,10 +171,17 @@
     Handler app = makeContext(env, cfg);
     if (cfg.getBoolean("httpd", "requestLog", !reverseProxy)) {
       RequestLogHandler handler = new RequestLogHandler();
-      handler.setRequestLog(new HttpLog(site, cfg));
+      handler.setRequestLog(httpLogFactory.get());
       handler.setHandler(app);
       app = handler;
     }
+    if (cfg.getBoolean("httpd", "registerMBeans", false)) {
+      MBeanContainer mbean =
+          new MBeanContainer(ManagementFactory.getPlatformMBeanServer());
+      httpd.addEventListener(mbean);
+      httpd.addBean(Log.getRootLogger());
+      httpd.addBean(mbean);
+    }
 
     httpd.setHandler(app);
     httpd.setStopAtShutdown(false);
@@ -361,7 +372,7 @@
 
   private Handler makeContext(final JettyEnv env, final Config cfg)
       throws MalformedURLException, IOException {
-    final Set<String> paths = new HashSet<String>();
+    final Set<String> paths = new HashSet<>();
     for (URI u : listenURLs(cfg)) {
       String p = u.getPath();
       if (p == null || p.isEmpty()) {
@@ -373,7 +384,7 @@
       paths.add(p);
     }
 
-    final List<ContextHandler> all = new ArrayList<ContextHandler>();
+    final List<ContextHandler> all = new ArrayList<>();
     for (String path : paths) {
       all.add(makeContext(path, env, cfg));
     }
@@ -578,7 +589,13 @@
         String pkg = "gerrit-gwtui";
         String target = "ui_" + rule.select((HttpServletRequest) request);
         String rule = "//" + pkg + ":" + target;
-        File zip = new File(new File(gen, pkg), target + ".zip");
+        // TODO(davido): instead of assuming specific Buck's internal
+        // target directory for gwt_binary() artifacts, ask Buck for
+        // the location of user agent permutation GWT zip, e. g.:
+        // $ buck targets --show_output //gerrit-gwtui:ui_safari \
+        //    | awk '{print $2}'
+        String child = String.format("%s/__gwt_binary_%s__", pkg, target);
+        File zip = new File(new File(gen, child), target + ".zip");
 
         synchronized (this) {
           try {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/AllUsersNameOnInitProvider.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/AllUsersNameOnInitProvider.java
new file mode 100644
index 0000000..4c65218a
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/AllUsersNameOnInitProvider.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.pgm.init;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Strings;
+import com.google.gerrit.server.config.AllUsersNameProvider;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class AllUsersNameOnInitProvider implements Provider<String> {
+  private final String name;
+
+  @Inject
+  AllUsersNameOnInitProvider(Section.Factory sections) {
+    String n = sections.get("gerrit", null).get("allUsers");
+    name = Objects.firstNonNull(
+        Strings.emptyToNull(n), AllUsersNameProvider.DEFAULT);
+  }
+
+  public String get() {
+    return name;
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DatabaseConfigModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DatabaseConfigModule.java
index 55419c2..607d6b4 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DatabaseConfigModule.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DatabaseConfigModule.java
@@ -39,5 +39,7 @@
         Names.named("oracle")).to(OracleInitializer.class);
     bind(DatabaseConfigInitializer.class).annotatedWith(
         Names.named("postgresql")).to(PostgreSQLInitializer.class);
+    bind(DatabaseConfigInitializer.class).annotatedWith(
+        Names.named("maxdb")).to(MaxDbInitializer.class);
   }
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java
index a2037b5..74884a4 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java
@@ -60,6 +60,7 @@
       case DEVELOPMENT_BECOME_ANY_ACCOUNT:
       case LDAP:
       case LDAP_BIND:
+      case OAUTH:
       case OPENID:
       case OPENID_SSO:
         break;
@@ -94,6 +95,7 @@
       case CUSTOM_EXTENSION:
       case DEVELOPMENT_BECOME_ANY_ACCOUNT:
       case HTTP:
+      case OAUTH:
       case OPENID:
       case OPENID_SSO:
         break;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitHttpd.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitHttpd.java
index 699daa8..8929a7b 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitHttpd.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitHttpd.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.pgm.init;
 
-import static com.google.gerrit.pgm.init.InitUtil.chmod;
+import static com.google.gerrit.common.FileUtil.chmod;
 import static com.google.gerrit.pgm.init.InitUtil.die;
 import static com.google.gerrit.pgm.init.InitUtil.domainOf;
 import static com.google.gerrit.pgm.init.InitUtil.isAnyAddress;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java
index 5584cf3..1372c31 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.pgm.util.ConsoleUI;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.plugins.JarPluginProvider;
 import com.google.gerrit.server.plugins.PluginLoader;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
@@ -54,7 +55,7 @@
 
   public Collection<InitStep> getInitSteps() {
     List<File> jars = scanJarsInPluginsDirectory();
-    ArrayList<InitStep> pluginsInitSteps = new ArrayList<InitStep>();
+    ArrayList<InitStep> pluginsInitSteps = new ArrayList<>();
 
     for (File jar : jars) {
       InitStep init = loadInitStep(jar);
@@ -95,9 +96,9 @@
     }
   }
 
-  private Injector getPluginInjector(File jarFile) throws IOException {
+  private Injector getPluginInjector(final File jarFile) throws IOException {
     final String pluginName =
-        Objects.firstNonNull(PluginLoader.getGerritPluginName(jarFile),
+        Objects.firstNonNull(JarPluginProvider.getJarPluginName(jarFile),
             PluginLoader.nameOf(jarFile));
     return initInjector.createChildInjector(new AbstractModule() {
       @Override
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java
index add5c8d..2326e48 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java
@@ -15,9 +15,10 @@
 package com.google.gerrit.pgm.init;
 
 import com.google.common.collect.Lists;
+import com.google.gerrit.common.PluginData;
 import com.google.gerrit.pgm.util.ConsoleUI;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.plugins.PluginLoader;
+import com.google.gerrit.server.plugins.JarPluginProvider;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 import com.google.inject.Singleton;
@@ -35,18 +36,6 @@
   public static final String PLUGIN_DIR = "WEB-INF/plugins/";
   public static final String JAR = ".jar";
 
-  public static class PluginData {
-    public final String name;
-    public final String version;
-    public final File pluginFile;
-
-    private PluginData(String name, String version, File pluginFile) {
-      this.name = name;
-      this.version = version;
-      this.pluginFile = pluginFile;
-    }
-  }
-
   public static List<PluginData> listPlugins(SitePaths site,
       PluginsDistribution pluginsDistribution) throws IOException {
     return listPlugins(site, false, pluginsDistribution);
@@ -64,7 +53,7 @@
     pluginsDistribution.foreach(new PluginsDistribution.Processor() {
       @Override
       public void process(String pluginName, InputStream in) throws IOException {
-        File tmpPlugin = PluginLoader.storeInTemp(pluginName, in, site);
+        File tmpPlugin = JarPluginProvider.storeInTemp(pluginName, in, site);
         String pluginVersion = getVersion(tmpPlugin);
         if (deleteTempPluginFile) {
           tmpPlugin.delete();
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSshd.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSshd.java
index 66c2dd0b..9003b30 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSshd.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSshd.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.pgm.init;
 
-import static com.google.gerrit.pgm.init.InitUtil.chmod;
+import static com.google.gerrit.common.FileUtil.chmod;
 import static com.google.gerrit.pgm.init.InitUtil.die;
 import static com.google.gerrit.pgm.init.InitUtil.hostname;
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitUtil.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitUtil.java
index dde0b06..7e06b5a 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitUtil.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitUtil.java
@@ -14,7 +14,10 @@
 
 package com.google.gerrit.pgm.init;
 
-import com.google.gerrit.pgm.util.Die;
+import static com.google.gerrit.common.FileUtil.chmod;
+import static com.google.gerrit.common.FileUtil.modified;
+
+import com.google.gerrit.common.Die;
 
 import org.eclipse.jgit.internal.storage.file.LockFile;
 import org.eclipse.jgit.lib.Constants;
@@ -33,7 +36,6 @@
 import java.net.URISyntaxException;
 import java.net.UnknownHostException;
 import java.nio.ByteBuffer;
-import java.util.Arrays;
 
 /** Utility functions to help initialize a site. */
 class InitUtil {
@@ -71,43 +73,12 @@
     }
   }
 
-  private static boolean modified(FileBasedConfig cfg) throws IOException {
-    byte[] curVers;
-    try {
-      curVers = IO.readFully(cfg.getFile());
-    } catch (FileNotFoundException notFound) {
-      return true;
-    }
-
-    byte[] newVers = Constants.encode(cfg.toText());
-    return !Arrays.equals(curVers, newVers);
-  }
-
   static void mkdir(final File path) {
     if (!path.isDirectory() && !path.mkdir()) {
       throw die("Cannot make directory " + path);
     }
   }
 
-  static void chmod(final int mode, final File path) {
-    path.setReadable(false, false /* all */);
-    path.setWritable(false, false /* all */);
-    path.setExecutable(false, false /* all */);
-
-    path.setReadable((mode & 0400) == 0400, true /* owner only */);
-    path.setWritable((mode & 0200) == 0200, true /* owner only */);
-    if (path.isDirectory() || (mode & 0100) == 0100) {
-      path.setExecutable(true, true /* owner only */);
-    }
-
-    if ((mode & 0044) == 0044) {
-      path.setReadable(true, false /* all */);
-    }
-    if ((mode & 0011) == 0011) {
-      path.setExecutable(true, false /* all */);
-    }
-  }
-
   static String version() {
     return com.google.gerrit.common.Version.getVersion();
   }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java
index 9555abd..b943ca3 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java
@@ -16,8 +16,8 @@
 
 import com.google.common.base.Strings;
 import com.google.common.io.Files;
+import com.google.gerrit.common.Die;
 import com.google.gerrit.pgm.util.ConsoleUI;
-import com.google.gerrit.pgm.util.Die;
 import com.google.gerrit.pgm.util.IoUtil;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.Inject;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/MaxDbInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/MaxDbInitializer.java
new file mode 100644
index 0000000..4d746cc
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/MaxDbInitializer.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.pgm.init;
+
+import static com.google.gerrit.pgm.init.InitUtil.username;
+
+public class MaxDbInitializer implements DatabaseConfigInitializer {
+
+  @Override
+  public void initConfig(Section databaseSection) {
+    final String defPort = "(maxdb default)";
+    databaseSection.string("Server hostname", "hostname", "localhost");
+    databaseSection.string("Server port", "port", defPort, true);
+    databaseSection.string("Database name", "database", "reviewdb");
+    databaseSection.string("Database username", "username", username());
+    databaseSection.password("username", "password");
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Section.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Section.java
index bbb1d9f..d0a89e7 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Section.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Section.java
@@ -51,11 +51,11 @@
   }
 
   String get(String name) {
-    return flags.cfg.getString(section, null, name);
+    return flags.cfg.getString(section, subsection, name);
   }
 
   public void set(final String name, final String value) {
-    final ArrayList<String> all = new ArrayList<String>();
+    final ArrayList<String> all = new ArrayList<>();
     all.addAll(Arrays.asList(flags.cfg.getStringList(section, subsection, name)));
 
     if (value != null) {
@@ -170,6 +170,24 @@
     return nv;
   }
 
+  public String passwordForKey(String key, String password) {
+    String ov = getSecure(password);
+    if (ov != null) {
+      // If the password is already stored, try to reuse it
+      // rather than prompting for a whole new one.
+      //
+      if (ui.isBatch() || !ui.yesno(false, "Change %s", key)) {
+        return ov;
+      }
+    }
+
+    final String nv = ui.password("%s", key);
+    if (!eq(ov, nv)) {
+      setSecure(password, nv);
+    }
+    return nv;
+  }
+
   public String getSecure(String name) {
     return flags.sec.getString(section, subsection, name);
   }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
index d980c09..800f2c3 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.pgm.init;
 
-import static com.google.gerrit.pgm.init.InitUtil.chmod;
+import static com.google.gerrit.common.FileUtil.chmod;
 import static com.google.gerrit.pgm.init.InitUtil.die;
 import static com.google.gerrit.pgm.init.InitUtil.extract;
 import static com.google.gerrit.pgm.init.InitUtil.mkdir;
@@ -126,7 +126,7 @@
   }
 
   private static List<InitStep> stepsOf(final Injector injector) {
-    final ArrayList<InitStep> r = new ArrayList<InitStep>();
+    final ArrayList<InitStep> r = new ArrayList<>();
     for (Binding<InitStep> b : all(injector)) {
       r.add(b.getProvider().get());
     }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/shell/JythonShell.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/shell/JythonShell.java
index 38f08c1..6efda29 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/shell/JythonShell.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/shell/JythonShell.java
@@ -85,7 +85,7 @@
     } catch (IllegalAccessException e) {
       throw noInterpreter(e);
     }
-    injectedVariables = new ArrayList<String>();
+    injectedVariables = new ArrayList<>();
     set("Shell", this);
   }
 
@@ -191,9 +191,9 @@
           + " is not found or not executable");
       }
     } catch (InvocationTargetException e) {
-      log.error("Exception occured while loading file " + p + " : ", e);
+      log.error("Exception occurred while loading file " + p + " : ", e);
     } catch (SecurityException e) {
-      log.error("SecurityException occured while loading file " + p + " : ", e);
+      log.error("SecurityException occurred while loading file " + p + " : ", e);
     }
   }
 
@@ -204,7 +204,7 @@
         new Object[] { in, p }
       );
     } catch (InvocationTargetException e) {
-      log.error("Exception occured while loading " + p + " : ", e);
+      log.error("Exception occurred while loading " + p + " : ", e);
     }
   }
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/AbstractProgram.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/AbstractProgram.java
index af65170..31fa7dd 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/AbstractProgram.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/AbstractProgram.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.pgm.util;
 
 
+import com.google.gerrit.common.Die;
 import com.google.gerrit.util.cli.CmdLineParser;
 import com.google.gerrit.util.cli.OptionHandlers;
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ConsoleUI.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ConsoleUI.java
index 9af95ab..3cbf047 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ConsoleUI.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ConsoleUI.java
@@ -16,6 +16,8 @@
 
 import static org.eclipse.jgit.util.StringUtils.equalsIgnoreCase;
 
+import com.google.gerrit.common.Die;
+
 import java.io.Console;
 import java.lang.reflect.InvocationTargetException;
 import java.util.Set;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ErrorLogFile.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ErrorLogFile.java
index da7704d..a766d1e 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ErrorLogFile.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ErrorLogFile.java
@@ -14,24 +14,19 @@
 
 package com.google.gerrit.pgm.util;
 
+import com.google.gerrit.common.Die;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.util.LogUtil;
+import com.google.gerrit.server.util.SystemLog;
 
-import org.apache.log4j.Appender;
 import org.apache.log4j.ConsoleAppender;
-import org.apache.log4j.DailyRollingFileAppender;
 import org.apache.log4j.Level;
 import org.apache.log4j.LogManager;
 import org.apache.log4j.Logger;
 import org.apache.log4j.PatternLayout;
-import org.apache.log4j.helpers.OnlyOnceErrorHandler;
-import org.apache.log4j.spi.ErrorHandler;
-import org.apache.log4j.spi.LoggingEvent;
 
 import java.io.File;
 import java.io.FileNotFoundException;
-import java.io.IOException;
 
 public class ErrorLogFile {
   static final String LOG_NAME = "error_log";
@@ -58,7 +53,7 @@
     if (!logdir.exists() && !logdir.mkdirs()) {
       throw new Die("Cannot create log directory: " + logdir);
     }
-    if (LogUtil.shouldConfigureLogSystem()) {
+    if (SystemLog.shouldConfigure()) {
       initLogSystem(logdir);
     }
 
@@ -75,68 +70,9 @@
   }
 
   private static void initLogSystem(final File logdir) {
-    final PatternLayout layout = new PatternLayout();
-    layout.setConversionPattern("[%d] %-5p %c %x: %m%n");
-
-    final DailyRollingFileAppender dst = new DailyRollingFileAppender();
-    dst.setName(LOG_NAME);
-    dst.setLayout(layout);
-    dst.setEncoding("UTF-8");
-    dst.setFile(new File(resolve(logdir), LOG_NAME).getPath());
-    dst.setImmediateFlush(true);
-    dst.setAppend(true);
-    dst.setThreshold(Level.INFO);
-    dst.setErrorHandler(new DieErrorHandler());
-    dst.activateOptions();
-    dst.setErrorHandler(new OnlyOnceErrorHandler());
-
     final Logger root = LogManager.getRootLogger();
     root.removeAllAppenders();
-    root.addAppender(dst);
-  }
-
-  private static File resolve(final File logs_dir) {
-    try {
-      return logs_dir.getCanonicalFile();
-    } catch (IOException e) {
-      return logs_dir.getAbsoluteFile();
-    }
-  }
-
-  private ErrorLogFile() {
-  }
-
-  private static final class DieErrorHandler implements ErrorHandler {
-    @Override
-    public void error(String message, Exception e, int errorCode,
-        LoggingEvent event) {
-      error(e != null ? e.getMessage() : message);
-    }
-
-    @Override
-    public void error(String message, Exception e, int errorCode) {
-      error(e != null ? e.getMessage() : message);
-    }
-
-    @Override
-    public void error(String message) {
-      throw new Die("Cannot open log file: " + message);
-    }
-
-    @Override
-    public void activateOptions() {
-    }
-
-    @Override
-    public void setAppender(Appender appender) {
-    }
-
-    @Override
-    public void setBackupAppender(Appender appender) {
-    }
-
-    @Override
-    public void setLogger(Logger logger) {
-    }
+    root.addAppender(SystemLog.createAppender(logdir, LOG_NAME,
+        new PatternLayout("[%d] %-5p %c %x: %m%n")));
   }
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/GarbageCollectionLogFile.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/GarbageCollectionLogFile.java
index 746355b..7d33a36 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/GarbageCollectionLogFile.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/GarbageCollectionLogFile.java
@@ -14,28 +14,20 @@
 
 package com.google.gerrit.pgm.util;
 
+import com.google.gerrit.common.Die;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.GarbageCollection;
-import com.google.gerrit.server.util.LogUtil;
+import com.google.gerrit.server.util.SystemLog;
 
-import org.apache.log4j.Appender;
-import org.apache.log4j.DailyRollingFileAppender;
-import org.apache.log4j.Level;
 import org.apache.log4j.LogManager;
 import org.apache.log4j.Logger;
 import org.apache.log4j.PatternLayout;
-import org.apache.log4j.helpers.OnlyOnceErrorHandler;
-import org.apache.log4j.spi.ErrorHandler;
-import org.apache.log4j.spi.LoggingEvent;
-import org.slf4j.LoggerFactory;
 
 import java.io.File;
 import java.io.FileNotFoundException;
-import java.io.IOException;
 
 public class GarbageCollectionLogFile {
-  private static final org.slf4j.Logger log = LoggerFactory.getLogger(GarbageCollectionLogFile.class);
 
   public static LifecycleListener start(File sitePath)
       throws FileNotFoundException {
@@ -43,7 +35,7 @@
     if (!logdir.exists() && !logdir.mkdirs()) {
       throw new Die("Cannot create log directory: " + logdir);
     }
-    if (LogUtil.shouldConfigureLogSystem()) {
+    if (SystemLog.shouldConfigure()) {
       initLogSystem(logdir);
     }
 
@@ -60,70 +52,10 @@
   }
 
   private static void initLogSystem(File logdir) {
-    PatternLayout layout = new PatternLayout();
-    layout.setConversionPattern("[%d] %-5p %x: %m%n");
-
-    DailyRollingFileAppender dst = new DailyRollingFileAppender();
-    dst.setName(GarbageCollection.LOG_NAME);
-    dst.setLayout(layout);
-    dst.setEncoding("UTF-8");
-    dst.setFile(new File(resolve(logdir), GarbageCollection.LOG_NAME).getPath());
-    dst.setImmediateFlush(true);
-    dst.setAppend(true);
-    dst.setThreshold(Level.INFO);
-    dst.setErrorHandler(new LogErrorHandler());
-    dst.activateOptions();
-    dst.setErrorHandler(new OnlyOnceErrorHandler());
-
     Logger gcLogger = LogManager.getLogger(GarbageCollection.LOG_NAME);
     gcLogger.removeAllAppenders();
-    gcLogger.addAppender(dst);
+    gcLogger.addAppender(SystemLog.createAppender(logdir,
+        GarbageCollection.LOG_NAME, new PatternLayout("[%d] %-5p %x: %m%n")));
     gcLogger.setAdditivity(false);
   }
-
-  private static File resolve(File logs_dir) {
-    try {
-      return logs_dir.getCanonicalFile();
-    } catch (IOException e) {
-      return logs_dir.getAbsoluteFile();
-    }
-  }
-
-  private GarbageCollectionLogFile() {
-  }
-
-  private static final class LogErrorHandler implements ErrorHandler {
-    @Override
-    public void error(String message, Exception e, int errorCode,
-        LoggingEvent event) {
-      error(e != null ? e.getMessage() : message);
-    }
-
-    @Override
-    public void error(String message, Exception e, int errorCode) {
-      error(e != null ? e.getMessage() : message);
-    }
-
-    @Override
-    public void error(String message) {
-      log.error("Cannot open '" + GarbageCollection.LOG_NAME + "' log file: "
-          + message);
-    }
-
-    @Override
-    public void activateOptions() {
-    }
-
-    @Override
-    public void setAppender(Appender appender) {
-    }
-
-    @Override
-    public void setBackupAppender(Appender appender) {
-    }
-
-    @Override
-    public void setLogger(Logger logger) {
-    }
-  }
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/LogFileCompressor.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/LogFileCompressor.java
index aed1b9a..db74ac3 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/LogFileCompressor.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/LogFileCompressor.java
@@ -57,7 +57,7 @@
 
     @Override
     public void start() {
-      queue.getDefaultQueue().scheduleWithFixedDelay(compresser, 1, 24, HOURS);
+      queue.getDefaultQueue().scheduleAtFixedRate(compresser, 1, 24, HOURS);
     }
 
     @Override
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/RuntimeShutdown.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/RuntimeShutdown.java
index c00ad7f..40aaa75 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/RuntimeShutdown.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/RuntimeShutdown.java
@@ -46,7 +46,7 @@
     private static final Logger log =
         LoggerFactory.getLogger(ShutdownCallback.class);
 
-    private final List<Runnable> tasks = new ArrayList<Runnable>();
+    private final List<Runnable> tasks = new ArrayList<>();
     private boolean shutdownStarted;
     private boolean shutdownComplete;
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java
index 11968db..3c60510 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java
@@ -18,6 +18,7 @@
 import static com.google.inject.Stage.PRODUCTION;
 
 import com.google.common.collect.Lists;
+import com.google.gerrit.common.Die;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -89,7 +90,7 @@
   /** @return provides database connectivity and site path. */
   protected Injector createDbInjector(final DataSourceProvider.Context context) {
     final File sitePath = getSitePath();
-    final List<Module> modules = new ArrayList<Module>();
+    final List<Module> modules = new ArrayList<>();
 
     Module sitePathModule = new AbstractModule() {
       @Override
diff --git a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/libraries.config b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/libraries.config
index b5e702f..16bceee 100644
--- a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/libraries.config
+++ b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/libraries.config
@@ -15,16 +15,16 @@
 
 # Version should match lib/bouncycastle/BUCK
 [library "bouncyCastleProvider"]
-  name = Bouncy Castle Crypto Provider v149
-  url = http://www.bouncycastle.org/download/bcprov-jdk15on-149.jar
-  sha1 = f5155f04330459104b79923274db5060c1057b99
+  name = Bouncy Castle Crypto Provider v151
+  url = http://www.bouncycastle.org/download/bcprov-jdk15on-151.jar
+  sha1 = 9ab8afcc2842d5ef06eb775a0a2b12783b99aa80
   remove = bcprov-.*[.]jar
 
 # Version should match lib/bouncycastle/BUCK
 [library "bouncyCastleSSL"]
-  name = Bouncy Castle Crypto SSL v149
-  url = http://www.bouncycastle.org/download/bcpkix-jdk15on-149.jar
-  sha1 = 924cc7ad2f589630c97b918f044296ebf1bb6855
+  name = Bouncy Castle Crypto SSL v151
+  url = http://www.bouncycastle.org/download/bcpkix-jdk15on-151.jar
+  sha1 = 6c8c1f61bf27a09f9b1a8abc201523669bba9597
   needs = bouncyCastleProvider
   remove = bcpkix-.*[.]jar
 
diff --git a/gerrit-plugin-api/BUCK b/gerrit-plugin-api/BUCK
index d808e0f..a6afafb 100644
--- a/gerrit-plugin-api/BUCK
+++ b/gerrit-plugin-api/BUCK
@@ -20,6 +20,8 @@
 java_library(
   name = 'lib',
   exported_deps = PLUGIN_API + [
+    '//gerrit-antlr:query_exception',
+    '//gerrit-antlr:query_parser',
     '//gerrit-common:annotations',
     '//gerrit-common:server',
     '//gerrit-extension-api:api',
@@ -59,4 +61,5 @@
     '//lib/bouncycastle:bcpg',
   ],
   visibility = ['PUBLIC'],
+  do_it_wrong = True,
 )
diff --git a/gerrit-plugin-api/pom.xml b/gerrit-plugin-api/pom.xml
index 8f2685b..563d7a9 100644
--- a/gerrit-plugin-api/pom.xml
+++ b/gerrit-plugin-api/pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-api</artifactId>
-  <version>2.9.5</version>
+  <version>2.10.8</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Plugin API</name>
   <description>API for Gerrit Plugins</description>
diff --git a/gerrit-plugin-archetype/pom.xml b/gerrit-plugin-archetype/pom.xml
index 72d9922..b3c0480 100644
--- a/gerrit-plugin-archetype/pom.xml
+++ b/gerrit-plugin-archetype/pom.xml
@@ -20,7 +20,7 @@
 
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-archetype</artifactId>
-  <version>2.9.5</version>
+  <version>2.10.8</version>
   <name>Gerrit Code Review - Plugin Archetype</name>
   <description>Maven Archetype for Gerrit Plugins</description>
   <url>http://code.google.com/p/gerrit/</url>
diff --git a/gerrit-plugin-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml b/gerrit-plugin-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
index 21f508b..681af10 100644
--- a/gerrit-plugin-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
+++ b/gerrit-plugin-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
@@ -58,6 +58,17 @@
       </includes>
     </fileSet>
 
+    <fileSet filtered="true">
+      <directory></directory>
+      <include>.buckconfig</include>
+      <include>BUCK</include>
+      <include>VERSION</include>
+      <include>lib/gerrit/BUCK</include>
+      <excludes>
+        <exclude>**/*.java</exclude>
+      </excludes>
+    </fileSet>
+
     <fileSet>
       <directory></directory>
       <includes>
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/.buckconfig b/gerrit-plugin-archetype/src/main/resources/archetype-resources/.buckconfig
new file mode 100644
index 0000000..1044c12
--- /dev/null
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/.buckconfig
@@ -0,0 +1,14 @@
+[alias]
+  ${pluginName} = //:${pluginName}
+  plugin = //:${pluginName}
+
+[java]
+  src_roots = java, resources
+
+[project]
+  ignore = .git
+
+[cache]
+  mode = dir
+  dir = buck-out/cache
+
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/.gitignore b/gerrit-plugin-archetype/src/main/resources/archetype-resources/.gitignore
index 80d6257..43838b0 100644
--- a/gerrit-plugin-archetype/src/main/resources/archetype-resources/.gitignore
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/.gitignore
@@ -1,3 +1,7 @@
+/.buckversion
+/.buckd
+/buck-out
+/bucklets
 /target
 /.classpath
 /.project
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/BUCK b/gerrit-plugin-archetype/src/main/resources/archetype-resources/BUCK
new file mode 100644
index 0000000..55a2a4a
--- /dev/null
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/BUCK
@@ -0,0 +1,22 @@
+include_defs('//bucklets/gerrit_plugin.bucklet')
+
+gerrit_plugin(
+  name = '${pluginName}',
+  srcs = glob(['src/main/java/**/*.java']),
+  resources = glob(['src/main/resources/**/*']),
+  manifest_entries = [
+    'Gerrit-PluginName: ${pluginName}',
+    'Gerrit-ApiType: ${gerritApiType}',
+    'Gerrit-ApiVersion: ${gerritApiVersion}',
+    'Gerrit-Module: ${package}.Module',
+    'Gerrit-SshModule: ${package}.SshModule',
+    'Gerrit-HttpModule: ${package}.HttpModule',
+  ],
+)
+
+# this is required for bucklets/tools/eclipse/project.py to work
+java_library(
+  name = 'classpath',
+  deps = [':${pluginName}__plugin'],
+)
+
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/VERSION b/gerrit-plugin-archetype/src/main/resources/archetype-resources/VERSION
new file mode 100644
index 0000000..8bbb460
--- /dev/null
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/VERSION
@@ -0,0 +1,5 @@
+# Used by BUCK to include "Implementation-Version" in plugin Manifest.
+# If this file doesn't exist the output of 'git describe' is used
+# instead.
+PLUGIN_VERSION = '${version}'
+
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/lib/gerrit/BUCK b/gerrit-plugin-archetype/src/main/resources/archetype-resources/lib/gerrit/BUCK
new file mode 100644
index 0000000..b1648d3
--- /dev/null
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/lib/gerrit/BUCK
@@ -0,0 +1,12 @@
+include_defs('//bucklets/maven_jar.bucklet')
+
+VER = '${gerritApiVersion}'
+REPO = MAVEN_LOCAL
+
+maven_jar(
+  name = '${gerritApiType}-api',
+  id = 'com.google.gerrit:gerrit-${gerritApiType}-api:' + VER,
+  attach_source = False,
+  repository = REPO,
+  license = 'Apache2.0',
+)
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/build.md b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/build.md
new file mode 100644
index 0000000..80f0627d
--- /dev/null
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/build.md
@@ -0,0 +1,79 @@
+Build
+=====
+
+This plugin can be built with Buck or Maven.
+
+Buck
+----
+
+Two build modes are supported: Standalone and in Gerrit tree.
+The standalone build mode is recommended, as this mode doesn't require
+the Gerrit tree to exist locally.
+
+
+### Build standalone
+
+Clone bucklets library:
+
+```
+  git clone https://gerrit.googlesource.com/bucklets
+
+```
+and link it to @PLUGIN@ plugin directory:
+
+```
+  cd @PLUGIN@ && ln -s ../bucklets .
+```
+
+Add link to the .buckversion file:
+
+```
+  cd @PLUGIN@ && ln -s bucklets/buckversion .buckversion
+```
+
+To build the plugin, issue the following command:
+
+
+```
+  buck build plugin
+```
+
+The output is created in
+
+```
+  buck-out/gen/@PLUGIN@.jar
+```
+
+### Build in Gerrit tree
+
+Clone or link this plugin to the plugins directory of Gerrit's source
+tree, and issue the command:
+
+```
+  buck build plugins/@PLUGIN@
+```
+
+The output is created in
+
+```
+  buck-out/gen/plugins/@PLUGIN@/@PLUGIN@.jar
+```
+
+This project can be imported into the Eclipse IDE:
+
+```
+  ./tools/eclipse/project.py
+```
+
+Maven
+-----
+
+Note that the Maven build is provided for compatibility reasons, but
+it is considered to be deprecated and will be removed in a future
+version of this plugin.
+
+To build with Maven, run
+
+```
+mvn clean package
+```
diff --git a/gerrit-plugin-gwt-archetype/pom.xml b/gerrit-plugin-gwt-archetype/pom.xml
index 7d541d6..a70604ec 100644
--- a/gerrit-plugin-gwt-archetype/pom.xml
+++ b/gerrit-plugin-gwt-archetype/pom.xml
@@ -20,7 +20,7 @@
 
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-gwt-archetype</artifactId>
-  <version>2.9.5</version>
+  <version>2.10.8</version>
   <name>Gerrit Code Review - Web UI GWT Plugin Archetype</name>
   <description>Maven Archetype for Gerrit Web UI GWT Plugins</description>
   <url>http://code.google.com/p/gerrit/</url>
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml b/gerrit-plugin-gwt-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
index f619f91..b443c4d 100644
--- a/gerrit-plugin-gwt-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
@@ -25,7 +25,7 @@
       <defaultValue>http://code.google.com/p/gerrit/</defaultValue>
     </requiredProperty>
     <requiredProperty key="Gwt-Version">
-      <defaultValue>2.5.1</defaultValue>
+      <defaultValue>2.6.1</defaultValue>
     </requiredProperty>
 
     <requiredProperty key="gerritApiVersion">
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/HelloMenu.java b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/HelloMenu.java
index 0baa044..f3c8dea 100644
--- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/HelloMenu.java
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/HelloMenu.java
@@ -27,7 +27,7 @@
 
   @Inject
   public HelloMenu(@PluginName String pluginName) {
-    menuEntries = new ArrayList<TopMenu.MenuEntry>();
+    menuEntries = new ArrayList<>();
     menuEntries.add(new MenuEntry("Hello", Collections
         .singletonList(new MenuItem("Hello Screen", "#/x/" + pluginName, ""))));
   }
diff --git a/gerrit-plugin-gwtui/BUCK b/gerrit-plugin-gwtui/BUCK
index 8022bd1..419176d 100644
--- a/gerrit-plugin-gwtui/BUCK
+++ b/gerrit-plugin-gwtui/BUCK
@@ -22,12 +22,12 @@
   visibility = ['PUBLIC'],
 )
 
-java_library2(
+java_library(
   name = 'gwtui-api-lib2',
   srcs = SRCS,
   resources = glob(['src/main/**/*']),
-  deps = ['//gerrit-gwtui-common:client-lib2'],
-  compile_deps = DEPS,
+  exported_deps = ['//gerrit-gwtui-common:client-lib2'],
+  provided_deps = DEPS,
   visibility = ['PUBLIC'],
 )
 
@@ -51,8 +51,8 @@
   name = 'gwtui-api-javadoc',
   title = 'Gerrit Review GWT Extension API Documentation',
   pkg = 'com.google.gerrit',
-  paths = ['$SRCDIR/src/main/java'] + COMMON,
-  srcs = SRCS + glob(COMMON),
-  deps = DEPS + ['//gerrit-gwtui-common:client-src-lib'],
+  paths = ['src/main/java'] + COMMON,
+  srcs = SRCS,
+  deps = DEPS + ['//gerrit-gwtui-common:client-lib2'],
   visibility = ['PUBLIC'],
 )
diff --git a/gerrit-plugin-gwtui/pom.xml b/gerrit-plugin-gwtui/pom.xml
index c05ba69..c29abf3 100644
--- a/gerrit-plugin-gwtui/pom.xml
+++ b/gerrit-plugin-gwtui/pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-gwtui</artifactId>
-  <version>2.9.5</version>
+  <version>2.10.8</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Plugin GWT UI</name>
   <description>Common Classes for Gerrit GWT UI Plugins</description>
diff --git a/gerrit-plugin-js-archetype/pom.xml b/gerrit-plugin-js-archetype/pom.xml
index d869165..4ce8f85 100644
--- a/gerrit-plugin-js-archetype/pom.xml
+++ b/gerrit-plugin-js-archetype/pom.xml
@@ -20,7 +20,7 @@
 
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-js-archetype</artifactId>
-  <version>2.9.5</version>
+  <version>2.10.8</version>
   <name>Gerrit Code Review - Web UI JavaScript Plugin Archetype</name>
   <description>Maven Archetype for Gerrit Web UI JavaScript Plugins</description>
   <url>http://code.google.com/p/gerrit/</url>
diff --git a/gerrit-prettify/BUCK b/gerrit-prettify/BUCK
index e753ad9..cc42d2b7 100644
--- a/gerrit-prettify/BUCK
+++ b/gerrit-prettify/BUCK
@@ -6,17 +6,15 @@
     SRC + 'client/**/*.java',
     SRC + 'common/**/*.java',
   ]),
-  gwtxml = SRC + 'PrettyFormatter.gwt.xml',
+  gwt_xml = SRC + 'PrettyFormatter.gwt.xml',
   resources = glob([
     'src/main/java/com/google/gerrit/prettify/client/*.properties',
   ]),
   deps = [
+    ':google-code-prettify',
     '//gerrit-patch-jgit:client',
     '//gerrit-reviewdb:client',
     '//gerrit-gwtexpui:SafeHtml',
-  ],
-  compile_deps = [
-    ':google-code-prettify',
     '//lib:guava',
     '//lib:gwtjsonrpc',
     '//lib/gwt:user',
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFormatter.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFormatter.java
index d4aef2a..ed42f10 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFormatter.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFormatter.java
@@ -125,7 +125,7 @@
   public void format(SparseFileContent src) {
     content = new SparseFileContent();
     content.setSize(src.size());
-    trailingEdits = new HashSet<Integer>();
+    trailingEdits = new HashSet<>();
 
     String html = toHTML(src);
 
@@ -365,7 +365,7 @@
     // in the source. That simplifies our loop below because we'll never
     // run off the end of the edit list.
     //
-    List<Edit> edits = new ArrayList<Edit>(this.edits.size() + 1);
+    List<Edit> edits = new ArrayList<>(this.edits.size() + 1);
     edits.addAll(this.edits);
     edits.add(new Edit(src.size(), src.size()));
 
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseFileContent.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseFileContent.java
index aa08af0..b6e3bf9 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseFileContent.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseFileContent.java
@@ -29,7 +29,7 @@
   private transient int currentRangeIdx;
 
   public SparseFileContent() {
-    ranges = new ArrayList<Range>();
+    ranges = new ArrayList<>();
   }
 
   public int size() {
@@ -222,7 +222,7 @@
 
   public SparseFileContent apply(SparseFileContent a, List<Edit> edits) {
     EditList list = new EditList(edits, size, a.size(), size);
-    ArrayList<String> lines = new ArrayList<String>(size);
+    ArrayList<String> lines = new ArrayList<>(size);
     for (final EditList.Hunk hunk : list.getHunks()) {
       while (hunk.next()) {
         if (hunk.isContextLine()) {
@@ -277,7 +277,7 @@
 
     private Range(final int b) {
       base = b;
-      lines = new ArrayList<String>();
+      lines = new ArrayList<>();
     }
 
     protected Range() {
diff --git a/gerrit-reviewdb/BUCK b/gerrit-reviewdb/BUCK
index b133f1a..faf80a8 100644
--- a/gerrit-reviewdb/BUCK
+++ b/gerrit-reviewdb/BUCK
@@ -3,8 +3,9 @@
 gwt_module(
   name = 'client',
   srcs = glob([SRC + 'client/**/*.java']),
-  gwtxml = SRC + 'ReviewDB.gwt.xml',
-  compile_deps = [
+  gwt_xml = SRC + 'ReviewDB.gwt.xml',
+  deps = [
+    '//gerrit-extension-api:client',
     '//lib:gwtorm',
     '//lib:gwtorm_src'
   ],
@@ -15,6 +16,9 @@
   name = 'server',
   srcs = glob([SRC + '**/*.java']),
   resources = glob(['src/main/resources/**/*']),
-  deps = ['//lib:gwtorm'],
+  deps = [
+    '//gerrit-extension-api:api',
+    '//lib:gwtorm',
+  ],
   visibility = ['PUBLIC'],
 )
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountDiffPreference.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountDiffPreference.java
index a19ae08..cf951c1 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountDiffPreference.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountDiffPreference.java
@@ -91,6 +91,7 @@
     p.setShowTabs(true);
     p.setContext(DEFAULT_CONTEXT);
     p.setManualReview(false);
+    p.setHideEmptyPane(false);
     return p;
   }
 
@@ -152,6 +153,9 @@
   @Column(id = 19, length = 20, notNull = false)
   protected String theme;
 
+  @Column(id = 20)
+  protected boolean hideEmptyPane;
+
   protected AccountDiffPreference() {
   }
 
@@ -178,6 +182,7 @@
     this.hideTopMenu = p.hideTopMenu;
     this.hideLineNumbers = p.hideLineNumbers;
     this.renderEntireFile = p.renderEntireFile;
+    this.hideEmptyPane = p.hideEmptyPane;
   }
 
   public Account.Id getAccountId() {
@@ -330,4 +335,12 @@
   public void setTheme(Theme theme) {
     this.theme = theme != null ? theme.name() : null;
   }
+
+  public boolean isHideEmptyPane() {
+    return hideEmptyPane;
+  }
+
+  public void setHideEmptyPane(boolean hideEmptyPane) {
+    this.hideEmptyPane = hideEmptyPane;
+  }
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountExternalId.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountExternalId.java
index 4be5113..8181d50 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountExternalId.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountExternalId.java
@@ -36,9 +36,6 @@
   /** Scheme for the username used to authenticate an account, e.g. over SSH. */
   public static final String SCHEME_USERNAME = "username:";
 
-  /** Very old scheme from Gerrit Code Review 1.x imports. */
-  public static final String LEGACY_GAE = "Google Account ";
-
   public static class Key extends StringKey<com.google.gwtorm.client.Key<?>> {
     private static final long serialVersionUID = 1L;
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java
index 2257ea2..c6b3089 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java
@@ -75,6 +75,14 @@
     EXPAND_ALL
   }
 
+  public static enum ReviewCategoryStrategy {
+    NONE,
+    NAME,
+    EMAIL,
+    USERNAME,
+    ABBREV
+  }
+
   public static enum DiffView {
     SIDE_BY_SIDE,
     UNIFIED_DIFF
@@ -103,6 +111,12 @@
     }
   }
 
+  public static AccountGeneralPreferences createDefault() {
+    AccountGeneralPreferences p = new AccountGeneralPreferences();
+    p.resetToDefaults();
+    return p;
+  }
+
   /** Number of changes to show in a screen. */
   @Column(id = 2)
   protected short maximumPageSize;
@@ -140,9 +154,6 @@
   @Column(id = 10)
   protected boolean reversePatchSetOrder;
 
-  @Column(id = 11)
-  protected boolean showUserInReview;
-
   @Column(id = 12)
   protected boolean relativeDateInChangeTable;
 
@@ -158,6 +169,12 @@
   @Column(id = 16)
   protected boolean sizeBarInChangeTable;
 
+  @Column(id = 17)
+  protected boolean legacycidInChangeTable;
+
+  @Column(id = 18, length = 20, notNull = false)
+  protected String reviewCategoryStrategy;
+
   public AccountGeneralPreferences() {
   }
 
@@ -231,12 +248,8 @@
     this.reversePatchSetOrder = reversePatchSetOrder;
   }
 
-  public boolean isShowUsernameInReviewCategory() {
-    return showUserInReview;
-  }
-
-  public void setShowUsernameInReviewCategory(final boolean showUsernameInReviewCategory) {
-    this.showUserInReview = showUsernameInReviewCategory;
+  public boolean isShowInfoInReviewCategory() {
+    return getReviewCategoryStrategy() != ReviewCategoryStrategy.NONE;
   }
 
   public DateFormat getDateFormat() {
@@ -269,6 +282,18 @@
     this.relativeDateInChangeTable = relativeDateInChangeTable;
   }
 
+  public ReviewCategoryStrategy getReviewCategoryStrategy() {
+    if (reviewCategoryStrategy == null) {
+      return ReviewCategoryStrategy.NONE;
+    }
+    return ReviewCategoryStrategy.valueOf(reviewCategoryStrategy);
+  }
+
+  public void setReviewCategoryStrategy(
+      ReviewCategoryStrategy strategy) {
+    reviewCategoryStrategy = strategy.name();
+  }
+
   public CommentVisibilityStrategy getCommentVisibilityStrategy() {
     if (commentVisibilityStrategy == null) {
       return CommentVisibilityStrategy.EXPAND_RECENT;
@@ -308,13 +333,21 @@
     this.sizeBarInChangeTable = sizeBarInChangeTable;
   }
 
+  public boolean isLegacycidInChangeTable() {
+    return legacycidInChangeTable;
+  }
+
+  public void setLegacycidInChangeTable(boolean legacycidInChangeTable) {
+    this.legacycidInChangeTable = legacycidInChangeTable;
+  }
+
   public void resetToDefaults() {
     maximumPageSize = DEFAULT_PAGESIZE;
     showSiteHeader = true;
     useFlashClipboard = true;
     copySelfOnEmail = false;
     reversePatchSetOrder = false;
-    showUserInReview = false;
+    reviewCategoryStrategy = null;
     downloadUrl = null;
     downloadCommand = null;
     dateFormat = null;
@@ -324,5 +357,6 @@
     diffView = null;
     changeScreen = null;
     sizeBarInChangeTable = true;
+    legacycidInChangeTable = false;
   }
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AuthType.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AuthType.java
index 6af9610..38a78ba 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AuthType.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AuthType.java
@@ -80,5 +80,8 @@
   CUSTOM_EXTENSION,
 
   /** Development mode to enable becoming anyone you want. */
-  DEVELOPMENT_BECOME_ANY_ACCOUNT
+  DEVELOPMENT_BECOME_ANY_ACCOUNT,
+
+  /** Generic OAuth provider over HTTP. */
+  OAUTH
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/InheritedBoolean.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/InheritedBoolean.java
index 954b494..b5cef60 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/InheritedBoolean.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/InheritedBoolean.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.reviewdb.client;
 
-import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
+import com.google.gerrit.extensions.common.InheritableBoolean;
 
 public class InheritedBoolean {
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java
index fc13a33..08c3f52 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java
@@ -18,6 +18,7 @@
 import com.google.gwtorm.client.StringKey;
 
 import java.sql.Timestamp;
+import java.util.Objects;
 
 /** A comment left by a user on a specific line of a {@link Patch}. */
 public final class PatchLineComment {
@@ -53,6 +54,21 @@
     protected void set(String newValue) {
       uuid = newValue;
     }
+
+    @Override
+    public String toString() {
+      StringBuilder builder = new StringBuilder();
+      builder.append("PatchLineComment.Key{");
+      builder.append("Change.Id=")
+        .append(getParentKey().getParentKey().getParentKey().get()).append(',');
+      builder.append("PatchSet.Id=")
+        .append(getParentKey().getParentKey().get()).append(',');
+      builder.append("filename=")
+        .append(getParentKey().getFileName()).append(',');
+      builder.append("uuid=").append(get());
+      builder.append("}");
+      return builder.toString();
+    }
   }
 
   public static final char STATUS_DRAFT = 'd';
@@ -120,6 +136,15 @@
   @Column(id = 9, notNull = false)
   protected CommentRange range;
 
+  /**
+   * The RevId for the commit to which this comment is referring.
+   *
+   * Note that this field is not stored in the database. It is just provided
+   * for users of this class to avoid a lookup when they don't have easy access
+   * to a ReviewDb.
+   */
+  protected RevId revId;
+
   protected PatchLineComment() {
   }
 
@@ -196,4 +221,51 @@
   public CommentRange getRange() {
     return range;
   }
+
+  public void setRevId(RevId rev) {
+    revId = rev;
+  }
+
+  public RevId getRevId() {
+    return revId;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (o instanceof PatchLineComment) {
+      PatchLineComment c = (PatchLineComment) o;
+      return Objects.equals(key, c.getKey())
+          && Objects.equals(lineNbr, c.getLine())
+          && Objects.equals(author, c.getAuthor())
+          && Objects.equals(writtenOn, c.getWrittenOn())
+          && Objects.equals(status, c.getStatus().getCode())
+          && Objects.equals(side, c.getSide())
+          && Objects.equals(message, c.getMessage())
+          && Objects.equals(parentUuid, c.getParentUuid())
+          && Objects.equals(range, c.getRange())
+          && Objects.equals(revId, c.getRevId());
+    }
+    return false;
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder builder = new StringBuilder();
+    builder.append("PatchLineComment{");
+    builder.append("key=").append(key).append(',');
+    builder.append("lineNbr=").append(lineNbr).append(',');
+    builder.append("author=").append(author.get()).append(',');
+    builder.append("writtenOn=").append(writtenOn.toString()).append(',');
+    builder.append("status=").append(status).append(',');
+    builder.append("side=").append(side).append(',');
+    builder.append("message=").append(Objects.toString(message, ""))
+      .append(',');
+    builder.append("parentUuid=").append(Objects.toString(parentUuid, ""))
+      .append(',');
+    builder.append("range=").append(Objects.toString(range, ""))
+      .append(',');
+    builder.append("revId=").append(revId != null ? revId.get() : "");
+    builder.append('}');
+    return builder.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 46e9b22..1114813 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
@@ -14,6 +14,9 @@
 
 package com.google.gerrit.reviewdb.client;
 
+import com.google.gerrit.extensions.api.projects.ProjectState;
+import com.google.gerrit.extensions.common.InheritableBoolean;
+import com.google.gerrit.extensions.common.SubmitType;
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.StringKey;
 
@@ -65,32 +68,6 @@
     }
   }
 
-  public static enum SubmitType {
-    FAST_FORWARD_ONLY,
-
-    MERGE_IF_NECESSARY,
-
-    REBASE_IF_NECESSARY,
-
-    MERGE_ALWAYS,
-
-    CHERRY_PICK
-  }
-
-  public static enum State {
-    ACTIVE,
-
-    READ_ONLY,
-
-    HIDDEN
-  }
-
-  public static enum InheritableBoolean {
-    TRUE,
-    FALSE,
-    INHERIT
-  }
-
   protected NameKey name;
 
   protected String description;
@@ -101,7 +78,7 @@
 
   protected SubmitType submitType;
 
-  protected State state;
+  protected ProjectState state;
 
   protected NameKey parent;
 
@@ -123,7 +100,7 @@
   public Project(Project.NameKey nameKey) {
     name = nameKey;
     submitType = SubmitType.MERGE_IF_NECESSARY;
-    state = State.ACTIVE;
+    state = ProjectState.ACTIVE;
     useContributorAgreements = InheritableBoolean.INHERIT;
     useSignedOffBy = InheritableBoolean.INHERIT;
     requireChangeID = InheritableBoolean.INHERIT;
@@ -194,11 +171,11 @@
     submitType = type;
   }
 
-  public State getState() {
+  public ProjectState getState() {
     return state;
   }
 
-  public void setState(final State newState) {
+  public void setState(final ProjectState newState) {
     state = newState;
   }
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
index ede5c26..968cfde 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.reviewdb.client;
 
+
 /** Constants and utilities for Gerrit-specific ref names. */
 public class RefNames {
   public static final String REFS_CHANGES = "refs/changes/";
@@ -24,6 +25,9 @@
   /** Configuration settings for a project {@code refs/meta/config} */
   public static final String REFS_CONFIG = "refs/meta/config";
 
+  /** Preference settings for a user {@code refs/users} */
+  public static final String REFS_USER = "refs/users/";
+
   /** Configurations of project-specific dashboards (canned search queries). */
   public static final String REFS_DASHBOARDS = "refs/meta/dashboards/";
 
@@ -39,6 +43,20 @@
    */
   public static final String REFS_CACHE_AUTOMERGE = "refs/cache-automerge/";
 
+  public static String refsUsers(Account.Id accountId) {
+    StringBuilder r = new StringBuilder();
+    r.append(REFS_USER);
+    int account = accountId.get();
+    int m = account % 100;
+    if (m < 10) {
+      r.append('0');
+    }
+    r.append(m);
+    r.append('/');
+    r.append(account);
+    return r.toString();
+  }
+
   private RefNames() {
   }
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/UserIdentity.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/UserIdentity.java
index c7b4cfe..ddc1297 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/UserIdentity.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/UserIdentity.java
@@ -23,6 +23,9 @@
   /** Email address (or user@host style string anyway). */
   protected String email;
 
+  /** Username of the user. */
+  protected String username;
+
   /** Time (in UTC) when the identity was constructed. */
   protected Timestamp when;
 
@@ -48,6 +51,10 @@
     email = e;
   }
 
+  public String getUsername() {
+    return username;
+  }
+
   public Timestamp getDate() {
     return when;
   }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountSshKeyAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountSshKeyAccess.java
index b31b5b6..b170a3d 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountSshKeyAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountSshKeyAccess.java
@@ -29,4 +29,7 @@
 
   @Query("WHERE id.accountId = ?")
   ResultSet<AccountSshKey> byAccount(Account.Id id) throws OrmException;
+
+  @Query("WHERE id.accountId = ? ORDER BY id.seq DESC LIMIT 1")
+  ResultSet<AccountSshKey> byAccountLast(Account.Id id) throws OrmException;
 }
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
index 165c8be..0b7f2c1 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
@@ -101,6 +101,11 @@
 
 
 -- *********************************************************************
+-- PatchSetAccess
+CREATE INDEX patch_sets_byRevision
+ON patch_sets (revision);
+
+-- *********************************************************************
 -- PatchSetAncestorAccess
 --    @PrimaryKey covers: ancestorsOf
 --    covers:             descendantsOf
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql
new file mode 100644
index 0000000..5faa71b
--- /dev/null
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql
@@ -0,0 +1,144 @@
+delimiter #
+-- Gerrit 2 : MaxDB
+--
+
+-- Indexes to support @Query
+--
+
+-- *********************************************************************
+-- AccountAccess
+--    covers:             byPreferredEmail, suggestByPreferredEmail
+CREATE INDEX accounts_byPreferredEmail
+ON accounts (preferred_email)
+#
+
+--    covers:             suggestByFullName
+CREATE INDEX accounts_byFullName
+ON accounts (full_name)
+#
+
+
+-- *********************************************************************
+-- AccountExternalIdAccess
+--    covers:             byAccount
+CREATE INDEX account_external_ids_byAccount
+ON account_external_ids (account_id)
+#
+
+--    covers:             byEmailAddress, suggestByEmailAddress
+CREATE INDEX account_external_ids_byEmail
+ON account_external_ids (email_address)
+#
+
+
+-- *********************************************************************
+-- AccountGroupMemberAccess
+--    @PrimaryKey covers: byAccount
+CREATE INDEX account_group_members_byGroup
+ON account_group_members (group_id)
+#
+
+-- *********************************************************************
+-- AccountGroupIncludeByUuidAccess
+--    @PrimaryKey covers: byGroup
+CREATE INDEX acc_gr_incl_by_uuid_byInclude
+ON account_group_by_id (include_uuid)
+#
+
+
+-- *********************************************************************
+-- AccountProjectWatchAccess
+--    @PrimaryKey covers: byAccount
+--    covers:             byProject
+CREATE INDEX acc_project_watches_byProject
+ON account_project_watches (project_name)
+#
+
+-- *********************************************************************
+-- AccountSshKeyAccess
+--    @PrimaryKey covers: byAccount, valid
+
+
+-- *********************************************************************
+-- ApprovalCategoryAccess
+--    too small to bother indexing
+
+
+-- *********************************************************************
+-- ApprovalCategoryValueAccess
+--     @PrimaryKey covers: byCategory
+
+
+-- *********************************************************************
+-- BranchAccess
+--    @PrimaryKey covers: byProject
+
+
+-- *********************************************************************
+-- ChangeAccess
+--    covers:             submitted, allSubmitted
+CREATE INDEX changes_submitted
+ON changes (status, dest_project_name, dest_branch_name, last_updated_on)
+#
+
+--    covers:             byProjectOpenPrev, byProjectOpenNext
+CREATE INDEX changes_byProjectOpen
+ON changes (open, dest_project_name, sort_key)
+#
+
+--    covers:             byProject
+CREATE INDEX changes_byProject
+ON changes (dest_project_name)
+#
+
+CREATE INDEX changes_key
+ON changes (change_key)
+#
+
+
+-- *********************************************************************
+-- ChangeMessageAccess
+--    @PrimaryKey covers: byChange
+
+--    covers:             byPatchSet
+CREATE INDEX change_messages_byPatchset
+ON change_messages (patchset_change_id, patchset_patch_set_id)
+#
+
+-- *********************************************************************
+-- PatchLineCommentAccess
+--    @PrimaryKey covers: published, draft
+CREATE INDEX patch_comment_drafts
+ON patch_comments (status, author_id)
+#
+
+-- *********************************************************************
+-- PatchSetAccess
+CREATE INDEX patch_sets_byRevision
+ON patch_sets (revision)
+#
+
+-- *********************************************************************
+-- PatchSetAncestorAccess
+--    @PrimaryKey covers: ancestorsOf
+--    covers:             descendantsOf
+CREATE INDEX patch_set_ancestors_desc
+ON patch_set_ancestors (ancestor_revision)
+#
+
+
+-- *********************************************************************
+-- StarredChangeAccess
+--    @PrimaryKey covers: byAccount
+
+CREATE INDEX starred_changes_byChange
+ON starred_changes (change_id)
+#
+
+-- *********************************************************************
+-- SubmoduleSubscriptionAccess
+
+CREATE INDEX submod_subscr_ac_bySubscription
+ON submodule_subscriptions (submodule_project_name, submodule_branch_name)
+#
+
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
index ad92293..25e3fae 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
@@ -152,6 +152,11 @@
 
 
 -- *********************************************************************
+-- PatchSetAccess
+CREATE INDEX patch_sets_byRevision
+ON patch_sets (revision);
+
+-- *********************************************************************
 -- PatchSetAncestorAccess
 --    @PrimaryKey covers: ancestorsOf
 --    covers:             descendantsOf
diff --git a/gerrit-server/BUCK b/gerrit-server/BUCK
index 070764a..7384cfd 100644
--- a/gerrit-server/BUCK
+++ b/gerrit-server/BUCK
@@ -8,14 +8,14 @@
 )
 RESOURCES =  glob(['src/main/resources/**/*'])
 
-java_library2(
+java_library(
   name = 'constants',
   srcs = CONSTANTS_SRC,
   visibility = ['PUBLIC'],
 )
 
 # TODO(sop) break up gerrit-server java_library(), its too big
-java_library2(
+java_library(
   name = 'server',
   srcs = SRCS,
   resources = RESOURCES,
@@ -41,9 +41,9 @@
     '//lib:jsch',
     '//lib:juniversalchardet',
     '//lib:mime-util',
-    '//lib:ow2-asm',
-    '//lib:ow2-asm-tree',
-    '//lib:ow2-asm-util',
+    '//lib/ow2:ow2-asm',
+    '//lib/ow2:ow2-asm-tree',
+    '//lib/ow2:ow2-asm-util',
     '//lib:parboiled-core',
     '//lib:pegdown',
     '//lib:protobuf',
@@ -61,12 +61,14 @@
     '//lib/jgit:jgit-archive',
     '//lib/joda:joda-time',
     '//lib/log:api',
+    '//lib/log:log4j',
     '//lib/prolog:prolog-cafe',
     '//lib/lucene:analyzers-common',
     '//lib/lucene:core',
     '//lib/lucene:query-parser',
   ],
-  compile_deps = [
+  provided_deps = [
+    '//lib:servlet-api-3_1',
     '//lib/bouncycastle:bcprov',
     '//lib/bouncycastle:bcpg',
     '//lib/bouncycastle:bcpkix',
@@ -101,6 +103,16 @@
     '//lib/guice:guice-servlet',
     '//lib/jgit:jgit',
     '//lib/jgit:junit',
+    '//lib/log:impl_log4j',
+    '//lib/log:log4j',
+  ],
+  exported_deps = [
+    '//lib/easymock:easymock',
+    '//lib/powermock:powermock-api-easymock',
+    '//lib/powermock:powermock-api-support',
+    '//lib/powermock:powermock-core',
+    '//lib/powermock:powermock-module-junit4',
+    '//lib/powermock:powermock-module-junit4-common',
   ],
   visibility = ['PUBLIC'],
 )
@@ -188,15 +200,16 @@
     '//gerrit-reviewdb:server',
     '//gerrit-server/src/main/prolog:common',
     '//lib:args4j',
-    '//lib:easymock',
     '//lib:guava',
     '//lib:gwtorm',
     '//lib:junit',
     '//lib/guice:guice',
+    '//lib/guice:guice-assistedinject',
     '//lib/jgit:jgit',
     '//lib/jgit:junit',
     '//lib/joda:joda-time',
     '//lib/prolog:prolog-cafe',
   ],
   source_under_test = [':server'],
+  visibility = ['//tools/eclipse:classpath'],
 )
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 17940c0..b8d45f6 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
@@ -156,7 +156,7 @@
     /** Listeners to receive changes as they happen (limited by visibility
      *  of holder's user). */
     private final Map<ChangeListener, ChangeListenerHolder> listeners =
-      new ConcurrentHashMap<ChangeListener, ChangeListenerHolder>();
+        new ConcurrentHashMap<>();
 
     /** Listeners to receive all changes as they happen. */
     private final DynamicSet<ChangeListener> unrestrictedListeners;
@@ -320,7 +320,7 @@
     public HookResult doRefUpdateHook(final Project project, final String refname,
         final Account uploader, final ObjectId oldId, final ObjectId newId) {
 
-      final List<String> args = new ArrayList<String>();
+      final List<String> args = new ArrayList<>();
       addArg(args, "--project", project.getName());
       addArg(args, "--refname", refname);
       addArg(args, "--uploader", getDisplayName(uploader));
@@ -349,16 +349,19 @@
           final ReviewDb db) throws OrmException {
         final PatchSetCreatedEvent event = new PatchSetCreatedEvent();
         final AccountState uploader = accountCache.get(patchSet.getUploader());
+        final AccountState owner = accountCache.get(change.getOwner());
 
         event.change = eventFactory.asChangeAttribute(change);
         event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
         event.uploader = eventFactory.asAccountAttribute(uploader.getAccount());
         fireEvent(change, event, db);
 
-        final List<String> args = new ArrayList<String>();
+        final List<String> args = new ArrayList<>();
         addArg(args, "--change", event.change.id);
-        addArg(args, "--is-draft", patchSet.isDraft() ? "true" : "false");
+        addArg(args, "--is-draft", String.valueOf(patchSet.isDraft()));
+        addArg(args, "--kind", String.valueOf(event.patchSet.kind));
         addArg(args, "--change-url", event.change.url);
+        addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
         addArg(args, "--project", event.change.project);
         addArg(args, "--branch", event.change.branch);
         addArg(args, "--topic", event.change.topic);
@@ -373,15 +376,17 @@
           final ReviewDb db) throws OrmException {
         final DraftPublishedEvent event = new DraftPublishedEvent();
         final AccountState uploader = accountCache.get(patchSet.getUploader());
+        final AccountState owner = accountCache.get(change.getOwner());
 
         event.change = eventFactory.asChangeAttribute(change);
         event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
         event.uploader = eventFactory.asAccountAttribute(uploader.getAccount());
         fireEvent(change, event, db);
 
-        final List<String> args = new ArrayList<String>();
+        final List<String> args = new ArrayList<>();
         addArg(args, "--change", event.change.id);
         addArg(args, "--change-url", event.change.url);
+        addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
         addArg(args, "--project", event.change.project);
         addArg(args, "--branch", event.change.branch);
         addArg(args, "--topic", event.change.topic);
@@ -396,6 +401,7 @@
           final PatchSet patchSet, final String comment, final Map<String, Short> approvals,
           final ReviewDb db) throws OrmException {
         final CommentAddedEvent event = new CommentAddedEvent();
+        final AccountState owner = accountCache.get(change.getOwner());
 
         event.change = eventFactory.asChangeAttribute(change);
         event.author =  eventFactory.asAccountAttribute(account);
@@ -413,10 +419,11 @@
 
         fireEvent(change, event, db);
 
-        final List<String> args = new ArrayList<String>();
+        final List<String> args = new ArrayList<>();
         addArg(args, "--change", event.change.id);
         addArg(args, "--is-draft", patchSet.isDraft() ? "true" : "false");
         addArg(args, "--change-url", event.change.url);
+        addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
         addArg(args, "--project", event.change.project);
         addArg(args, "--branch", event.change.branch);
         addArg(args, "--topic", event.change.topic);
@@ -436,15 +443,17 @@
     public void doChangeMergedHook(final Change change, final Account account,
           final PatchSet patchSet, final ReviewDb db) throws OrmException {
         final ChangeMergedEvent event = new ChangeMergedEvent();
+        final AccountState owner = accountCache.get(change.getOwner());
 
         event.change = eventFactory.asChangeAttribute(change);
         event.submitter = eventFactory.asAccountAttribute(account);
         event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
         fireEvent(change, event, db);
 
-        final List<String> args = new ArrayList<String>();
+        final List<String> args = new ArrayList<>();
         addArg(args, "--change", event.change.id);
         addArg(args, "--change-url", event.change.url);
+        addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
         addArg(args, "--project", event.change.project);
         addArg(args, "--branch", event.change.branch);
         addArg(args, "--topic", event.change.topic);
@@ -458,6 +467,7 @@
           final PatchSet patchSet, final String reason,
           final ReviewDb db) throws OrmException {
         final MergeFailedEvent event = new MergeFailedEvent();
+        final AccountState owner = accountCache.get(change.getOwner());
 
         event.change = eventFactory.asChangeAttribute(change);
         event.submitter = eventFactory.asAccountAttribute(account);
@@ -465,9 +475,10 @@
         event.reason = reason;
         fireEvent(change, event, db);
 
-        final List<String> args = new ArrayList<String>();
+        final List<String> args = new ArrayList<>();
         addArg(args, "--change", event.change.id);
         addArg(args, "--change-url", event.change.url);
+        addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
         addArg(args, "--project", event.change.project);
         addArg(args, "--branch", event.change.branch);
         addArg(args, "--topic", event.change.topic);
@@ -482,6 +493,7 @@
           final PatchSet patchSet, final String reason, final ReviewDb db)
           throws OrmException {
         final ChangeAbandonedEvent event = new ChangeAbandonedEvent();
+        final AccountState owner = accountCache.get(change.getOwner());
 
         event.change = eventFactory.asChangeAttribute(change);
         event.abandoner = eventFactory.asAccountAttribute(account);
@@ -489,9 +501,10 @@
         event.reason = reason;
         fireEvent(change, event, db);
 
-        final List<String> args = new ArrayList<String>();
+        final List<String> args = new ArrayList<>();
         addArg(args, "--change", event.change.id);
         addArg(args, "--change-url", event.change.url);
+        addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
         addArg(args, "--project", event.change.project);
         addArg(args, "--branch", event.change.branch);
         addArg(args, "--topic", event.change.topic);
@@ -506,6 +519,7 @@
           final PatchSet patchSet, final String reason, final ReviewDb db)
           throws OrmException {
         final ChangeRestoredEvent event = new ChangeRestoredEvent();
+        final AccountState owner = accountCache.get(change.getOwner());
 
         event.change = eventFactory.asChangeAttribute(change);
         event.restorer = eventFactory.asAccountAttribute(account);
@@ -513,9 +527,10 @@
         event.reason = reason;
         fireEvent(change, event, db);
 
-        final List<String> args = new ArrayList<String>();
+        final List<String> args = new ArrayList<>();
         addArg(args, "--change", event.change.id);
         addArg(args, "--change-url", event.change.url);
+        addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
         addArg(args, "--project", event.change.project);
         addArg(args, "--branch", event.change.branch);
         addArg(args, "--topic", event.change.topic);
@@ -539,7 +554,7 @@
       event.refUpdate = eventFactory.asRefUpdateAttribute(oldId, newId, refName);
       fireEvent(refName, event);
 
-      final List<String> args = new ArrayList<String>();
+      final List<String> args = new ArrayList<>();
       addArg(args, "--oldrev", event.refUpdate.oldRev);
       addArg(args, "--newrev", event.refUpdate.newRev);
       addArg(args, "--refname", event.refUpdate.refName);
@@ -554,15 +569,17 @@
     public void doReviewerAddedHook(final Change change, final Account account,
         final PatchSet patchSet, final ReviewDb db) throws OrmException {
       final ReviewerAddedEvent event = new ReviewerAddedEvent();
+      final AccountState owner = accountCache.get(change.getOwner());
 
       event.change = eventFactory.asChangeAttribute(change);
       event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
       event.reviewer = eventFactory.asAccountAttribute(account);
       fireEvent(change, event, db);
 
-      final List<String> args = new ArrayList<String>();
+      final List<String> args = new ArrayList<>();
       addArg(args, "--change", event.change.id);
       addArg(args, "--change-url", event.change.url);
+      addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
       addArg(args, "--project", event.change.project);
       addArg(args, "--branch", event.change.branch);
       addArg(args, "--reviewer", getDisplayName(account));
@@ -574,14 +591,16 @@
         final String oldTopic, final ReviewDb db)
             throws OrmException {
       final TopicChangedEvent event = new TopicChangedEvent();
+      final AccountState owner = accountCache.get(change.getOwner());
 
       event.change = eventFactory.asChangeAttribute(change);
       event.changer = eventFactory.asAccountAttribute(account);
       event.oldTopic = oldTopic;
       fireEvent(change, event, db);
 
-      final List<String> args = new ArrayList<String>();
+      final List<String> args = new ArrayList<>();
       addArg(args, "--change", event.change.id);
+      addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
       addArg(args, "--project", event.change.project);
       addArg(args, "--branch", event.change.branch);
       addArg(args, "--changer", getDisplayName(account));
@@ -593,7 +612,7 @@
 
     public void doClaSignupHook(Account account, ContributorAgreement cla) {
       if (account != null) {
-        final List<String> args = new ArrayList<String>();
+        final List<String> args = new ArrayList<>();
         addArg(args, "--submitter", getDisplayName(account));
         addArg(args, "--user-id", account.getId().toString());
         addArg(args, "--cla-name", cla.getName());
@@ -721,7 +740,7 @@
     }
 
     SyncHookTask syncHook = new SyncHookTask(project, hook, args);
-    FutureTask<HookResult> task = new FutureTask<HookResult>(syncHook);
+    FutureTask<HookResult> task = new FutureTask<>(syncHook);
 
     syncHookThreadPool.execute(task);
 
@@ -781,7 +800,7 @@
       HookResult result = null;
       try {
 
-        final List<String> argv = new ArrayList<String>(1 + args.size());
+        final List<String> argv = new ArrayList<>(1 + args.size());
         argv.add(hook.getAbsolutePath());
         argv.addAll(args);
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/PrologCompiler.java b/gerrit-server/src/main/java/com/google/gerrit/rules/PrologCompiler.java
index 8202ac2..7f6bff4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/PrologCompiler.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/PrologCompiler.java
@@ -154,13 +154,13 @@
     }
 
     DiagnosticCollector<JavaFileObject> diagnostics =
-        new DiagnosticCollector<JavaFileObject>();
+        new DiagnosticCollector<>();
     StandardJavaFileManager fileManager =
         compiler.getStandardFileManager(diagnostics, null, null);
     try {
       Iterable<? extends JavaFileObject> compilationUnits = fileManager
         .getJavaFileObjectsFromFiles(getAllFiles(tempDir, ".java"));
-      ArrayList<String> options = new ArrayList<String>();
+      ArrayList<String> options = new ArrayList<>();
       String classpath = getMyClasspath();
       if (classpath != null) {
         options.add("-classpath");
@@ -270,7 +270,7 @@
   }
 
   private List<File> getAllFiles(File dir, String extension) {
-    ArrayList<File> fileList = new ArrayList<File>();
+    ArrayList<File> fileList = new ArrayList<>();
     getAllFiles(dir, extension, fileList);
     return fileList;
   }
@@ -287,7 +287,7 @@
   }
 
   private List<String> getRelativePaths(File dir, String extension) {
-    ArrayList<String> pathList = new ArrayList<String>();
+    ArrayList<String> pathList = new ArrayList<>();
     getRelativePaths(dir, extension, "", pathList);
     return pathList;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/PrologEnvironment.java b/gerrit-server/src/main/java/com/google/gerrit/rules/PrologEnvironment.java
index 234a0e7b..029a5d7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/PrologEnvironment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/PrologEnvironment.java
@@ -73,8 +73,8 @@
     setMaxArity(MAX_ARITY);
     setEnabled(EnumSet.allOf(Prolog.Feature.class), false);
     args = a;
-    storedValues = new HashMap<StoredValue<Object>, Object>();
-    cleanup = new LinkedList<Runnable>();
+    storedValues = new HashMap<>();
+    cleanup = new LinkedList<>();
   }
 
   public Args getArgs() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/RulesCache.java b/gerrit-server/src/main/java/com/google/gerrit/rules/RulesCache.java
index f263bed..5dea6a2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/RulesCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/RulesCache.java
@@ -80,11 +80,10 @@
   private static final List<String> PACKAGE_LIST = ImmutableList.of(
       Prolog.BUILTIN, "gerrit");
 
-  private final Map<ObjectId, MachineRef> machineCache =
-      new HashMap<ObjectId, MachineRef>();
+  private final Map<ObjectId, MachineRef> machineCache = new HashMap<>();
 
   private final ReferenceQueue<PrologMachineCopy> dead =
-      new ReferenceQueue<PrologMachineCopy>();
+      new ReferenceQueue<>();
 
   private static final class MachineRef extends WeakReference<PrologMachineCopy> {
     final ObjectId key;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValue.java b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValue.java
index e17346b..132360b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValue.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValue.java
@@ -25,12 +25,12 @@
 public class StoredValue<T> {
   /** Construct a new unique key that does not match any other key. */
   public static <T> StoredValue<T> create() {
-    return new StoredValue<T>();
+    return new StoredValue<>();
   }
 
   /** Construct a key based on a Java Class object, useful for singletons. */
   public static <T> StoredValue<T> create(Class<T> clazz) {
-    return new StoredValue<T>(clazz);
+    return new StoredValue<>(clazz);
   }
 
   private final Object key;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
index 14aa2e3..b74eb77 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
@@ -38,6 +38,7 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
@@ -56,6 +57,7 @@
  * database at submit time, or refreshed on demand, as when reading approvals
  * from the notedb.
  */
+@Singleton
 public class ApprovalCopier {
   private final GitRepositoryManager repoManager;
   private final ProjectCache projectCache;
@@ -81,6 +83,11 @@
     db.patchSetApprovals().insert(getForPatchSet(db, ctl, ps));
   }
 
+  Iterable<PatchSetApproval> getForPatchSet(ReviewDb db,
+      ChangeControl ctl, PatchSet.Id psId) throws OrmException {
+    return getForPatchSet(db, ctl, db.patchSets().get(psId));
+  }
+
   private Iterable<PatchSetApproval> getForPatchSet(ReviewDb db,
       ChangeControl ctl, PatchSet ps) throws OrmException {
     ChangeData cd = changeDataFactory.create(db, ctl);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
index b648548..900bbdd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
@@ -32,6 +32,8 @@
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -43,14 +45,18 @@
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.notedb.ReviewerState;
+import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
 import java.sql.Timestamp;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -65,6 +71,7 @@
  * <p>
  * The methods in this class only modify the gwtorm database.
  */
+@Singleton
 public class ApprovalsUtil {
   private static Ordering<PatchSetApproval> SORT_APPROVALS = Ordering.natural()
       .onResultOf(new Function<PatchSetApproval, Timestamp>() {
@@ -90,11 +97,14 @@
   }
 
   private final NotesMigration migration;
+  private final ApprovalCopier copier;
 
   @VisibleForTesting
   @Inject
-  public ApprovalsUtil(NotesMigration migration) {
+  public ApprovalsUtil(NotesMigration migration,
+      ApprovalCopier copier) {
     this.migration = migration;
+    this.copier = copier;
   }
 
   /**
@@ -211,6 +221,52 @@
     return Collections.unmodifiableList(cells);
   }
 
+  public void addApprovals(ReviewDb db, ChangeUpdate update, LabelTypes labelTypes,
+      PatchSet ps, PatchSetInfo info, Change change, ChangeControl changeCtl,
+      Map<String, Short> approvals) throws OrmException {
+    if (!approvals.isEmpty()) {
+      checkApprovals(approvals, labelTypes, change, changeCtl);
+      List<PatchSetApproval> cells = new ArrayList<>(approvals.size());
+      Timestamp ts = TimeUtil.nowTs();
+      for (Map.Entry<String, Short> vote : approvals.entrySet()) {
+        LabelType lt = labelTypes.byLabel(vote.getKey());
+        cells.add(new PatchSetApproval(new PatchSetApproval.Key(
+            ps.getId(),
+            info.getCommitter().getAccount(),
+            lt.getLabelId()),
+            vote.getValue(),
+            ts));
+        update.putApproval(vote.getKey(), vote.getValue());
+      }
+      db.patchSetApprovals().insert(cells);
+    }
+  }
+
+  public static void checkLabel(LabelTypes labelTypes, String name, Short value) {
+    LabelType label = labelTypes.byLabel(name);
+    if (label == null) {
+      throw new IllegalArgumentException(String.format(
+          "label \"%s\" is not a configured label", name));
+    }
+    if (label.getValue(value) == null) {
+      throw new IllegalArgumentException(String.format(
+          "label \"%s\": %d is not a valid value", name, value));
+    }
+  }
+
+  private static void checkApprovals(Map<String, Short> approvals, LabelTypes labelTypes,
+      Change change, ChangeControl changeCtl) {
+    for (Map.Entry<String, Short> vote : approvals.entrySet()) {
+      String name = vote.getKey();
+      Short value = vote.getValue();
+      PermissionRange range = changeCtl.getRange(Permission.forLabel(name));
+      if (range == null || !range.contains(value)) {
+        throw new IllegalArgumentException(String.format(
+            "applying label \"%s\": %d is restricted", name, value));
+      }
+    }
+  }
+
   public ListMultimap<PatchSet.Id, PatchSetApproval> byChange(ReviewDb db,
       ChangeNotes notes) throws OrmException {
     if (!migration.readPatchSetApprovals()) {
@@ -225,23 +281,22 @@
     return notes.load().getApprovals();
   }
 
-  public List<PatchSetApproval> byPatchSet(ReviewDb db, ChangeNotes notes,
+  public Iterable<PatchSetApproval> byPatchSet(ReviewDb db, ChangeControl ctl,
       PatchSet.Id psId) throws OrmException {
     if (!migration.readPatchSetApprovals()) {
       return sortApprovals(db.patchSetApprovals().byPatchSet(psId));
     }
-    return notes.load().getApprovals().get(psId);
+    return copier.getForPatchSet(db, ctl, psId);
   }
 
-  public List<PatchSetApproval> byPatchSetUser(ReviewDb db,
-      ChangeNotes notes, PatchSet.Id psId, Account.Id accountId)
+  public Iterable<PatchSetApproval> byPatchSetUser(ReviewDb db,
+      ChangeControl ctl, PatchSet.Id psId, Account.Id accountId)
       throws OrmException {
     if (!migration.readPatchSetApprovals()) {
       return sortApprovals(
           db.patchSetApprovals().byPatchSetUser(psId, accountId));
     }
-    return ImmutableList.copyOf(
-        filterApprovals(byPatchSet(db, notes, psId), accountId));
+    return filterApprovals(byPatchSet(db, ctl, psId), accountId);
   }
 
   public PatchSetApproval getSubmitter(ReviewDb db, ChangeNotes notes,
@@ -250,7 +305,8 @@
       return null;
     }
     try {
-      return getSubmitter(c, byPatchSet(db, notes, c));
+      // Submit approval is never copied, so bypass expensive byPatchSet call.
+      return getSubmitter(c, byChange(db, notes).get(c));
     } catch (OrmException e) {
       return null;
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeMessagesUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeMessagesUtil.java
new file mode 100644
index 0000000..72fd1a1
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeMessagesUtil.java
@@ -0,0 +1,74 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.annotations.VisibleForTesting;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Utility functions to manipulate ChangeMessages.
+ * <p>
+ * These methods either query for and update ChangeMessages in the NoteDb or
+ * ReviewDb, depending on the state of the NotesMigration.
+ */
+@Singleton
+public class ChangeMessagesUtil {
+  private static List<ChangeMessage> sortChangeMessages(
+      Iterable<ChangeMessage> changeMessage) {
+    return ChangeNotes.MESSAGE_BY_TIME.sortedCopy(changeMessage);
+  }
+
+  private final NotesMigration migration;
+
+  @VisibleForTesting
+  @Inject
+  public ChangeMessagesUtil(NotesMigration migration) {
+    this.migration = migration;
+  }
+
+  public List<ChangeMessage> byChange(ReviewDb db, ChangeNotes notes) throws OrmException {
+    if (!migration.readChangeMessages()) {
+      return
+          sortChangeMessages(db.changeMessages().byChange(notes.getChangeId()));
+    } else {
+      return sortChangeMessages(notes.load().getChangeMessages().values());
+    }
+  }
+
+  public List<ChangeMessage> byPatchSet(ReviewDb db, ChangeNotes notes,
+      PatchSet.Id psId) throws OrmException {
+    if (!migration.readChangeMessages()) {
+      return sortChangeMessages(db.changeMessages().byPatchSet(psId));
+    }
+    return notes.load().getChangeMessages().get(psId);
+  }
+
+  public void addChangeMessage(ReviewDb db, ChangeUpdate update,
+      ChangeMessage changeMessage) throws OrmException {
+    update.setChangeMessage(changeMessage.getMessage());
+    db.changeMessages().insert(Collections.singleton(changeMessage));
+  }
+}
\ No newline at end of file
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 b8d1e47..249bd38 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
@@ -51,6 +51,7 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
@@ -79,6 +80,7 @@
 import java.util.List;
 import java.util.Map;
 
+@Singleton
 public class ChangeUtil {
   /**
    * Epoch for sort key calculations, Tue Sep 30 2008 17:00:00.
@@ -94,6 +96,10 @@
   private static int uuidPrefix;
   private static int uuidSeq;
 
+  private static final int SUBJECT_MAX_LENGTH = 80;
+  private static final String SUBJECT_CROP_APPENDIX = "...";
+  private static final int SUBJECT_CROP_RANGE = 10;
+
   private static final Logger log =
       LoggerFactory.getLogger(ChangeUtil.class);
 
@@ -147,7 +153,7 @@
   public static void insertAncestors(ReviewDb db, PatchSet.Id id, RevCommit src)
       throws OrmException {
     int cnt = src.getParentCount();
-    List<PatchSetAncestor> toInsert = new ArrayList<PatchSetAncestor>(cnt);
+    List<PatchSetAncestor> toInsert = new ArrayList<>(cnt);
     for (int p = 0; p < cnt; p++) {
       PatchSetAncestor a =
           new PatchSetAncestor(new PatchSetAncestor.Id(id, p + 1));
@@ -194,6 +200,20 @@
     return nextPatchSetId(git.getRefDatabase().getRefs(RefDatabase.ALL), id);
   }
 
+  public static String cropSubject(String subject) {
+    if (subject.length() > SUBJECT_MAX_LENGTH) {
+      int maxLength = SUBJECT_MAX_LENGTH - SUBJECT_CROP_APPENDIX.length();
+      for (int cropPosition = maxLength;
+          cropPosition > maxLength - SUBJECT_CROP_RANGE; cropPosition--) {
+        if (Character.isWhitespace(subject.charAt(cropPosition - 1))) {
+          return subject.substring(0, cropPosition) + SUBJECT_CROP_APPENDIX;
+        }
+      }
+      return subject.substring(0, maxLength) + SUBJECT_CROP_APPENDIX;
+    }
+    return subject;
+  }
+
   private final Provider<CurrentUser> userProvider;
   private final CommitValidators.Factory commitValidatorsFactory;
   private final Provider<ReviewDb> db;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/CmdLineParserModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/CmdLineParserModule.java
index 0e86d34..d10366e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/CmdLineParserModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/CmdLineParserModule.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.server.args4j.PatchSetIdHandler;
 import com.google.gerrit.server.args4j.ProjectControlHandler;
 import com.google.gerrit.server.args4j.SocketAddressHandler;
+import com.google.gerrit.server.args4j.TimestampHandler;
 import com.google.gerrit.server.config.FactoryModule;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.util.cli.CmdLineParser;
@@ -35,6 +36,7 @@
 import org.kohsuke.args4j.spi.OptionHandler;
 
 import java.net.SocketAddress;
+import java.sql.Timestamp;
 
 public class CmdLineParserModule extends FactoryModule {
   public CmdLineParserModule() {
@@ -53,6 +55,7 @@
     registerOptionHandler(PatchSet.Id.class, PatchSetIdHandler.class);
     registerOptionHandler(ProjectControl.class, ProjectControlHandler.class);
     registerOptionHandler(SocketAddress.class, SocketAddressHandler.class);
+    registerOptionHandler(Timestamp.class, TimestampHandler.class);
   }
 
   private <T> void registerOptionHandler(Class<T> type,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/MimeUtilFileTypeRegistry.java b/gerrit-server/src/main/java/com/google/gerrit/server/MimeUtilFileTypeRegistry.java
index ff46b00..5263c6b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/MimeUtilFileTypeRegistry.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/MimeUtilFileTypeRegistry.java
@@ -94,7 +94,7 @@
 
   @SuppressWarnings("unchecked")
   public MimeType getMimeType(final String path, final byte[] content) {
-    Set<MimeType> mimeTypes = new HashSet<MimeType>();
+    Set<MimeType> mimeTypes = new HashSet<>();
     if (content != null && content.length > 0) {
       try {
         mimeTypes.addAll(mimeUtil.getMimeTypes(content));
@@ -112,7 +112,7 @@
       return MimeUtil2.UNKNOWN_MIME_TYPE;
     }
 
-    final List<MimeType> types = new ArrayList<MimeType>(mimeTypes);
+    final List<MimeType> types = new ArrayList<>(mimeTypes);
     Collections.sort(types, new Comparator<MimeType>() {
       @Override
       public int compare(MimeType a, MimeType b) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/PatchLineCommentsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/PatchLineCommentsUtil.java
new file mode 100644
index 0000000..4918546
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/PatchLineCommentsUtil.java
@@ -0,0 +1,100 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.annotations.VisibleForTesting;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Utility functions to manipulate PatchLineComments.
+ * <p>
+ * These methods either query for and update PatchLineComments in the NoteDb or
+ * ReviewDb, depending on the state of the NotesMigration.
+ */
+@Singleton
+public class PatchLineCommentsUtil {
+  private final NotesMigration migration;
+
+  @VisibleForTesting
+  @Inject
+  public PatchLineCommentsUtil(NotesMigration migration) {
+    this.migration = migration;
+  }
+
+  public List<PatchLineComment> publishedByChangeFile(ReviewDb db,
+      ChangeNotes notes, Change.Id changeId, String file) throws OrmException {
+    if (!migration.readPublishedComments()) {
+      return db.patchComments().publishedByChangeFile(changeId, file).toList();
+    }
+    notes.load();
+    List<PatchLineComment> commentsOnFile = new ArrayList<PatchLineComment>();
+
+    // We must iterate through all comments to find the ones on this file.
+    addCommentsInFile(commentsOnFile, notes.getBaseComments().values(), file);
+    addCommentsInFile(commentsOnFile, notes.getPatchSetComments().values(),
+        file);
+
+    Collections.sort(commentsOnFile, ChangeNotes.PatchLineCommentComparator);
+    return commentsOnFile;
+  }
+
+  public List<PatchLineComment> publishedByPatchSet(ReviewDb db,
+      ChangeNotes notes, PatchSet.Id psId) throws OrmException {
+    if (!migration.readPublishedComments()) {
+      return db.patchComments().publishedByPatchSet(psId).toList();
+    }
+    notes.load();
+    List<PatchLineComment> commentsOnPs = new ArrayList<PatchLineComment>();
+    commentsOnPs.addAll(notes.getPatchSetComments().get(psId));
+    commentsOnPs.addAll(notes.getBaseComments().get(psId));
+    return commentsOnPs;
+  }
+
+  private static Collection<PatchLineComment> addCommentsInFile(
+      Collection<PatchLineComment> commentsOnFile,
+      Collection<PatchLineComment> allComments,
+      String file) {
+    for (PatchLineComment c : allComments) {
+      String currentFilename = c.getKey().getParentKey().getFileName();
+      if (currentFilename.equals(file)) {
+        commentsOnFile.add(c);
+      }
+    }
+    return commentsOnFile;
+  }
+
+  public void addPublishedComments(ReviewDb db, ChangeUpdate update,
+      Iterable<PatchLineComment> comments) throws OrmException {
+    for (PatchLineComment c : comments) {
+      update.putComment(c);
+    }
+    db.patchComments().upsert(comments);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/RequestCleanup.java b/gerrit-server/src/main/java/com/google/gerrit/server/RequestCleanup.java
index d836646..b8c0888 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/RequestCleanup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/RequestCleanup.java
@@ -32,7 +32,7 @@
   private static final Logger log =
       LoggerFactory.getLogger(RequestCleanup.class);
 
-  private final List<Runnable> cleanup = new LinkedList<Runnable>();
+  private final List<Runnable> cleanup = new LinkedList<>();
   private boolean ran;
 
   /** Register a task to be completed after the request ends. */
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/WebLinks.java b/gerrit-server/src/main/java/com/google/gerrit/server/WebLinks.java
new file mode 100644
index 0000000..fe07100
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/WebLinks.java
@@ -0,0 +1,53 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.collect.Lists;
+import com.google.gerrit.extensions.common.WebLinkInfo;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.webui.PatchSetWebLink;
+import com.google.gerrit.extensions.webui.ProjectWebLink;
+
+import java.util.List;
+
+public class WebLinks {
+
+  private final DynamicSet<PatchSetWebLink> patchSetLinks;
+  private final DynamicSet<ProjectWebLink> projectLinks;
+
+  public WebLinks(DynamicSet<PatchSetWebLink> patchSetLinks,
+      DynamicSet<ProjectWebLink> projectLinks) {
+    this.patchSetLinks = patchSetLinks;
+    this.projectLinks = projectLinks;
+  }
+
+  public Iterable<WebLinkInfo> getPatchSetLinks(String project, String commit) {
+    List<WebLinkInfo> links = Lists.newArrayList();
+    for (PatchSetWebLink webLink : patchSetLinks) {
+      links.add(new WebLinkInfo(webLink.getLinkName(),
+          webLink.getPatchSetUrl(project, commit)));
+    }
+    return links;
+  }
+
+  public Iterable<WebLinkInfo> getProjectLinks(String project) {
+    List<WebLinkInfo> links = Lists.newArrayList();
+    for (ProjectWebLink webLink : projectLinks) {
+      links.add(new WebLinkInfo(webLink.getLinkName(),
+          webLink.getProjectUrl(project)));
+    }
+    return links;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/WebLinksProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/WebLinksProvider.java
new file mode 100644
index 0000000..e3ffa62
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/WebLinksProvider.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server;
+
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.webui.PatchSetWebLink;
+import com.google.gerrit.extensions.webui.ProjectWebLink;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class WebLinksProvider implements Provider<WebLinks> {
+
+  private final DynamicSet<PatchSetWebLink> patchSetLinks;
+  private final DynamicSet<ProjectWebLink> projectLinks;
+
+  @Inject
+  public WebLinksProvider(DynamicSet<PatchSetWebLink> patchSetLinks,
+      DynamicSet<ProjectWebLink> projectLinks) {
+    this.patchSetLinks = patchSetLinks;
+    this.projectLinks = projectLinks;
+  }
+
+  @Override
+  public WebLinks get() {
+    WebLinks webLinks = new WebLinks(patchSetLinks, projectLinks);
+    return webLinks;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/access/AccessCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/access/AccessCollection.java
index 58f93d8..aa04b33 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/access/AccessCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/access/AccessCollection.java
@@ -22,7 +22,9 @@
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
+@Singleton
 public class AccessCollection implements
     RestCollection<TopLevelResource, AccessResource> {
   private final Provider<ListAccess> list;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/access/ListAccess.java b/gerrit-server/src/main/java/com/google/gerrit/server/access/ListAccess.java
index 3a0a27d..5d651ae 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/access/ListAccess.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/access/ListAccess.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.data.RefConfigSection;
 import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
@@ -41,7 +42,6 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.ProjectJson;
-import com.google.gerrit.server.project.ProjectJson.ProjectInfo;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.RefControl;
 import com.google.inject.Inject;
@@ -157,8 +157,7 @@
           pc.controlForRef(RefNames.REFS_CONFIG);
       local = Maps.newHashMap();
       ownerOf = Sets.newHashSet();
-      Map<AccountGroup.UUID, Boolean> visibleGroups =
-          new HashMap<AccountGroup.UUID, Boolean>();
+      Map<AccountGroup.UUID, Boolean> visibleGroups = new HashMap<>();
 
       for (AccessSection section : config.getAccessSections()) {
         String name = section.getName();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
index 65b166b..a521840 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
@@ -163,7 +163,7 @@
           Collections.unmodifiableCollection(db.accountExternalIds().byAccount(
               who).toList());
 
-      Set<AccountGroup.UUID> internalGroups = new HashSet<AccountGroup.UUID>();
+      Set<AccountGroup.UUID> internalGroups = new HashSet<>();
       for (AccountGroupMember g : db.accountGroupMembers().byAccount(who)) {
         final AccountGroup.Id groupId = g.getAccountGroupId();
         final AccountGroup group = groupCache.get(groupId);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountInfoCacheFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountInfoCacheFactory.java
index e3cf023..32781f0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountInfoCacheFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountInfoCacheFactory.java
@@ -36,7 +36,7 @@
   @Inject
   AccountInfoCacheFactory(final AccountCache accountCache) {
     this.accountCache = accountCache;
-    this.out = new HashMap<Account.Id, Account>();
+    this.out = new HashMap<>();
   }
 
   /**
@@ -66,7 +66,7 @@
    * Create an AccountInfoCache with the currently loaded Account entities.
    * */
   public AccountInfoCache create() {
-    final List<AccountInfo> r = new ArrayList<AccountInfo>(out.size());
+    final List<AccountInfo> r = new ArrayList<>(out.size());
     for (final Account a : out.values()) {
       r.add(new AccountInfo(a));
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
index f068812..77ebe0f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.gerrit.common.auth.openid.OpenIdUrls;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.Permission;
@@ -27,7 +26,6 @@
 import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
@@ -38,9 +36,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.util.ArrayList;
 import java.util.Collections;
-import java.util.List;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 /** Tracks authentication related details for user accounts. */
@@ -52,7 +48,6 @@
   private final SchemaFactory<ReviewDb> schema;
   private final AccountCache byIdCache;
   private final AccountByEmailCache byEmailCache;
-  private final AuthConfig authConfig;
   private final Realm realm;
   private final IdentifiedUser.GenericFactory userFactory;
   private final ChangeUserName.Factory changeUserNameFactory;
@@ -62,14 +57,13 @@
   @Inject
   AccountManager(final SchemaFactory<ReviewDb> schema,
       final AccountCache byIdCache, final AccountByEmailCache byEmailCache,
-      final AuthConfig authConfig, final Realm accountMapper,
+      final Realm accountMapper,
       final IdentifiedUser.GenericFactory userFactory,
       final ChangeUserName.Factory changeUserNameFactory,
       final ProjectCache projectCache) throws OrmException {
     this.schema = schema;
     this.byIdCache = byIdCache;
     this.byEmailCache = byEmailCache;
-    this.authConfig = authConfig;
     this.realm = accountMapper;
     this.userFactory = userFactory;
     this.changeUserNameFactory = changeUserNameFactory;
@@ -197,67 +191,6 @@
 
   private AuthResult create(final ReviewDb db, final AuthRequest who)
       throws OrmException, AccountException {
-    if (authConfig.isAllowGoogleAccountUpgrade()
-        && who.isScheme(OpenIdUrls.URL_GOOGLE + "?")
-        && who.getEmailAddress() != null) {
-      final List<AccountExternalId> openId = new ArrayList<AccountExternalId>();
-      final List<AccountExternalId> v1 = new ArrayList<AccountExternalId>();
-
-      for (final AccountExternalId extId : db.accountExternalIds()
-          .byEmailAddress(who.getEmailAddress())) {
-        if (extId.isScheme(OpenIdUrls.URL_GOOGLE + "?")) {
-          openId.add(extId);
-        } else if (extId.isScheme(AccountExternalId.LEGACY_GAE)) {
-          v1.add(extId);
-        }
-      }
-
-      if (!openId.isEmpty()) {
-        // The user has already registered with an OpenID from Google, but
-        // Google may have changed the user's OpenID identity if this server
-        // name has changed. Insert a new identity for the user.
-        //
-        final Account.Id accountId = openId.get(0).getAccountId();
-
-        if (openId.size() > 1) {
-          // Validate all matching identities are actually the same user.
-          //
-          for (final AccountExternalId extId : openId) {
-            if (!accountId.equals(extId.getAccountId())) {
-              throw new AccountException("Multiple user accounts for "
-                  + who.getEmailAddress() + " using Google Accounts provider");
-            }
-          }
-        }
-
-        final AccountExternalId newId = createId(accountId, who);
-        newId.setEmailAddress(who.getEmailAddress());
-
-        db.accountExternalIds().upsert(Collections.singleton(newId));
-        if (openId.size() == 1) {
-          final AccountExternalId oldId = openId.get(0);
-          db.accountExternalIds().delete(Collections.singleton(oldId));
-        }
-        return new AuthResult(accountId, newId.getKey(), false);
-
-      } else if (v1.size() == 1) {
-        // Exactly one user was imported from Gerrit 1.x with this email
-        // address. Upgrade their account by deleting the legacy import
-        // identity and creating a new identity matching the token we have.
-        //
-        final AccountExternalId oldId = v1.get(0);
-        final AccountExternalId newId = createId(oldId.getAccountId(), who);
-        newId.setEmailAddress(who.getEmailAddress());
-
-        db.accountExternalIds().upsert(Collections.singleton(newId));
-        db.accountExternalIds().delete(Collections.singleton(oldId));
-        return new AuthResult(newId.getAccountId(), newId.getKey(), false);
-
-      } else if (v1.size() > 1) {
-        throw new AccountException("Multiple Gerrit 1.x accounts found");
-      }
-    }
-
     final Account.Id newId = new Account.Id(db.nextAccountId());
     final Account account = new Account(newId, TimeUtil.nowTs());
     final AccountExternalId extId = createId(newId, who);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
index 383ed05..558f8c0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
@@ -21,6 +21,7 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import java.util.Collections;
 import java.util.HashSet;
@@ -29,6 +30,7 @@
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+@Singleton
 public class AccountResolver {
   private final Realm realm;
   private final AccountByEmailCache byEmail;
@@ -179,7 +181,7 @@
     // At this point we have no clue. Just perform a whole bunch of suggestions
     // and pray we come up with a reasonable result list.
     //
-    Set<Account.Id> result = new HashSet<Account.Id>();
+    Set<Account.Id> result = new HashSet<>();
     String a = nameOrEmail;
     String b = nameOrEmail + "\u9fa5";
     for (Account act : schema.get().accounts().suggestByFullName(a, b, 10)) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java
index 488370e..66607e2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java
@@ -74,7 +74,7 @@
    * validated by Gerrit directly.
    */
   public Set<String> getEmailAddresses() {
-    final Set<String> emails = new HashSet<String>();
+    final Set<String> emails = new HashSet<>();
     for (final AccountExternalId e : externalIds) {
       if (e.getEmailAddress() != null && !e.getEmailAddress().isEmpty()) {
         emails.add(e.getEmailAddress());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
index 213ecd1..5ef745b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
@@ -30,7 +30,9 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
+@Singleton
 public class AccountsCollection implements
     RestCollection<TopLevelResource, AccountResource>,
     AcceptsCreate<TopLevelResource>{
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java
index 4d73fe1..3c21d17 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java
@@ -16,6 +16,7 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.common.collect.Iterables;
 import com.google.common.io.ByteSource;
 import com.google.gerrit.common.errors.InvalidSshKeyException;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -31,13 +32,16 @@
 import com.google.gerrit.server.account.GetSshKeys.SshKeyInfo;
 import com.google.gerrit.server.ssh.SshKeyCache;
 import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.Collections;
 
+@Singleton
 public class AddSshKey implements RestModifyView<AccountResource, Input> {
   public static class Input {
     public RawInput raw;
@@ -74,11 +78,10 @@
       throw new BadRequestException("SSH public key missing");
     }
 
-    int max = 0;
-    for (AccountSshKey k : dbProvider.get().accountSshKeys()
-        .byAccount(user.getAccountId())) {
-      max = Math.max(max, k.getKey().get());
-    }
+    ResultSet<AccountSshKey> byAccountLast =
+        dbProvider.get().accountSshKeys().byAccountLast(user.getAccountId());
+    AccountSshKey last = Iterables.getOnlyElement(byAccountLast, null);
+    int max = last == null ? 0 : last.getKey().get();
 
     final RawInput rawKey = input.raw;
     String sshPublicKey = new ByteSource() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Capabilities.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Capabilities.java
index 38e5013..95338fe 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Capabilities.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Capabilities.java
@@ -25,7 +25,9 @@
 import com.google.gerrit.server.account.AccountResource.Capability;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
+@Singleton
 class Capabilities implements
     ChildCollection<AccountResource, AccountResource.Capability> {
   private final Provider<CurrentUser> self;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityCollection.java
index 0247dbe..f60c794 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityCollection.java
@@ -43,8 +43,7 @@
       section = new AccessSection(AccessSection.GLOBAL_CAPABILITIES);
     }
 
-    Map<String, List<PermissionRule>> tmp =
-        new HashMap<String, List<PermissionRule>>();
+    Map<String, List<PermissionRule>> tmp = new HashMap<>();
     for (Permission permission : section.getPermissions()) {
       for (PermissionRule rule : permission.getRules()) {
         if (!permission.getName().equals(GlobalCapability.EMAIL_REVIEWERS)
@@ -54,7 +53,7 @@
 
         List<PermissionRule> r = tmp.get(permission.getName());
         if (r == null) {
-          r = new ArrayList<PermissionRule>(2);
+          r = new ArrayList<>(2);
           tmp.put(permission.getName(), r);
         }
         r.add(rule);
@@ -62,8 +61,7 @@
     }
     configureDefaults(tmp, section);
 
-    Map<String, List<PermissionRule>> res =
-        new HashMap<String, List<PermissionRule>>();
+    Map<String, List<PermissionRule>> res = new HashMap<>();
     for (Map.Entry<String, List<PermissionRule>> e : tmp.entrySet()) {
       List<PermissionRule> rules = e.getValue();
       if (rules.size() == 1) {
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 7556173..f0f22b5 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
@@ -54,7 +54,7 @@
   CapabilityControl(ProjectCache projectCache, @Assisted CurrentUser currentUser) {
     capabilities = projectCache.getAllProjects().getCapabilityCollection();
     user = currentUser;
-    effective = new HashMap<String, List<PermissionRule>>();
+    effective = new HashMap<>();
   }
 
   /** Identity of the user the control will compute for. */
@@ -274,7 +274,7 @@
       return rules;
     }
 
-    List<PermissionRule> mine = new ArrayList<PermissionRule>(rules.size());
+    List<PermissionRule> mine = new ArrayList<>(rules.size());
     for (PermissionRule rule : rules) {
       if (match(groups, rule)) {
         mine.add(rule);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java
index 1210906..6dd51e1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java
@@ -141,7 +141,7 @@
   }
 
   private Collection<AccountExternalId> old() throws OrmException {
-    final Collection<AccountExternalId> r = new ArrayList<AccountExternalId>(1);
+    final Collection<AccountExternalId> r = new ArrayList<>(1);
     for (AccountExternalId i : db.accountExternalIds().byAccount(
         user.getAccountId())) {
       if (i.isScheme(SCHEME_USERNAME)) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java
index 60c448c..4be8067 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java
@@ -59,7 +59,7 @@
   private final AuthConfig authConfig;
   private final AccountManager accountManager;
   private final RegisterNewEmailSender.Factory registerNewEmailFactory;
-  private final Provider<PutPreferred> putPreferredProvider;
+  private final PutPreferred putPreferred;
   private final String email;
 
   @Inject
@@ -68,14 +68,14 @@
       AuthConfig authConfig,
       AccountManager accountManager,
       RegisterNewEmailSender.Factory registerNewEmailFactory,
-      Provider<PutPreferred> putPreferredProvider,
+      PutPreferred putPreferred,
       @Assisted String email) {
     this.self = self;
     this.realm = realm;
     this.authConfig = authConfig;
     this.accountManager = accountManager;
     this.registerNewEmailFactory = registerNewEmailFactory;
-    this.putPreferredProvider = putPreferredProvider;
+    this.putPreferred = putPreferred;
     this.email = email;
   }
 
@@ -128,7 +128,7 @@
         throw new ResourceConflictException(e.getMessage());
       }
       if (input.preferred) {
-        putPreferredProvider.get().apply(
+        putPreferred.apply(
             new AccountResource.Email(user, email),
             null);
         info.preferred = true;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
index d9fb303..938d940 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
@@ -20,9 +20,11 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
 import java.util.Set;
 
+@Singleton
 public class DefaultRealm implements Realm {
   private final EmailExpander emailExpander;
   private final AccountByEmailCache byEmail;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java
index 4382655..52ab651 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java
@@ -25,10 +25,12 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import java.util.Collections;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@Singleton
 public class DeleteActive implements RestModifyView<AccountResource, Input> {
   public static class Input {
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java
index 5e0597b..6048586 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java
@@ -29,7 +29,9 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
+@Singleton
 public class DeleteEmail implements RestModifyView<AccountResource.Email, Input> {
   public static class Input {
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteSshKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteSshKey.java
index bbba48a..7df1848 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteSshKey.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteSshKey.java
@@ -22,9 +22,11 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import java.util.Collections;
 
+@Singleton
 public class DeleteSshKey implements
     RestModifyView<AccountResource.SshKey, Input> {
   public static class Input {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Emails.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Emails.java
index f523e15..a178562 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Emails.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Emails.java
@@ -25,18 +25,20 @@
 import com.google.gerrit.server.account.AccountResource.Email;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
+@Singleton
 public class Emails implements
     ChildCollection<AccountResource, AccountResource.Email>,
     AcceptsCreate<AccountResource> {
   private final DynamicMap<RestView<AccountResource.Email>> views;
-  private final Provider<GetEmails> list;
+  private final GetEmails list;
   private final Provider<CurrentUser> self;
   private final CreateEmail.Factory createEmailFactory;
 
   @Inject
   Emails(DynamicMap<RestView<AccountResource.Email>> views,
-      Provider<GetEmails> list,
+      GetEmails list,
       Provider<CurrentUser> self,
       CreateEmail.Factory createEmailFactory) {
     this.views = views;
@@ -47,7 +49,7 @@
 
   @Override
   public RestView<AccountResource> list() {
-    return list.get();
+    return list;
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAccount.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAccount.java
index f990b5b..200595f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAccount.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAccount.java
@@ -17,7 +17,9 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
+@Singleton
 public class GetAccount implements RestReadView<AccountResource> {
   private final AccountInfo.Loader.Factory infoFactory;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetActive.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetActive.java
index c042e18..10b6df9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetActive.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetActive.java
@@ -17,7 +17,9 @@
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.inject.Singleton;
 
+@Singleton
 public class GetActive implements RestReadView<AccountResource> {
   @Override
   public Object apply(AccountResource rsrc) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAvatarChangeUrl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAvatarChangeUrl.java
index ec538bc..ccff183 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAvatarChangeUrl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAvatarChangeUrl.java
@@ -20,7 +20,9 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.avatar.AvatarProvider;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
+@Singleton
 public class GetAvatarChangeUrl implements RestReadView<AccountResource> {
   private final DynamicItem<AvatarProvider> avatarProvider;
 
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 465ddab..47047ed 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
@@ -48,6 +48,7 @@
 import com.google.gson.reflect.TypeToken;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import org.kohsuke.args4j.Option;
 
@@ -178,6 +179,7 @@
     }
   }
 
+  @Singleton
   static class CheckOne implements RestReadView<AccountResource.Capability> {
     @Override
     public BinaryResult apply(Capability resource) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDiffPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDiffPreferences.java
index ccb0418..5959fac 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDiffPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDiffPreferences.java
@@ -25,7 +25,9 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
+@Singleton
 public class GetDiffPreferences implements RestReadView<AccountResource> {
   private final Provider<CurrentUser> self;
   private final Provider<ReviewDb> db;
@@ -72,6 +74,7 @@
       info.syntaxHighlighting = p.isSyntaxHighlighting() ? true : null;
       info.tabSize = p.getTabSize();
       info.renderEntireFile = p.isRenderEntireFile() ? true : null;
+      info.hideEmptyPane = p.isHideEmptyPane() ? true : null;
       info.theme = p.getTheme();
       return info;
     }
@@ -92,6 +95,7 @@
     public Boolean hideTopMenu;
     public Boolean hideLineNumbers;
     public Boolean renderEntireFile;
+    public Boolean hideEmptyPane;
     public int tabSize;
     public Theme theme;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEmail.java
index c56a0a0..a4a6bd0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEmail.java
@@ -16,7 +16,9 @@
 
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.account.GetEmails.EmailInfo;
+import com.google.inject.Singleton;
 
+@Singleton
 public class GetEmail implements RestReadView<AccountResource.Email> {
   @Override
   public EmailInfo apply(AccountResource.Email rsrc) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEmails.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEmails.java
index 0e0e77a..64991e6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEmails.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEmails.java
@@ -21,11 +21,13 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
 
+@Singleton
 public class GetEmails implements RestReadView<AccountResource> {
   private final Provider<CurrentUser> self;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetGroups.java
index 97e4e70..d335add 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetGroups.java
@@ -24,9 +24,11 @@
 import com.google.gerrit.server.group.GroupJson.GroupInfo;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
 import java.util.List;
 
+@Singleton
 public class GetGroups implements RestReadView<AccountResource> {
   private final GroupControl.Factory groupControlFactory;
   private final GroupJson json;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetHttpPassword.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetHttpPassword.java
index 7fc82f9..c49ab98 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetHttpPassword.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetHttpPassword.java
@@ -20,7 +20,9 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
+@Singleton
 public class GetHttpPassword implements RestReadView<AccountResource> {
 
   private final Provider<CurrentUser> self;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetName.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetName.java
index 646a3b2..7add77a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetName.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetName.java
@@ -16,7 +16,9 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.inject.Singleton;
 
+@Singleton
 public class GetName implements RestReadView<AccountResource> {
   @Override
   public String apply(AccountResource rsrc) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetPreferences.java
index 8eb6947..ccc6e48 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetPreferences.java
@@ -14,13 +14,16 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.common.base.Strings;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.extensions.webui.TopMenu;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.ChangeScreen;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.CommentVisibilityStrategy;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.ReviewCategoryStrategy;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DateFormat;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DiffView;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
@@ -28,23 +31,54 @@
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.TimeFormat;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Repository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+@Singleton
 public class GetPreferences implements RestReadView<AccountResource> {
+  private static final Logger log = LoggerFactory.getLogger(GetPreferences.class);
+
+  public static final String MY = "my";
+  public static final String KEY_URL = "url";
+  public static final String KEY_TARGET = "target";
+  public static final String KEY_ID = "id";
+
   private final Provider<CurrentUser> self;
   private final Provider<ReviewDb> db;
+  private final AllUsersName allUsersName;
+  private final GitRepositoryManager gitMgr;
 
   @Inject
-  GetPreferences(Provider<CurrentUser> self, Provider<ReviewDb> db) {
+  GetPreferences(Provider<CurrentUser> self, Provider<ReviewDb> db,
+      AllUsersName allUsersName,
+      GitRepositoryManager gitMgr) {
     this.self = self;
     this.db = db;
+    this.allUsersName = allUsersName;
+    this.gitMgr = gitMgr;
   }
 
   @Override
   public PreferenceInfo apply(AccountResource rsrc)
-      throws AuthException, ResourceNotFoundException, OrmException {
+      throws AuthException,
+      ResourceNotFoundException,
+      OrmException,
+      IOException,
+      ConfigInvalidException {
     if (self.get() != rsrc.getUser()
         && !self.get().getCapabilities().canAdministrateServer()) {
       throw new AuthException("restricted to administrator");
@@ -53,13 +87,20 @@
     if (a == null) {
       throw new ResourceNotFoundException();
     }
-    return new PreferenceInfo(a.getGeneralPreferences());
+
+    Repository git = gitMgr.openRepository(allUsersName);
+    try {
+      VersionedAccountPreferences p =
+          VersionedAccountPreferences.forUser(rsrc.getUser().getAccountId());
+      p.load(git);
+      return new PreferenceInfo(a.getGeneralPreferences(), p, git);
+    } finally {
+      git.close();
+    }
   }
 
-  static class PreferenceInfo {
-    final String kind = "gerritcodereview#preferences";
-
-    short changesPerPage;
+  public static class PreferenceInfo {
+    Short changesPerPage;
     Boolean showSiteHeader;
     Boolean useFlashClipboard;
     DownloadScheme downloadScheme;
@@ -68,29 +109,79 @@
     DateFormat dateFormat;
     TimeFormat timeFormat;
     Boolean reversePatchSetOrder;
-    Boolean showUsernameInReviewCategory;
     Boolean relativeDateInChangeTable;
     Boolean sizeBarInChangeTable;
+    Boolean legacycidInChangeTable;
+    ReviewCategoryStrategy reviewCategoryStrategy;
     CommentVisibilityStrategy commentVisibilityStrategy;
     DiffView diffView;
     ChangeScreen changeScreen;
+    List<TopMenu.MenuItem> my;
 
-    PreferenceInfo(AccountGeneralPreferences p) {
-      changesPerPage = p.getMaximumPageSize();
-      showSiteHeader = p.isShowSiteHeader() ? true : null;
-      useFlashClipboard = p.isUseFlashClipboard() ? true : null;
-      downloadScheme = p.getDownloadUrl();
-      downloadCommand = p.getDownloadCommand();
-      copySelfOnEmail = p.isCopySelfOnEmails() ? true : null;
-      dateFormat = p.getDateFormat();
-      timeFormat = p.getTimeFormat();
-      reversePatchSetOrder = p.isReversePatchSetOrder() ? true : null;
-      showUsernameInReviewCategory = p.isShowUsernameInReviewCategory() ? true : null;
-      relativeDateInChangeTable = p.isRelativeDateInChangeTable() ? true : null;
-      sizeBarInChangeTable = p.isSizeBarInChangeTable() ? true : null;
-      commentVisibilityStrategy = p.getCommentVisibilityStrategy();
-      diffView = p.getDiffView();
-      changeScreen = p.getChangeScreen();
+    public PreferenceInfo(AccountGeneralPreferences p,
+        VersionedAccountPreferences v, Repository allUsers) {
+      if (p != null) {
+        changesPerPage = p.getMaximumPageSize();
+        showSiteHeader = p.isShowSiteHeader() ? true : null;
+        useFlashClipboard = p.isUseFlashClipboard() ? true : null;
+        downloadScheme = p.getDownloadUrl();
+        downloadCommand = p.getDownloadCommand();
+        copySelfOnEmail = p.isCopySelfOnEmails() ? true : null;
+        dateFormat = p.getDateFormat();
+        timeFormat = p.getTimeFormat();
+        reversePatchSetOrder = p.isReversePatchSetOrder() ? true : null;
+        relativeDateInChangeTable = p.isRelativeDateInChangeTable() ? true : null;
+        sizeBarInChangeTable = p.isSizeBarInChangeTable() ? true : null;
+        legacycidInChangeTable = p.isLegacycidInChangeTable() ? true : null;
+        reviewCategoryStrategy = p.getReviewCategoryStrategy();
+        commentVisibilityStrategy = p.getCommentVisibilityStrategy();
+        diffView = p.getDiffView();
+        changeScreen = p.getChangeScreen();
+      }
+      my = my(v, allUsers);
+    }
+
+    private List<TopMenu.MenuItem> my(VersionedAccountPreferences v,
+        Repository allUsers) {
+      List<TopMenu.MenuItem> my = my(v);
+      if (my.isEmpty() && !v.isDefaults()) {
+        try {
+          VersionedAccountPreferences d = VersionedAccountPreferences.forDefault();
+          d.load(allUsers);
+          my = my(d);
+        } catch (ConfigInvalidException | IOException e) {
+          log.warn("cannot read default preferences", e);
+        }
+      }
+      if (my.isEmpty()) {
+        my.add(new TopMenu.MenuItem("Changes", "#/dashboard/self", null));
+        my.add(new TopMenu.MenuItem("Drafts", "#/q/is:draft", null));
+        my.add(new TopMenu.MenuItem("Draft Comments", "#/q/has:draft", null));
+        my.add(new TopMenu.MenuItem("Watched Changes", "#/q/is:watched+is:open", null));
+        my.add(new TopMenu.MenuItem("Starred Changes", "#/q/is:starred", null));
+        my.add(new TopMenu.MenuItem("Groups", "#/groups/self", null));
+      }
+      return my;
+    }
+
+    private List<TopMenu.MenuItem> my(VersionedAccountPreferences v) {
+      List<TopMenu.MenuItem> my = new ArrayList<>();
+      Config cfg = v.getConfig();
+      for (String subsection : cfg.getSubsections(MY)) {
+        String url = my(cfg, subsection, KEY_URL, "#/");
+        String target = my(cfg, subsection, KEY_TARGET,
+            url.startsWith("#") ? null : "_blank");
+        my.add(new TopMenu.MenuItem(
+            subsection, url, target,
+            my(cfg, subsection, KEY_ID, null)));
+      }
+      return my;
+    }
+
+    private static String my(Config cfg, String subsection, String key,
+        String defaultValue) {
+      String val = cfg.getString(MY, subsection, key);
+      return !Strings.isNullOrEmpty(val) ? val : defaultValue;
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKey.java
index 37445e9..a7700cf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKey.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKey.java
@@ -17,7 +17,9 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.account.AccountResource.SshKey;
 import com.google.gerrit.server.account.GetSshKeys.SshKeyInfo;
+import com.google.inject.Singleton;
 
+@Singleton
 public class GetSshKey implements RestReadView<AccountResource.SshKey> {
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKeys.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKeys.java
index 8c878d75..9266c3a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKeys.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKeys.java
@@ -25,9 +25,11 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import java.util.List;
 
+@Singleton
 public class GetSshKeys implements RestReadView<AccountResource> {
 
   private final Provider<CurrentUser> self;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetUsername.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetUsername.java
index 8dcb236..41622cf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetUsername.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetUsername.java
@@ -20,7 +20,9 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
+@Singleton
 public class GetUsername implements RestReadView<AccountResource> {
 
   private final Provider<CurrentUser> self;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java
index 9b4f4df..889addf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java
@@ -83,7 +83,7 @@
   }
 
   private List<AccountGroupMember> loadMembers() throws OrmException {
-    List<AccountGroupMember> members = new ArrayList<AccountGroupMember>();
+    List<AccountGroupMember> members = new ArrayList<>();
     for (final AccountGroupMember m : db.accountGroupMembers().byGroup(groupId)) {
       if (control.canSeeMember(m.getAccountId())) {
         aic.want(m.getAccountId());
@@ -117,7 +117,7 @@
   }
 
   private List<AccountGroupById> loadIncludes() throws OrmException {
-    List<AccountGroupById> groups = new ArrayList<AccountGroupById>();
+    List<AccountGroupById> groups = new ArrayList<>();
 
     for (final AccountGroupById m : db.accountGroupById().byGroup(groupId)) {
       if (control.canSeeGroup(m.getIncludeUUID())) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCache.java
index d130243..6ba6bf0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCache.java
@@ -21,14 +21,14 @@
 /** Tracks group inclusions in memory for efficient access. */
 public interface GroupIncludeCache {
   /** @return groups directly a member of the passed group. */
-  public Set<AccountGroup.UUID> membersOf(AccountGroup.UUID group);
+  public Set<AccountGroup.UUID> subgroupsOf(AccountGroup.UUID group);
 
   /** @return any groups the passed group belongs to. */
-  public Set<AccountGroup.UUID> memberIn(AccountGroup.UUID groupId);
+  public Set<AccountGroup.UUID> parentGroupsOf(AccountGroup.UUID groupId);
 
   /** @return set of any UUIDs that are not internal groups. */
   public Set<AccountGroup.UUID> allExternalMembers();
 
-  public void evictMembersOf(AccountGroup.UUID groupId);
-  public void evictMemberIn(AccountGroup.UUID groupId);
+  public void evictSubgroupsOf(AccountGroup.UUID groupId);
+  public void evictParentGroupsOf(AccountGroup.UUID groupId);
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
index 37d407c..9e7918d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
@@ -42,23 +42,23 @@
 public class GroupIncludeCacheImpl implements GroupIncludeCache {
   private static final Logger log = LoggerFactory
       .getLogger(GroupIncludeCacheImpl.class);
-  private static final String BYINCLUDE_NAME = "groups_byinclude";
-  private static final String MEMBERS_NAME = "groups_members";
+  private static final String PARENT_GROUPS_NAME = "groups_byinclude";
+  private static final String SUBGROUPS_NAME = "groups_members";
   private static final String EXTERNAL_NAME = "groups_external";
 
   public static Module module() {
     return new CacheModule() {
       @Override
       protected void configure() {
-        cache(BYINCLUDE_NAME,
+        cache(PARENT_GROUPS_NAME,
             AccountGroup.UUID.class,
             new TypeLiteral<Set<AccountGroup.UUID>>() {})
-          .loader(MemberInLoader.class);
+          .loader(ParentGroupsLoader.class);
 
-        cache(MEMBERS_NAME,
+        cache(SUBGROUPS_NAME,
             AccountGroup.UUID.class,
             new TypeLiteral<Set<AccountGroup.UUID>>() {})
-          .loader(MembersOfLoader.class);
+          .loader(SubgroupsLoader.class);
 
         cache(EXTERNAL_NAME,
             String.class,
@@ -71,24 +71,24 @@
     };
   }
 
-  private final LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> membersOf;
-  private final LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> memberIn;
+  private final LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> subgroups;
+  private final LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> parentGroups;
   private final LoadingCache<String, Set<AccountGroup.UUID>> external;
 
   @Inject
   GroupIncludeCacheImpl(
-      @Named(MEMBERS_NAME) LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> membersOf,
-      @Named(BYINCLUDE_NAME) LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> memberIn,
+      @Named(SUBGROUPS_NAME) LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> subgroups,
+      @Named(PARENT_GROUPS_NAME) LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> parentGroups,
       @Named(EXTERNAL_NAME) LoadingCache<String, Set<AccountGroup.UUID>> external) {
-    this.membersOf = membersOf;
-    this.memberIn = memberIn;
+    this.subgroups = subgroups;
+    this.parentGroups = parentGroups;
     this.external = external;
   }
 
   @Override
-  public Set<AccountGroup.UUID> membersOf(AccountGroup.UUID groupId) {
+  public Set<AccountGroup.UUID> subgroupsOf(AccountGroup.UUID groupId) {
     try {
-      return membersOf.get(groupId);
+      return subgroups.get(groupId);
     } catch (ExecutionException e) {
       log.warn("Cannot load members of group", e);
       return Collections.emptySet();
@@ -96,9 +96,9 @@
   }
 
   @Override
-  public Set<AccountGroup.UUID> memberIn(AccountGroup.UUID groupId) {
+  public Set<AccountGroup.UUID> parentGroupsOf(AccountGroup.UUID groupId) {
     try {
-      return memberIn.get(groupId);
+      return parentGroups.get(groupId);
     } catch (ExecutionException e) {
       log.warn("Cannot load included groups", e);
       return Collections.emptySet();
@@ -106,16 +106,16 @@
   }
 
   @Override
-  public void evictMembersOf(AccountGroup.UUID groupId) {
+  public void evictSubgroupsOf(AccountGroup.UUID groupId) {
     if (groupId != null) {
-      membersOf.invalidate(groupId);
+      subgroups.invalidate(groupId);
     }
   }
 
   @Override
-  public void evictMemberIn(AccountGroup.UUID groupId) {
+  public void evictParentGroupsOf(AccountGroup.UUID groupId) {
     if (groupId != null) {
-      memberIn.invalidate(groupId);
+      parentGroups.invalidate(groupId);
 
       if (!AccountGroup.isInternalGroup(groupId)) {
         external.invalidate(EXTERNAL_NAME);
@@ -133,12 +133,12 @@
     }
   }
 
-  static class MembersOfLoader extends
+  static class SubgroupsLoader extends
       CacheLoader<AccountGroup.UUID, Set<AccountGroup.UUID>> {
     private final SchemaFactory<ReviewDb> schema;
 
     @Inject
-    MembersOfLoader(final SchemaFactory<ReviewDb> sf) {
+    SubgroupsLoader(final SchemaFactory<ReviewDb> sf) {
       schema = sf;
     }
 
@@ -163,12 +163,12 @@
     }
   }
 
-  static class MemberInLoader extends
+  static class ParentGroupsLoader extends
       CacheLoader<AccountGroup.UUID, Set<AccountGroup.UUID>> {
     private final SchemaFactory<ReviewDb> schema;
 
     @Inject
-    MemberInLoader(final SchemaFactory<ReviewDb> sf) {
+    ParentGroupsLoader(final SchemaFactory<ReviewDb> sf) {
       schema = sf;
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembers.java
index df5b2c1..3fe9d25 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembers.java
@@ -91,7 +91,7 @@
         projectControl.controlFor(project, currentUser).getProjectState()
             .getOwners();
 
-    final HashSet<Account> projectOwners = new HashSet<Account>();
+    final HashSet<Account> projectOwners = new HashSet<>();
     for (final AccountGroup.UUID ownerGroup : ownerGroups) {
       if (!seen.contains(ownerGroup)) {
         projectOwners.addAll(listAccounts(ownerGroup, project, seen));
@@ -107,7 +107,7 @@
     final GroupDetail groupDetail =
         groupDetailFactory.create(group.getId()).call();
 
-    final Set<Account> members = new HashSet<Account>();
+    final Set<Account> members = new HashSet<>();
     if (groupDetail.members != null) {
       for (final AccountGroupMember member : groupDetail.members) {
         members.add(accountCache.get(member.getAccountId()).getAccount());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/IncludingGroupMembership.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/IncludingGroupMembership.java
index 2e278c3..b8a67ff 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/IncludingGroupMembership.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/IncludingGroupMembership.java
@@ -90,7 +90,7 @@
         }
 
         memberOf.put(id, false);
-        if (search(includeCache.membersOf(id))) {
+        if (search(includeCache.subgroupsOf(id))) {
           memberOf.put(id, true);
           return true;
         }
@@ -131,7 +131,7 @@
 
     while (!q.isEmpty()) {
       AccountGroup.UUID id = q.remove(q.size() - 1);
-      for (AccountGroup.UUID g : includeCache.memberIn(id)) {
+      for (AccountGroup.UUID g : includeCache.parentGroupsOf(id)) {
         if (g != null && r.add(g)) {
           q.add(g);
           memberOf.put(g, true);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java
index ca1bf32..86c840b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java
@@ -116,7 +116,7 @@
 
     if (createGroupArgs.initialGroups != null) {
       addGroups(groupId, createGroupArgs.initialGroups);
-      groupIncludeCache.evictMembersOf(uuid);
+      groupIncludeCache.evictSubgroupsOf(uuid);
     }
 
     groupCache.onCreateGroup(createGroupArgs.getGroup());
@@ -126,10 +126,8 @@
 
   private void addMembers(final AccountGroup.Id groupId,
       final Collection<? extends Account.Id> members) throws OrmException {
-    final List<AccountGroupMember> memberships =
-        new ArrayList<AccountGroupMember>();
-    final List<AccountGroupMemberAudit> membershipsAudit =
-        new ArrayList<AccountGroupMemberAudit>();
+    List<AccountGroupMember> memberships = new ArrayList<>();
+    List<AccountGroupMemberAudit> membershipsAudit = new ArrayList<>();
     for (Account.Id accountId : members) {
       final AccountGroupMember membership =
           new AccountGroupMember(new AccountGroupMember.Key(accountId, groupId));
@@ -149,10 +147,8 @@
 
   private void addGroups(final AccountGroup.Id groupId,
       final Collection<? extends AccountGroup.UUID> groups) throws OrmException {
-    final List<AccountGroupById> includeList =
-      new ArrayList<AccountGroupById>();
-    final List<AccountGroupByIdAud> includesAudit =
-      new ArrayList<AccountGroupByIdAud>();
+    List<AccountGroupById> includeList = new ArrayList<>();
+    List<AccountGroupByIdAud> includesAudit = new ArrayList<>();
     for (AccountGroup.UUID includeUUID : groups) {
       final AccountGroupById groupInclude =
         new AccountGroupById(new AccountGroupById.Key(groupId, includeUUID));
@@ -166,7 +162,7 @@
     db.accountGroupByIdAud().insert(includesAudit);
 
     for (AccountGroup.UUID uuid : groups) {
-      groupIncludeCache.evictMemberIn(uuid);
+      groupIncludeCache.evictParentGroupsOf(uuid);
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutAccount.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutAccount.java
index f7584ed..17e177f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutAccount.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutAccount.java
@@ -17,7 +17,9 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.account.CreateAccount.Input;
+import com.google.inject.Singleton;
 
+@Singleton
 public class PutAccount implements RestModifyView<AccountResource, Input> {
   @Override
   public Object apply(AccountResource resource, Input input)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java
index f1b5151..69d16d8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java
@@ -25,10 +25,12 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import java.util.Collections;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@Singleton
 public class PutActive implements RestModifyView<AccountResource, Input> {
   public static class Input {
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutEmail.java
index ba12bbf..3831cbf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutEmail.java
@@ -18,7 +18,9 @@
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.account.CreateEmail.Input;
+import com.google.inject.Singleton;
 
+@Singleton
 public class PutEmail implements RestModifyView<AccountResource.Email, Input> {
   @Override
   public Response<?> apply(AccountResource.Email rsrc, Input input)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java
index c814fb8..63c5420 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java
@@ -30,6 +30,7 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import org.apache.commons.codec.binary.Base64;
 
@@ -37,6 +38,7 @@
 import java.security.SecureRandom;
 import java.util.Collections;
 
+@Singleton
 public class PutHttpPassword implements RestModifyView<AccountResource, Input> {
   public static class Input {
     public String httpPassword;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java
index 87629d8..554bae7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java
@@ -34,9 +34,11 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import java.util.Collections;
 
+@Singleton
 public class PutName implements RestModifyView<AccountResource, Input> {
   public static class Input {
     @DefaultInput
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java
index 8fc2e6c..7ac987d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java
@@ -26,9 +26,11 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import java.util.Collections;
 
+@Singleton
 public class PutPreferred implements
     RestModifyView<AccountResource.Email, Input> {
   static class Input {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java
index 146f241..9b971e4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java
@@ -27,9 +27,11 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import java.util.Collections;
 
+@Singleton
 public class SetDiffPreferences implements RestModifyView<AccountResource, Input> {
   static class Input {
     Short context;
@@ -50,13 +52,14 @@
     Boolean renderEntireFile;
     Integer tabSize;
     Theme theme;
+    Boolean hideEmptyPane;
   }
 
   private final Provider<CurrentUser> self;
-  private final ReviewDb db;
+  private final Provider<ReviewDb> db;
 
   @Inject
-  SetDiffPreferences(Provider<CurrentUser> self, ReviewDb db) {
+  SetDiffPreferences(Provider<CurrentUser> self, Provider<ReviewDb> db) {
     this.self = self;
     this.db = db;
   }
@@ -75,9 +78,9 @@
     Account.Id accountId = rsrc.getUser().getAccountId();
     AccountDiffPreference p;
 
-    db.accounts().beginTransaction(accountId);
+    db.get().accounts().beginTransaction(accountId);
     try {
-      p = db.accountDiffPreferences().get(accountId);
+      p = db.get().accountDiffPreferences().get(accountId);
       if (p == null) {
         p = new AccountDiffPreference(accountId);
       }
@@ -136,11 +139,14 @@
       if (input.theme != null) {
         p.setTheme(input.theme);
       }
+      if (input.hideEmptyPane != null) {
+        p.setHideEmptyPane(input.hideEmptyPane);
+      }
 
-      db.accountDiffPreferences().upsert(Collections.singleton(p));
-      db.commit();
+      db.get().accountDiffPreferences().upsert(Collections.singleton(p));
+      db.get().commit();
     } finally {
-      db.rollback();
+      db.get().rollback();
     }
     return DiffPreferencesInfo.parse(p);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
index d0418eb..c3cc636 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
@@ -14,60 +14,86 @@
 
 package com.google.gerrit.server.account;
 
+import static com.google.gerrit.server.account.GetPreferences.KEY_ID;
+import static com.google.gerrit.server.account.GetPreferences.KEY_TARGET;
+import static com.google.gerrit.server.account.GetPreferences.KEY_URL;
+import static com.google.gerrit.server.account.GetPreferences.MY;
+
+import com.google.common.base.Strings;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.webui.TopMenu;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.ChangeScreen;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.CommentVisibilityStrategy;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.ReviewCategoryStrategy;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DateFormat;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DiffView;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.ChangeScreen;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.TimeFormat;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.SetPreferences.Input;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+
+import java.io.IOException;
 import java.util.Collections;
+import java.util.List;
 
+@Singleton
 public class SetPreferences implements RestModifyView<AccountResource, Input> {
-  static class Input {
-    Short changesPerPage;
-    Boolean showSiteHeader;
-    Boolean useFlashClipboard;
-    DownloadScheme downloadScheme;
-    DownloadCommand downloadCommand;
-    Boolean copySelfOnEmail;
-    DateFormat dateFormat;
-    TimeFormat timeFormat;
-    Boolean reversePatchSetOrder;
-    Boolean showUsernameInReviewCategory;
-    Boolean relativeDateInChangeTable;
-    Boolean sizeBarInChangeTable;
-    CommentVisibilityStrategy commentVisibilityStrategy;
-    DiffView diffView;
-    ChangeScreen changeScreen;
+  public static class Input {
+    public Short changesPerPage;
+    public Boolean showSiteHeader;
+    public Boolean useFlashClipboard;
+    public DownloadScheme downloadScheme;
+    public DownloadCommand downloadCommand;
+    public Boolean copySelfOnEmail;
+    public DateFormat dateFormat;
+    public TimeFormat timeFormat;
+    public Boolean reversePatchSetOrder;
+    public Boolean relativeDateInChangeTable;
+    public Boolean sizeBarInChangeTable;
+    public Boolean legacycidInChangeTable;
+    public CommentVisibilityStrategy commentVisibilityStrategy;
+    public ReviewCategoryStrategy reviewCategoryStrategy;
+    public DiffView diffView;
+    public ChangeScreen changeScreen;
+    public List<TopMenu.MenuItem> my;
   }
 
   private final Provider<CurrentUser> self;
   private final AccountCache cache;
-  private final ReviewDb db;
+  private final Provider<ReviewDb> db;
+  private final MetaDataUpdate.User metaDataUpdateFactory;
+  private final AllUsersName allUsersName;
 
   @Inject
-  SetPreferences(Provider<CurrentUser> self, AccountCache cache, ReviewDb db) {
+  SetPreferences(Provider<CurrentUser> self, AccountCache cache,
+      Provider<ReviewDb> db, MetaDataUpdate.User metaDataUpdateFactory,
+      AllUsersName allUsersName) {
     this.self = self;
     this.cache = cache;
     this.db = db;
+    this.metaDataUpdateFactory = metaDataUpdateFactory;
+    this.allUsersName = allUsersName;
   }
 
   @Override
   public GetPreferences.PreferenceInfo apply(AccountResource rsrc, Input i)
-      throws AuthException, ResourceNotFoundException, OrmException {
+      throws AuthException, ResourceNotFoundException, OrmException,
+      IOException, ConfigInvalidException {
     if (self.get() != rsrc.getUser()
         && !self.get().getCapabilities().canAdministrateServer()) {
       throw new AuthException("restricted to administrator");
@@ -78,13 +104,18 @@
 
     Account.Id accountId = rsrc.getUser().getAccountId();
     AccountGeneralPreferences p;
-    db.accounts().beginTransaction(accountId);
+    VersionedAccountPreferences versionedPrefs;
+    MetaDataUpdate md = metaDataUpdateFactory.create(allUsersName);
+    db.get().accounts().beginTransaction(accountId);
     try {
-      Account a = db.accounts().get(accountId);
+      Account a = db.get().accounts().get(accountId);
       if (a == null) {
         throw new ResourceNotFoundException();
       }
 
+      versionedPrefs = VersionedAccountPreferences.forUser(accountId);
+      versionedPrefs.load(md);
+
       p = a.getGeneralPreferences();
       if (p == null) {
         p = new AccountGeneralPreferences();
@@ -118,15 +149,18 @@
       if (i.reversePatchSetOrder != null) {
         p.setReversePatchSetOrder(i.reversePatchSetOrder);
       }
-      if (i.showUsernameInReviewCategory != null) {
-        p.setShowUsernameInReviewCategory(i.showUsernameInReviewCategory);
-      }
       if (i.relativeDateInChangeTable != null) {
         p.setRelativeDateInChangeTable(i.relativeDateInChangeTable);
       }
       if (i.sizeBarInChangeTable != null) {
         p.setSizeBarInChangeTable(i.sizeBarInChangeTable);
       }
+      if (i.legacycidInChangeTable != null) {
+        p.setLegacycidInChangeTable(i.legacycidInChangeTable);
+      }
+      if (i.reviewCategoryStrategy != null) {
+        p.setReviewCategoryStrategy(i.reviewCategoryStrategy);
+      }
       if (i.commentVisibilityStrategy != null) {
         p.setCommentVisibilityStrategy(i.commentVisibilityStrategy);
       }
@@ -137,12 +171,45 @@
         p.setChangeScreen(i.changeScreen);
       }
 
-      db.accounts().update(Collections.singleton(a));
-      db.commit();
+      db.get().accounts().update(Collections.singleton(a));
+      db.get().commit();
+      storeMyMenus(versionedPrefs, i.my);
+      versionedPrefs.commit(md);
       cache.evict(accountId);
+      return new GetPreferences.PreferenceInfo(
+          p, versionedPrefs,
+          md.getRepository());
     } finally {
-      db.rollback();
+      md.close();
+      db.get().rollback();
     }
-    return new GetPreferences.PreferenceInfo(p);
+  }
+
+  public static void storeMyMenus(VersionedAccountPreferences prefs,
+      List<TopMenu.MenuItem> my) {
+    Config cfg = prefs.getConfig();
+    if (my != null) {
+      unsetSection(cfg, MY);
+      for (TopMenu.MenuItem item : my) {
+        set(cfg, item.name, KEY_URL, item.url);
+        set(cfg, item.name, KEY_TARGET, item.target);
+        set(cfg, item.name, KEY_ID, item.id);
+      }
+    }
+  }
+
+  private static void set(Config cfg, String section, String key, String val) {
+    if (Strings.isNullOrEmpty(val)) {
+      cfg.unset(MY, section, key);
+    } else {
+      cfg.setString(MY, section, key, val);
+    }
+  }
+
+  private static void unsetSection(Config cfg, String section) {
+    cfg.unsetSection(section, null);
+    for (String subsection: cfg.getSubsections(section)) {
+      cfg.unsetSection(section, subsection);
+    }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SshKeys.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SshKeys.java
index 69acd1e..b94158f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SshKeys.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SshKeys.java
@@ -26,17 +26,19 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
+@Singleton
 public class SshKeys implements
     ChildCollection<AccountResource, AccountResource.SshKey> {
   private final DynamicMap<RestView<AccountResource.SshKey>> views;
-  private final Provider<GetSshKeys> list;
+  private final GetSshKeys list;
   private final Provider<CurrentUser> self;
   private final Provider<ReviewDb> dbProvider;
 
   @Inject
   SshKeys(DynamicMap<RestView<AccountResource.SshKey>> views,
-      Provider<GetSshKeys> list, Provider<CurrentUser> self,
+      GetSshKeys list, Provider<CurrentUser> self,
       Provider<ReviewDb> dbProvider) {
     this.views = views;
     this.list = list;
@@ -46,7 +48,7 @@
 
   @Override
   public RestView<AccountResource> list() {
-    return list.get();
+    return list;
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/StarredChanges.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/StarredChanges.java
index b8984ab..a3c0d37 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/StarredChanges.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/StarredChanges.java
@@ -38,13 +38,15 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.util.Collections;
 
-class StarredChanges implements
+@Singleton
+public class StarredChanges implements
     ChildCollection<AccountResource, AccountResource.StarredChange>,
     AcceptsCreate<AccountResource> {
   private static final Logger log = LoggerFactory.getLogger(StarredChanges.class);
@@ -112,7 +114,8 @@
     }
   }
 
-  static class Create implements RestModifyView<AccountResource, EmptyInput> {
+  @Singleton
+  public static class Create implements RestModifyView<AccountResource, EmptyInput> {
     private final Provider<CurrentUser> self;
     private final Provider<ReviewDb> dbProvider;
     private ChangeResource change;
@@ -123,7 +126,7 @@
       this.dbProvider = dbProvider;
     }
 
-    Create setChange(ChangeResource change) {
+    public Create setChange(ChangeResource change) {
       this.change = change;
       return this;
     }
@@ -146,6 +149,7 @@
     }
   }
 
+  @Singleton
   static class Put implements
       RestModifyView<AccountResource.StarredChange, EmptyInput> {
     private final Provider<CurrentUser> self;
@@ -165,7 +169,8 @@
     }
   }
 
-  static class Delete implements
+  @Singleton
+  public static class Delete implements
       RestModifyView<AccountResource.StarredChange, EmptyInput> {
     private final Provider<CurrentUser> self;
     private final Provider<ReviewDb> dbProvider;
@@ -190,6 +195,6 @@
     }
   }
 
-  static class EmptyInput {
+  public static class EmptyInput {
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAccountPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAccountPreferences.java
new file mode 100644
index 0000000..c4d4b06
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAccountPreferences.java
@@ -0,0 +1,75 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.git.VersionedMetaData;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Config;
+
+import java.io.IOException;
+
+/** Preferences for user accounts. */
+public class VersionedAccountPreferences extends VersionedMetaData {
+  private static final String REFS_USER_DEFAULT = RefNames.REFS_USER + "default";
+  private static final String PREFERENCES = "preferences.config";
+
+  public static VersionedAccountPreferences forUser(Account.Id id) {
+    return new VersionedAccountPreferences(RefNames.refsUsers(id));
+  }
+
+  public static VersionedAccountPreferences forDefault() {
+    return new VersionedAccountPreferences(REFS_USER_DEFAULT);
+  }
+
+  private final String ref;
+  private Config cfg;
+
+  private VersionedAccountPreferences(String ref) {
+    this.ref = ref;
+  }
+
+  public boolean isDefaults() {
+    return REFS_USER_DEFAULT.equals(getRefName());
+  }
+
+  @Override
+  protected String getRefName() {
+    return ref;
+  }
+
+  public Config getConfig() {
+    return cfg;
+  }
+
+  @Override
+  protected void onLoad() throws IOException, ConfigInvalidException {
+    cfg = readConfig(PREFERENCES);
+  }
+
+  @Override
+  protected boolean onSave(CommitBuilder commit) throws IOException,
+      ConfigInvalidException {
+    if (Strings.isNullOrEmpty(commit.getMessage())) {
+      commit.setMessage("Updated preferences\n");
+    }
+    saveConfig(PREFERENCES, cfg);
+    return true;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/GerritApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/GerritApiImpl.java
index 33314d3..5f581c3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/GerritApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/GerritApiImpl.java
@@ -15,29 +15,39 @@
 package com.google.gerrit.server.api;
 
 import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.extensions.api.accounts.Accounts;
 import com.google.gerrit.extensions.api.changes.Changes;
 import com.google.gerrit.extensions.api.projects.Projects;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
-class GerritApiImpl implements GerritApi {
-  private final Provider<Changes> changes;
-  private final Provider<Projects> projects;
+@Singleton
+class GerritApiImpl extends GerritApi.NotImplemented implements GerritApi {
+  private final Accounts accounts;
+  private final Changes changes;
+  private final Projects projects;
 
   @Inject
-  GerritApiImpl(Provider<Changes> changes,
-      Provider<Projects> projects) {
+  GerritApiImpl(Accounts accounts,
+      Changes changes,
+      Projects projects) {
+    this.accounts = accounts;
     this.changes = changes;
     this.projects = projects;
   }
 
   @Override
+  public Accounts accounts() {
+    return accounts;
+  }
+
+  @Override
   public Changes changes() {
-    return changes.get();
+    return changes;
   }
 
   @Override
   public Projects projects() {
-    return projects.get();
+    return projects;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/Module.java
index 1919ff5..23f4b8d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/Module.java
@@ -24,5 +24,6 @@
 
     install(new com.google.gerrit.server.api.changes.Module());
     install(new com.google.gerrit.server.api.projects.Module());
+    install(new com.google.gerrit.server.api.accounts.Module());
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
new file mode 100644
index 0000000..b1fd979
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
@@ -0,0 +1,93 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.api.accounts;
+
+import com.google.gerrit.extensions.api.accounts.AccountApi;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.server.account.AccountInfo;
+import com.google.gerrit.server.account.AccountResource;
+import com.google.gerrit.server.account.StarredChanges;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.ChangesCollection;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+public class AccountApiImpl extends AccountApi.NotImplemented implements AccountApi {
+  interface Factory {
+    AccountApiImpl create(AccountResource account);
+  }
+
+  private final AccountResource account;
+  private final ChangesCollection changes;
+  private final AccountInfo.Loader.Factory accountLoaderFactory;
+  private final StarredChanges.Create starredChangesCreate;
+  private final StarredChanges.Delete starredChangesDelete;
+
+  @Inject
+  AccountApiImpl(AccountInfo.Loader.Factory ailf,
+      ChangesCollection changes,
+      StarredChanges.Create starredChangesCreate,
+      StarredChanges.Delete starredChangesDelete,
+      @Assisted AccountResource account) {
+    this.account = account;
+    this.accountLoaderFactory = ailf;
+    this.changes = changes;
+    this.starredChangesCreate = starredChangesCreate;
+    this.starredChangesDelete = starredChangesDelete;
+  }
+
+  @Override
+  public com.google.gerrit.extensions.common.AccountInfo get()
+      throws RestApiException {
+    AccountInfo.Loader accountLoader = accountLoaderFactory.create(true);
+    try {
+      AccountInfo ai = accountLoader.get(account.getUser().getAccountId());
+      accountLoader.fill();
+      return AccountInfoMapper.fromAcountInfo(ai);
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot parse change", e);
+    }
+  }
+
+  @Override
+  public void starChange(String id) throws RestApiException {
+    try {
+      ChangeResource rsrc = changes.parse(
+        TopLevelResource.INSTANCE,
+        IdString.fromUrl(id));
+      starredChangesCreate.setChange(rsrc);
+      starredChangesCreate.apply(account, new StarredChanges.EmptyInput());
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot star change", e);
+    }
+  }
+
+  @Override
+  public void unstarChange(String id) throws RestApiException {
+    try {
+      ChangeResource rsrc =
+          changes.parse(TopLevelResource.INSTANCE, IdString.fromUrl(id));
+      AccountResource.StarredChange starredChange =
+          new AccountResource.StarredChange(account.getUser(), rsrc);
+      starredChangesDelete.apply(starredChange,
+          new StarredChanges.EmptyInput());
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot unstar change", e);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountInfoMapper.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountInfoMapper.java
new file mode 100644
index 0000000..10a9116
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountInfoMapper.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.api.accounts;
+
+import com.google.gerrit.extensions.common.AccountInfo;
+
+public class AccountInfoMapper {
+  public static AccountInfo fromAcountInfo(
+      com.google.gerrit.server.account.AccountInfo i) {
+    if (i == null) {
+      return null;
+    }
+    AccountInfo ai = new AccountInfo();
+    fromAccount(i, ai);
+    return ai;
+  }
+
+  public static void fromAccount(
+      com.google.gerrit.server.account.AccountInfo i, AccountInfo ai) {
+    ai._accountId = i._accountId;
+    ai.email = i.email;
+    ai.name = i.name;
+    ai.username = i.username;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountsImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountsImpl.java
new file mode 100644
index 0000000..0c02c99
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountsImpl.java
@@ -0,0 +1,64 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.api.accounts;
+
+import com.google.gerrit.extensions.api.accounts.AccountApi;
+import com.google.gerrit.extensions.api.accounts.Accounts;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountResource;
+import com.google.gerrit.server.account.AccountsCollection;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+@Singleton
+public class AccountsImpl extends Accounts.NotImplemented implements Accounts {
+  private final AccountsCollection accounts;
+  private final AccountApiImpl.Factory api;
+  private final Provider<CurrentUser> self;
+
+  @Inject
+  AccountsImpl(AccountsCollection accounts,
+      AccountApiImpl.Factory api,
+      Provider<CurrentUser> self) {
+    this.accounts = accounts;
+    this.api = api;
+    this.self = self;
+  }
+
+  @Override
+  public AccountApi id(String id) throws RestApiException {
+    try {
+      return api.create(accounts.parse(TopLevelResource.INSTANCE,
+          IdString.fromDecoded(id)));
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot parse change", e);
+    }
+  }
+
+  @Override
+  public AccountApi self() throws RestApiException {
+    if (!self.get().isIdentifiedUser()) {
+      throw new AuthException("Authentication required");
+    }
+    return api.create(new AccountResource((IdentifiedUser)self.get()));
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/Module.java
new file mode 100644
index 0000000..5e3855e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/Module.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.api.accounts;
+
+import com.google.gerrit.extensions.api.accounts.Accounts;
+import com.google.gerrit.server.config.FactoryModule;
+
+public class Module extends FactoryModule {
+  @Override
+  protected void configure() {
+    bind(Accounts.class).to(AccountsImpl.class);
+
+    factory(AccountApiImpl.Factory.class);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index 0bcfd65..5f90d34 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -41,7 +41,7 @@
 import java.io.IOException;
 import java.util.EnumSet;
 
-class ChangeApiImpl implements ChangeApi {
+class ChangeApiImpl extends ChangeApi.NotImplemented implements ChangeApi {
   interface Factory {
     ChangeApiImpl create(ChangeResource change);
   }
@@ -50,21 +50,21 @@
   private final Revisions revisions;
   private final RevisionApiImpl.Factory revisionApi;
   private final ChangeResource change;
-  private final Provider<Abandon> abandon;
-  private final Provider<Revert> revert;
-  private final Provider<Restore> restore;
+  private final Abandon abandon;
+  private final Revert revert;
+  private final Restore restore;
   private final Provider<PostReviewers> postReviewers;
-  private final ChangeJson changeJson;
+  private final Provider<ChangeJson> changeJson;
 
   @Inject
   ChangeApiImpl(Changes changeApi,
       Revisions revisions,
       RevisionApiImpl.Factory revisionApi,
-      Provider<Abandon> abandon,
-      Provider<Revert> revert,
-      Provider<Restore> restore,
+      Abandon abandon,
+      Revert revert,
+      Restore restore,
       Provider<PostReviewers> postReviewers,
-      ChangeJson changeJson,
+      Provider<ChangeJson> changeJson,
       @Assisted ChangeResource change) {
     this.changeApi = changeApi;
     this.revert = revert;
@@ -110,7 +110,7 @@
   @Override
   public void abandon(AbandonInput in) throws RestApiException {
     try {
-      abandon.get().apply(change, in);
+      abandon.apply(change, in);
     } catch (OrmException | IOException e) {
       throw new RestApiException("Cannot abandon change", e);
     }
@@ -124,7 +124,7 @@
   @Override
   public void restore(RestoreInput in) throws RestApiException {
     try {
-      restore.get().apply(change, in);
+      restore.apply(change, in);
     } catch (OrmException | IOException e) {
       throw new RestApiException("Cannot restore change", e);
     }
@@ -138,7 +138,7 @@
   @Override
   public ChangeApi revert(RevertInput in) throws RestApiException {
     try {
-      return changeApi.id(revert.get().apply(change, in)._number);
+      return changeApi.id(revert.apply(change, in)._number);
     } catch (OrmException | EmailException | IOException e) {
       throw new RestApiException("Cannot revert change", e);
     }
@@ -164,8 +164,8 @@
   public ChangeInfo get(EnumSet<ListChangesOption> s)
       throws RestApiException {
     try {
-      return new ChangeInfoMapper(s).map(
-          changeJson.addOptions(s).format(change));
+      return ChangeInfoMapper.INSTANCE.apply(
+          changeJson.get().addOptions(s).format(change));
     } catch (OrmException e) {
       throw new RestApiException("Cannot retrieve change", e);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeInfoMapper.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeInfoMapper.java
index 9d813fc..8e6c20b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeInfoMapper.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeInfoMapper.java
@@ -14,61 +14,55 @@
 
 package com.google.gerrit.server.api.changes;
 
-import static com.google.gerrit.extensions.common.ListChangesOption.ALL_REVISIONS;
-import static com.google.gerrit.extensions.common.ListChangesOption.CURRENT_ACTIONS;
-import static com.google.gerrit.extensions.common.ListChangesOption.CURRENT_REVISION;
-import static com.google.gerrit.extensions.common.ListChangesOption.DETAILED_LABELS;
-import static com.google.gerrit.extensions.common.ListChangesOption.LABELS;
-import static com.google.gerrit.extensions.common.ListChangesOption.MESSAGES;
-
-import com.google.common.collect.ImmutableMap;
+import com.google.common.base.Function;
+import com.google.common.collect.EnumBiMap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
-import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ApprovalInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
 import com.google.gerrit.extensions.common.ChangeStatus;
 import com.google.gerrit.extensions.common.LabelInfo;
-import com.google.gerrit.extensions.common.ListChangesOption;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Change.Status;
+import com.google.gerrit.server.api.accounts.AccountInfoMapper;
 import com.google.gerrit.server.change.ChangeJson;
 
-import java.util.EnumSet;
 import java.util.List;
 import java.util.Map;
 
-class ChangeInfoMapper {
-  private final static ImmutableMap<Change.Status, ChangeStatus> MAP =
-      Maps.immutableEnumMap(ImmutableMap.of(
-          Status.DRAFT, ChangeStatus.DRAFT,
-          Status.NEW, ChangeStatus.NEW,
-          Status.SUBMITTED, ChangeStatus.SUBMITTED,
-          Status.MERGED, ChangeStatus.MERGED,
-          Status.ABANDONED, ChangeStatus.ABANDONED));
+public class ChangeInfoMapper
+    implements Function<ChangeJson.ChangeInfo, ChangeInfo> {
+  public static final ChangeInfoMapper INSTANCE = new ChangeInfoMapper();
 
-  private final EnumSet<ListChangesOption> s;
-
-  ChangeInfoMapper(EnumSet<ListChangesOption> s) {
-    this.s = s;
+  private final static EnumBiMap<Change.Status, ChangeStatus> STATUS_MAP =
+      EnumBiMap.create(Change.Status.class, ChangeStatus.class);
+  static {
+    STATUS_MAP.put(Status.DRAFT, ChangeStatus.DRAFT);
+    STATUS_MAP.put(Status.NEW, ChangeStatus.NEW);
+    STATUS_MAP.put(Status.SUBMITTED, ChangeStatus.SUBMITTED);
+    STATUS_MAP.put(Status.MERGED, ChangeStatus.MERGED);
+    STATUS_MAP.put(Status.ABANDONED, ChangeStatus.ABANDONED);
   }
 
-  ChangeInfo map(ChangeJson.ChangeInfo i) {
+  public static Status changeStatus2Status(ChangeStatus status) {
+    if (status != null) {
+      return STATUS_MAP.inverse().get(status);
+    }
+    return Change.Status.NEW;
+  }
+
+  private ChangeInfoMapper() {
+  }
+
+  @Override
+  public ChangeInfo apply(ChangeJson.ChangeInfo i) {
     ChangeInfo o = new ChangeInfo();
     mapCommon(i, o);
-    if (has(LABELS) || has(DETAILED_LABELS)) {
-      mapLabels(i, o);
-    }
-    if (has(MESSAGES)) {
-      mapMessages(i, o);
-    }
-    if (has(ALL_REVISIONS) || has(CURRENT_REVISION)) {
-      o.revisions = i.revisions;
-    }
-    if (has(CURRENT_ACTIONS)) {
-      o.actions = i.actions;
-    }
+    mapLabels(i, o);
+    mapMessages(i, o);
+    o.revisions = i.revisions;
+    o.actions = i.actions;
     return o;
   }
 
@@ -79,7 +73,7 @@
     o.topic = i.topic;
     o.changeId = i.changeId;
     o.subject = i.subject;
-    o.status = MAP.get(i.status);
+    o.status = STATUS_MAP.get(i.status);
     o.created = i.created;
     o.updated = i.updated;
     o.starred = i.starred;
@@ -87,17 +81,21 @@
     o.mergeable = i.mergeable;
     o.insertions = i.insertions;
     o.deletions = i.deletions;
-    o.owner = fromAcountInfo(i.owner);
+    o.owner = AccountInfoMapper.fromAcountInfo(i.owner);
     o.currentRevision = i.currentRevision;
+    o._number = i._number;
   }
 
   private void mapMessages(ChangeJson.ChangeInfo i, ChangeInfo o) {
+    if (i.messages == null) {
+      return;
+    }
     List<ChangeMessageInfo> r =
         Lists.newArrayListWithCapacity(i.messages.size());
     for (ChangeJson.ChangeMessageInfo m : i.messages) {
       ChangeMessageInfo cmi = new ChangeMessageInfo();
       cmi.id = m.id;
-      cmi.author = fromAcountInfo(m.author);
+      cmi.author = AccountInfoMapper.fromAcountInfo(m.author);
       cmi.date = m.date;
       cmi.message = m.message;
       cmi._revisionNumber = m._revisionNumber;
@@ -107,15 +105,19 @@
   }
 
   private void mapLabels(ChangeJson.ChangeInfo i, ChangeInfo o) {
+    if (i.labels == null) {
+      return;
+    }
     Map<String, LabelInfo> r = Maps.newLinkedHashMap();
     for (Map.Entry<String, ChangeJson.LabelInfo> e : i.labels.entrySet()) {
       ChangeJson.LabelInfo li = e.getValue();
       LabelInfo lo = new LabelInfo();
-      lo.approved = fromAcountInfo(li.approved);
-      lo.rejected = fromAcountInfo(li.rejected);
-      lo.recommended = fromAcountInfo(li.recommended);
-      lo.disliked = fromAcountInfo(li.disliked);
+      lo.approved = AccountInfoMapper.fromAcountInfo(li.approved);
+      lo.rejected = AccountInfoMapper.fromAcountInfo(li.rejected);
+      lo.recommended = AccountInfoMapper.fromAcountInfo(li.recommended);
+      lo.disliked = AccountInfoMapper.fromAcountInfo(li.disliked);
       lo.value = li.value;
+      lo.defaultValue = li.defaultValue;
       lo.optional = li.optional;
       lo.blocking = li.blocking;
       lo.values = li.values;
@@ -130,33 +132,11 @@
     o.labels = r;
   }
 
-  private boolean has(ListChangesOption o) {
-    return s.contains(o);
-  }
-
   private static ApprovalInfo fromApprovalInfo(ChangeJson.ApprovalInfo ai) {
     ApprovalInfo ao = new ApprovalInfo();
     ao.value = ai.value;
     ao.date = ai.date;
-    fromAccount(ai, ao);
+    AccountInfoMapper.fromAccount(ai, ao);
     return ao;
   }
-
-  private static AccountInfo fromAcountInfo(
-      com.google.gerrit.server.account.AccountInfo i) {
-    if (i == null) {
-      return null;
-    }
-    AccountInfo ai = new AccountInfo();
-    fromAccount(i, ai);
-    return ai;
-  }
-
-  private static void fromAccount(
-      com.google.gerrit.server.account.AccountInfo i, AccountInfo ai) {
-    ai._accountId = i._accountId;
-    ai.email = i.email;
-    ai.name = i.name;
-    ai.username = i.username;
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangesImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangesImpl.java
index fdd0817..db72c9c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangesImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangesImpl.java
@@ -14,26 +14,51 @@
 
 package com.google.gerrit.server.api.changes;
 
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
 import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.api.changes.Changes;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ListChangesOption;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.change.ChangesCollection;
+import com.google.gerrit.server.change.CreateChange;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.query.change.QueryChanges;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
+import java.io.IOException;
+import java.util.List;
+
+@Singleton
 class ChangesImpl implements Changes {
   private final ChangesCollection changes;
   private final ChangeApiImpl.Factory api;
+  private final CreateChange createChange;
+  private final Provider<QueryChanges> queryProvider;
 
   @Inject
-  ChangesImpl(ChangesCollection changes, ChangeApiImpl.Factory api) {
+  ChangesImpl(ChangesCollection changes,
+      ChangeApiImpl.Factory api,
+      CreateChange createChange,
+      Provider<QueryChanges> queryProvider) {
     this.changes = changes;
     this.api = api;
+    this.createChange = createChange;
+    this.queryProvider = queryProvider;
   }
 
   @Override
@@ -60,4 +85,62 @@
       throw new RestApiException("Cannot parse change", e);
     }
   }
+
+  @Override
+  public ChangeApi create(ChangeInfo in) throws RestApiException {
+    try {
+      ChangeJson.ChangeInfo out = createChange.apply(
+          TopLevelResource.INSTANCE, in).value();
+      return api.create(changes.parse(TopLevelResource.INSTANCE,
+          IdString.fromUrl(out.changeId)));
+    } catch (OrmException | IOException | InvalidChangeOperationException e) {
+      throw new RestApiException("Cannot create change", e);
+    }
+  }
+
+  @Override
+  public QueryRequest query() {
+    return new QueryRequest() {
+      @Override
+      public List<ChangeInfo> get() throws RestApiException {
+        return ChangesImpl.this.get(this);
+      }
+    };
+  }
+
+  @Override
+  public QueryRequest query(String query) {
+    return query().withQuery(query);
+  }
+
+  private List<ChangeInfo> get(final QueryRequest q) throws RestApiException {
+    QueryChanges qc = queryProvider.get();
+    if (q.getQuery() != null) {
+      qc.addQuery(q.getQuery());
+    }
+    qc.setLimit(q.getLimit());
+    qc.setStart(q.getStart());
+    for (ListChangesOption option : q.getOptions()) {
+      qc.addOption(option);
+    }
+
+    try {
+      List<?> result = qc.apply(TopLevelResource.INSTANCE);
+      if (result.isEmpty()) {
+        return ImmutableList.of();
+      }
+
+      // Check type safety of result; the extension API should be safer than the
+      // REST API in this case, since it's intended to be used in Java.
+      Object first = checkNotNull(result.iterator().next());
+      checkState(first instanceof ChangeJson.ChangeInfo);
+      @SuppressWarnings("unchecked")
+      List<ChangeJson.ChangeInfo> infos = (List<ChangeJson.ChangeInfo>) result;
+
+      return ImmutableList.copyOf(
+          Lists.transform(infos, ChangeInfoMapper.INSTANCE));
+    } catch (BadRequestException | AuthException | OrmException e) {
+      throw new RestApiException("Cannot query changes", e);
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
index a9253c3..c709b34 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.api.changes;
 
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.api.changes.Changes;
@@ -21,51 +22,73 @@
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.RevisionApi;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
+import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.change.CherryPick;
 import com.google.gerrit.server.change.DeleteDraftPatchSet;
+import com.google.gerrit.server.change.FileResource;
+import com.google.gerrit.server.change.Files;
 import com.google.gerrit.server.change.PostReview;
 import com.google.gerrit.server.change.Publish;
 import com.google.gerrit.server.change.Rebase;
+import com.google.gerrit.server.change.Reviewed;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.change.Submit;
+import com.google.gerrit.server.changedetail.RebaseChange;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 
 import java.io.IOException;
+import java.util.Set;
 
-class RevisionApiImpl implements RevisionApi {
+class RevisionApiImpl extends RevisionApi.NotImplemented implements RevisionApi {
   interface Factory {
     RevisionApiImpl create(RevisionResource r);
   }
 
   private final Changes changes;
-  private final Provider<CherryPick> cherryPick;
-  private final Provider<DeleteDraftPatchSet> deleteDraft;
-  private final Provider<Rebase> rebase;
-  private final Provider<PostReview> review;
-  private final Provider<Submit> submit;
-  private final Provider<Publish> publish;
+  private final CherryPick cherryPick;
+  private final DeleteDraftPatchSet deleteDraft;
+  private final Rebase rebase;
+  private final RebaseChange rebaseChange;
+  private final Submit submit;
+  private final Publish publish;
+  private final Reviewed.PutReviewed putReviewed;
+  private final Reviewed.DeleteReviewed deleteReviewed;
   private final RevisionResource revision;
+  private final Provider<Files> files;
+  private final Provider<Files.ListFiles> listFiles;
+  private final Provider<PostReview> review;
 
   @Inject
   RevisionApiImpl(Changes changes,
-      Provider<CherryPick> cherryPick,
-      Provider<DeleteDraftPatchSet> deleteDraft,
-      Provider<Rebase> rebase,
+      CherryPick cherryPick,
+      DeleteDraftPatchSet deleteDraft,
+      Rebase rebase,
+      RebaseChange rebaseChange,
+      Submit submit,
+      Publish publish,
+      Reviewed.PutReviewed putReviewed,
+      Reviewed.DeleteReviewed deleteReviewed,
+      Provider<Files> files,
+      Provider<Files.ListFiles> listFiles,
       Provider<PostReview> review,
-      Provider<Submit> submit,
-      Provider<Publish> publish,
       @Assisted RevisionResource r) {
     this.changes = changes;
     this.cherryPick = cherryPick;
     this.deleteDraft = deleteDraft;
     this.rebase = rebase;
+    this.rebaseChange = rebaseChange;
     this.review = review;
     this.submit = submit;
     this.publish = publish;
+    this.files = files;
+    this.putReviewed = putReviewed;
+    this.deleteReviewed = deleteReviewed;
+    this.listFiles = listFiles;
     this.revision = r;
   }
 
@@ -88,7 +111,7 @@
   @Override
   public void submit(SubmitInput in) throws RestApiException {
     try {
-      submit.get().apply(revision, in);
+      submit.apply(revision, in);
     } catch (OrmException | IOException e) {
       throw new RestApiException("Cannot submit change", e);
     }
@@ -97,7 +120,7 @@
   @Override
   public void publish() throws RestApiException {
     try {
-      publish.get().apply(revision, new Publish.Input());
+      publish.apply(revision, new Publish.Input());
     } catch (OrmException | IOException e) {
       throw new RestApiException("Cannot publish draft patch set", e);
     }
@@ -106,7 +129,7 @@
   @Override
   public void delete() throws RestApiException {
     try {
-      deleteDraft.get().apply(revision, null);
+      deleteDraft.apply(revision, null);
     } catch (OrmException | IOException e) {
       throw new RestApiException("Cannot delete draft ps", e);
     }
@@ -115,18 +138,52 @@
   @Override
   public ChangeApi rebase() throws RestApiException {
     try {
-      return changes.id(rebase.get().apply(revision, null)._number);
+      return changes.id(rebase.apply(revision, null)._number);
     } catch (OrmException | EmailException e) {
       throw new RestApiException("Cannot rebase ps", e);
     }
   }
 
   @Override
+  public boolean canRebase() {
+    return rebaseChange.canRebase(revision);
+  }
+
+  @Override
   public ChangeApi cherryPick(CherryPickInput in) throws RestApiException {
     try {
-      return changes.id(cherryPick.get().apply(revision, in)._number);
+      return changes.id(cherryPick.apply(revision, in)._number);
     } catch (OrmException | EmailException | IOException e) {
       throw new RestApiException("Cannot cherry pick", e);
     }
   }
+
+  @Override
+  public void setReviewed(String path, boolean reviewed) throws RestApiException {
+    try {
+      RestModifyView<FileResource, Reviewed.Input> view;
+      if (reviewed) {
+        view = putReviewed;
+      } else {
+        view = deleteReviewed;
+      }
+      view.apply(
+          files.get().parse(revision, IdString.fromDecoded(path)),
+          new Reviewed.Input());
+    } catch (Exception e) {
+      throw new RestApiException("Cannot update reviewed flag", e);
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public Set<String> reviewed() throws RestApiException {
+    try {
+      return ImmutableSet.copyOf((Iterable<String>) listFiles
+          .get().setReviewed(true)
+          .apply(revision).value());
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot list reviewed files", e);
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/BranchApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/BranchApiImpl.java
index 39166c3..f4dc67e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/BranchApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/BranchApiImpl.java
@@ -24,7 +24,7 @@
 
 import java.io.IOException;
 
-public class BranchApiImpl implements BranchApi {
+public class BranchApiImpl extends BranchApi.NotImplemented implements BranchApi {
   interface Factory {
     BranchApiImpl create(ProjectResource project, String ref);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
index 14b05b6..7f73a38 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
@@ -14,29 +14,111 @@
 
 package com.google.gerrit.server.api.projects;
 
+import com.google.common.base.Preconditions;
+import com.google.gerrit.common.errors.ProjectCreationFailedException;
 import com.google.gerrit.extensions.api.projects.BranchApi;
 import com.google.gerrit.extensions.api.projects.ProjectApi;
+import com.google.gerrit.extensions.api.projects.ProjectInput;
+import com.google.gerrit.extensions.common.ProjectInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.server.project.CreateProject;
+import com.google.gerrit.server.project.ProjectJson;
 import com.google.gerrit.server.project.ProjectResource;
-import com.google.inject.Inject;
+import com.google.gerrit.server.project.ProjectsCollection;
+import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
 
-public class ProjectApiImpl implements ProjectApi {
+import java.io.IOException;
+
+public class ProjectApiImpl extends ProjectApi.NotImplemented implements ProjectApi {
   interface Factory {
     ProjectApiImpl create(ProjectResource project);
+    ProjectApiImpl create(String name);
   }
 
+  private final Provider<CreateProject.Factory> createProjectFactory;
+  private final ProjectApiImpl.Factory projectApi;
+  private final ProjectsCollection projects;
   private final ProjectResource project;
+  private final ProjectJson projectJson;
+  private final String name;
   private final BranchApiImpl.Factory branchApi;
 
-  @Inject
-  ProjectApiImpl(
+  @AssistedInject
+  ProjectApiImpl(Provider<CreateProject.Factory> createProjectFactory,
+      ProjectApiImpl.Factory projectApi,
+      ProjectsCollection projects,
+      ProjectJson projectJson,
       BranchApiImpl.Factory branchApiFactory,
       @Assisted ProjectResource project) {
+    this(createProjectFactory, projectApi, projects, projectJson,
+        branchApiFactory, project, null);
+  }
+
+  @AssistedInject
+  ProjectApiImpl(Provider<CreateProject.Factory> createProjectFactory,
+      ProjectApiImpl.Factory projectApi,
+      ProjectsCollection projects,
+      ProjectJson projectJson,
+      BranchApiImpl.Factory branchApiFactory,
+      @Assisted String name) {
+    this(createProjectFactory, projectApi, projects, projectJson,
+        branchApiFactory, null, name);
+  }
+
+  private ProjectApiImpl(Provider<CreateProject.Factory> createProjectFactory,
+      ProjectApiImpl.Factory projectApi,
+      ProjectsCollection projects,
+      ProjectJson projectJson,
+      BranchApiImpl.Factory branchApiFactory,
+      ProjectResource project,
+      String name) {
+    this.createProjectFactory = createProjectFactory;
+    this.projectApi = projectApi;
+    this.projects = projects;
+    this.projectJson = projectJson;
     this.project = project;
+    this.name = name;
     this.branchApi = branchApiFactory;
   }
 
   @Override
+  public ProjectApi create() throws RestApiException {
+    return create(new ProjectInput());
+  }
+
+  @Override
+  public ProjectApi create(ProjectInput in) throws RestApiException {
+    try {
+      if (name == null) {
+        throw new ResourceConflictException("Project already exists");
+      }
+      if (in.name != null && !name.equals(in.name)) {
+        throw new BadRequestException("name must match input.name");
+      }
+      createProjectFactory.get().create(name)
+          .apply(TopLevelResource.INSTANCE, in);
+      return projectApi.create(projects.parse(name));
+    } catch (BadRequestException | UnprocessableEntityException
+        | ResourceNotFoundException | ProjectCreationFailedException
+        | IOException e) {
+      throw new RestApiException("Cannot create project: " + e.getMessage(), e);
+    }
+  }
+
+  @Override
+  public ProjectInfo get() {
+    Preconditions.checkNotNull(project);
+    return projectJson.format(project);
+  }
+
+  @Override
   public BranchApi branch(String ref) {
     return branchApi.create(project, ref);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectsImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectsImpl.java
index bd5e2ac..86baa1e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectsImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectsImpl.java
@@ -14,31 +14,64 @@
 
 package com.google.gerrit.server.api.projects;
 
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.extensions.api.projects.ProjectApi;
 import com.google.gerrit.extensions.api.projects.Projects;
+import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.server.project.ListProjects;
 import com.google.gerrit.server.project.ProjectsCollection;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import java.io.IOException;
+import java.util.List;
 
-class ProjectsImpl implements Projects {
+@Singleton
+class ProjectsImpl extends Projects.NotImplemented implements Projects {
   private final ProjectsCollection projects;
   private final ProjectApiImpl.Factory api;
+  private final Provider<ListProjects> listProvider;
 
   @Inject
-  ProjectsImpl(ProjectsCollection projects, ProjectApiImpl.Factory api) {
+  ProjectsImpl(ProjectsCollection projects,
+      ProjectApiImpl.Factory api,
+      Provider<ListProjects> listProvider) {
     this.projects = projects;
     this.api = api;
+    this.listProvider = listProvider;
   }
 
   @Override
   public ProjectApi name(String name) throws RestApiException {
     try {
       return api.create(projects.parse(name));
-    } catch (IOException | UnprocessableEntityException e) {
+    } catch (UnprocessableEntityException e) {
+      return api.create(name);
+    } catch (IOException e) {
       throw new RestApiException("Cannot retrieve project");
     }
   }
+
+  @Override
+  public ListRequest list() {
+    return new ListRequest() {
+      @Override
+      public List<ProjectInfo> get() throws RestApiException {
+        return list(this);
+      }
+    };
+  }
+
+  private List<ProjectInfo> list(ListRequest request) throws RestApiException {
+    ListProjects lp = listProvider.get();
+    lp.setShowDescription(request.getDescription());
+    lp.setLimit(request.getLimit());
+    lp.setStart(request.getStart());
+    lp.setMatchPrefix(request.getPrefix());
+
+    return ImmutableList.copyOf(lp.apply().values());
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/TimestampHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/TimestampHandler.java
new file mode 100644
index 0000000..8dd4270
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/TimestampHandler.java
@@ -0,0 +1,61 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.args4j;
+
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.kohsuke.args4j.CmdLineException;
+import org.kohsuke.args4j.CmdLineParser;
+import org.kohsuke.args4j.OptionDef;
+import org.kohsuke.args4j.spi.OptionHandler;
+import org.kohsuke.args4j.spi.Parameters;
+import org.kohsuke.args4j.spi.Setter;
+
+import java.sql.Timestamp;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.TimeZone;
+
+public class TimestampHandler extends OptionHandler<Timestamp> {
+  public final static String TIMESTAMP_FORMAT = "yyyyMMdd_HHmm";
+
+  @Inject
+  public TimestampHandler(@Assisted CmdLineParser parser,
+      @Assisted OptionDef option, @Assisted Setter<Timestamp> setter) {
+    super(parser, option, setter);
+  }
+
+  @Override
+  public int parseArguments(Parameters params) throws CmdLineException {
+    String timestamp = params.getParameter(0);
+    try {
+      DateFormat fmt = new SimpleDateFormat(TIMESTAMP_FORMAT);
+      fmt.setTimeZone(TimeZone.getTimeZone("UTC"));
+      setter.addValue(new Timestamp(fmt.parse(timestamp).getTime()));
+      return 1;
+    } catch (ParseException e) {
+      throw new CmdLineException(owner,
+          String.format("Invalid timestamp: %s; expected format: %s",
+              timestamp, TIMESTAMP_FORMAT), e);
+    }
+  }
+
+  @Override
+  public String getDefaultMetaVariable() {
+    return "TIMESTAMP";
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/InternalAuthBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/InternalAuthBackend.java
index 6e9e71b..0b4baf2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/InternalAuthBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/InternalAuthBackend.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.auth;
 
+import com.google.common.base.Strings;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.config.AuthConfig;
@@ -42,7 +43,8 @@
   public AuthUser authenticate(AuthRequest req)
       throws MissingCredentialsException, InvalidCredentialsException,
       UnknownUserException, UserNotAllowedException, AuthException {
-    if (req.getUsername() == null || req.getPassword() == null) {
+    if (Strings.isNullOrEmpty(req.getUsername())
+        || Strings.isNullOrEmpty(req.getPassword())) {
       throw new MissingCredentialsException();
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java
index afefe6f..618aa7d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java
@@ -58,7 +58,7 @@
 @Singleton class Helper {
   static final String LDAP_UUID = "ldap:";
 
-  private final Cache<String, ImmutableSet<String>> groupsByInclude;
+  private final Cache<String, ImmutableSet<String>> parentGroups;
   private final Config config;
   private final String server;
   private final String username;
@@ -67,28 +67,41 @@
   private final boolean sslVerify;
   private final String authentication;
   private volatile LdapSchema ldapSchema;
-  private final String readTimeOutMillis;
+  private final String readTimeoutMillis;
+  private final String connectTimeoutMillis;
+  private final boolean useConnectionPooling;
 
   @Inject
   Helper(@GerritServerConfig final Config config,
-      @Named(LdapModule.GROUPS_BYINCLUDE_CACHE)
-      Cache<String, ImmutableSet<String>> groupsByInclude) {
+      @Named(LdapModule.PARENT_GROUPS_CACHE)
+      Cache<String, ImmutableSet<String>> parentGroups) {
     this.config = config;
     this.server = LdapRealm.optional(config, "server");
     this.username = LdapRealm.optional(config, "username");
-    this.password = LdapRealm.optional(config, "password");
-    this.referral = LdapRealm.optional(config, "referral");
+    this.password = LdapRealm.optional(config, "password", "");
+    this.referral = LdapRealm.optional(config, "referral", "ignore");
     this.sslVerify = config.getBoolean("ldap", "sslverify", true);
-    this.authentication = LdapRealm.optional(config, "authentication");
-    String timeout = LdapRealm.optional(config, "readTimeout");
-    if (timeout != null) {
-      readTimeOutMillis =
-          Long.toString(ConfigUtil.getTimeUnit(timeout, 0,
+    this.authentication =
+        LdapRealm.optional(config, "authentication", "simple");
+    String readTimeout = LdapRealm.optional(config, "readTimeout");
+    if (readTimeout != null) {
+      readTimeoutMillis =
+          Long.toString(ConfigUtil.getTimeUnit(readTimeout, 0,
               TimeUnit.MILLISECONDS));
     } else {
-      readTimeOutMillis = null;
+      readTimeoutMillis = null;
     }
-    this.groupsByInclude = groupsByInclude;
+    String connectTimeout = LdapRealm.optional(config, "connectTimeout");
+    if (connectTimeout != null) {
+      connectTimeoutMillis =
+          Long.toString(ConfigUtil.getTimeUnit(connectTimeout, 0,
+              TimeUnit.MILLISECONDS));
+    } else {
+      connectTimeoutMillis = null;
+    }
+    this.parentGroups = parentGroups;
+    this.useConnectionPooling =
+        LdapRealm.optional(config, "useConnectionPooling", false);
   }
 
   private Properties createContextProperties() {
@@ -99,22 +112,28 @@
       Class<? extends SSLSocketFactory> factory = BlindSSLSocketFactory.class;
       env.put("java.naming.ldap.factory.socket", factory.getName());
     }
-    if (readTimeOutMillis != null) {
-      env.put("com.sun.jndi.ldap.read.timeout", readTimeOutMillis);
+    if (readTimeoutMillis != null) {
+      env.put("com.sun.jndi.ldap.read.timeout", readTimeoutMillis);
+    }
+    if (connectTimeoutMillis != null) {
+      env.put("com.sun.jndi.ldap.connect.timeout", connectTimeoutMillis);
+    }
+    if (useConnectionPooling) {
+      env.put("com.sun.jndi.ldap.connect.pool", "true");
     }
     return env;
   }
 
   DirContext open() throws NamingException, LoginException {
     final Properties env = createContextProperties();
-    env.put(Context.SECURITY_AUTHENTICATION, authentication != null ? authentication : "simple");
-    env.put(Context.REFERRAL, referral != null ? referral : "ignore");
+    env.put(Context.SECURITY_AUTHENTICATION, authentication);
+    env.put(Context.REFERRAL, referral);
     if ("GSSAPI".equals(authentication)) {
       return kerberosOpen(env);
     } else {
       if (username != null) {
         env.put(Context.SECURITY_PRINCIPAL, username);
-        env.put(Context.SECURITY_CREDENTIALS, password != null ? password : "");
+        env.put(Context.SECURITY_CREDENTIALS, password);
       }
       return new InitialDirContext(env);
     }
@@ -146,8 +165,8 @@
     final Properties env = createContextProperties();
     env.put(Context.SECURITY_AUTHENTICATION, "simple");
     env.put(Context.SECURITY_PRINCIPAL, dn);
-    env.put(Context.SECURITY_CREDENTIALS, password != null ? password : "");
-    env.put(Context.REFERRAL, referral != null ? referral : "ignore");
+    env.put(Context.SECURITY_CREDENTIALS, password);
+    env.put(Context.REFERRAL, referral);
     try {
       return new InitialDirContext(env);
     } catch (NamingException e) {
@@ -166,41 +185,42 @@
     return ldapSchema;
   }
 
-  LdapQuery.Result findAccount(final Helper.LdapSchema schema,
-      final DirContext ctx, final String username) throws NamingException,
-      AccountException {
-    final HashMap<String, String> params = new HashMap<String, String>();
+  LdapQuery.Result findAccount(Helper.LdapSchema schema,
+      DirContext ctx, String username, boolean fetchMemberOf)
+      throws NamingException, AccountException {
+    final HashMap<String, String> params = new HashMap<>();
     params.put(LdapRealm.USERNAME, username);
 
-    final List<LdapQuery.Result> res = new ArrayList<LdapQuery.Result>();
-    for (LdapQuery accountQuery : schema.accountQueryList) {
-      res.addAll(accountQuery.query(ctx, params));
+    List<LdapQuery> accountQueryList;
+    if (fetchMemberOf && schema.type.accountMemberField() != null) {
+      accountQueryList = schema.accountWithMemberOfQueryList;
+    } else {
+      accountQueryList = schema.accountQueryList;
     }
 
-    switch (res.size()) {
-      case 0:
-        throw new NoSuchUserException(username);
-
-      case 1:
+    for (LdapQuery accountQuery : accountQueryList) {
+      List<LdapQuery.Result> res = accountQuery.query(ctx, params);
+      if (res.size() == 1) {
         return res.get(0);
-
-      default:
+      } else if (res.size() > 1) {
         throw new AccountException("Duplicate users: " + username);
+      }
     }
+    throw new NoSuchUserException(username);
   }
 
   Set<AccountGroup.UUID> queryForGroups(final DirContext ctx,
       final String username, LdapQuery.Result account)
       throws NamingException, AccountException {
     final LdapSchema schema = getSchema(ctx);
-    final Set<String> groupDNs = new HashSet<String>();
+    final Set<String> groupDNs = new HashSet<>();
 
     if (!schema.groupMemberQueryList.isEmpty()) {
-      final HashMap<String, String> params = new HashMap<String, String>();
+      final HashMap<String, String> params = new HashMap<>();
 
       if (account == null) {
         try {
-          account = findAccount(schema, ctx, username);
+          account = findAccount(schema, ctx, username, false);
         } catch (AccountException e) {
           LdapRealm.log.warn("Account " + username +
               " not found, assuming empty group membership");
@@ -221,9 +241,9 @@
     }
 
     if (schema.accountMemberField != null) {
-      if (account == null) {
+      if (account == null || account.getAll(schema.accountMemberField) == null) {
         try {
-          account = findAccount(schema, ctx, username);
+          account = findAccount(schema, ctx, username, true);
         } catch (AccountException e) {
           LdapRealm.log.warn("Account " + username +
               " not found, assuming empty group membership");
@@ -244,7 +264,7 @@
       }
     }
 
-    final Set<AccountGroup.UUID> actual = new HashSet<AccountGroup.UUID>();
+    final Set<AccountGroup.UUID> actual = new HashSet<>();
     for (String dn : groupDNs) {
       actual.add(new AccountGroup.UUID(LDAP_UUID + dn));
     }
@@ -259,14 +279,15 @@
   private void recursivelyExpandGroups(final Set<String> groupDNs,
       final LdapSchema schema, final DirContext ctx, final String groupDN) {
     if (groupDNs.add(groupDN) && schema.accountMemberField != null) {
-      ImmutableSet<String> cachedGroupDNs = groupsByInclude.getIfPresent(groupDN);
-      if (cachedGroupDNs == null) {
+      ImmutableSet<String> cachedParentsDNs = parentGroups.getIfPresent(groupDN);
+      if (cachedParentsDNs == null) {
         // Recursively identify the groups it is a member of.
         ImmutableSet.Builder<String> dns = ImmutableSet.builder();
         try {
           final Name compositeGroupName = new CompositeName().add(groupDN);
           final Attribute in =
-              ctx.getAttributes(compositeGroupName).get(schema.accountMemberField);
+              ctx.getAttributes(compositeGroupName, schema.accountMemberFieldArray)
+                .get(schema.accountMemberField);
           if (in != null) {
             final NamingEnumeration<?> groups = in.getAll();
             try {
@@ -279,10 +300,10 @@
         } catch (NamingException e) {
           LdapRealm.log.warn("Could not find group " + groupDN, e);
         }
-        cachedGroupDNs = dns.build();
-        groupsByInclude.put(groupDN, cachedGroupDNs);
+        cachedParentsDNs = dns.build();
+        parentGroups.put(groupDN, cachedParentsDNs);
       }
-      for (String dn : cachedGroupDNs) {
+      for (String dn : cachedParentsDNs) {
         recursivelyExpandGroups(groupDNs, schema, ctx, dn);
       }
     }
@@ -295,7 +316,9 @@
     final ParameterizedString accountEmailAddress;
     final ParameterizedString accountSshUserName;
     final String accountMemberField;
+    final String[] accountMemberFieldArray;
     final List<LdapQuery> accountQueryList;
+    final List<LdapQuery> accountWithMemberOfQueryList;
 
     final List<String> groupBases;
     final SearchScope groupScope;
@@ -305,10 +328,11 @@
 
     LdapSchema(final DirContext ctx) {
       type = discoverLdapType(ctx);
-      groupMemberQueryList = new ArrayList<LdapQuery>();
-      accountQueryList = new ArrayList<LdapQuery>();
+      groupMemberQueryList = new ArrayList<>();
+      accountQueryList = new ArrayList<>();
+      accountWithMemberOfQueryList = new ArrayList<>();
 
-      final Set<String> accountAtts = new HashSet<String>();
+      final Set<String> accountAtts = new HashSet<>();
 
       // Group query
       //
@@ -359,15 +383,24 @@
       accountMemberField =
           LdapRealm.optdef(config, "accountMemberField", type.accountMemberField());
       if (accountMemberField != null) {
-        accountAtts.add(accountMemberField);
+        accountMemberFieldArray = new String[] {accountMemberField};
+      } else {
+        accountMemberFieldArray = null;
       }
 
       final SearchScope accountScope = LdapRealm.scope(config, "accountScope");
       final String accountPattern =
           LdapRealm.reqdef(config, "accountPattern", type.accountPattern());
 
+      Set<String> accountWithMemberOfAtts;
+      if (accountMemberField != null) {
+        accountWithMemberOfAtts = new HashSet<>(accountAtts);
+        accountWithMemberOfAtts.add(accountMemberField);
+      } else {
+        accountWithMemberOfAtts = null;
+      }
       for (String accountBase : LdapRealm.requiredList(config, "accountBase")) {
-        final LdapQuery accountQuery =
+        LdapQuery accountQuery =
             new LdapQuery(accountBase, accountScope, new ParameterizedString(
                 accountPattern), accountAtts);
         if (accountQuery.getParameters().isEmpty()) {
@@ -375,6 +408,13 @@
               "No variables in ldap.accountPattern");
         }
         accountQueryList.add(accountQuery);
+
+        if (accountWithMemberOfAtts != null) {
+          LdapQuery accountWithMemberOfQuery =
+              new LdapQuery(accountBase, accountScope, new ParameterizedString(
+                  accountPattern), accountWithMemberOfAtts);
+          accountWithMemberOfQueryList.add(accountWithMemberOfQuery);
+        }
       }
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapAuthBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapAuthBackend.java
index eb6249c..1d90f0c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapAuthBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapAuthBackend.java
@@ -83,7 +83,7 @@
       }
       try {
         final Helper.LdapSchema schema = helper.getSchema(ctx);
-        final LdapQuery.Result m = helper.findAccount(schema, ctx, username);
+        final LdapQuery.Result m = helper.findAccount(schema, ctx, username, false);
 
         if (authConfig.getAuthType() == AuthType.LDAP) {
           // We found the user account, but we need to verify
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
index 26c1c7a..6cdce8d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
@@ -31,7 +31,6 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupMembership;
-import com.google.gerrit.server.account.ListGroupMembership;
 import com.google.gerrit.server.auth.ldap.Helper.LdapSchema;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectControl;
@@ -60,7 +59,7 @@
  * Implementation of GroupBackend for the LDAP group system.
  */
 public class LdapGroupBackend implements GroupBackend {
-  private static final Logger log = LoggerFactory.getLogger(LdapGroupBackend.class);
+  static final Logger log = LoggerFactory.getLogger(LdapGroupBackend.class);
 
   private static final String LDAP_NAME = "ldap/";
   private static final String GROUPNAME = "groupname";
@@ -185,21 +184,7 @@
     if (id == null) {
       return GroupMembership.EMPTY;
     }
-
-    try {
-      final Set<AccountGroup.UUID> groups = membershipCache.get(id);
-      return new ListGroupMembership(groups) {
-        @Override
-        public Set<AccountGroup.UUID> getKnownGroups() {
-          Set<AccountGroup.UUID> g = Sets.newHashSet(groups);
-          g.retainAll(projectCache.guessRelevantGroupUUIDs());
-          return g;
-        }
-      };
-    } catch (ExecutionException e) {
-      log.warn(String.format("Cannot lookup membershipsOf %s in LDAP", id), e);
-      return GroupMembership.EMPTY;
-    }
+    return new LdapGroupMembership(membershipCache, projectCache, id);
   }
 
   private static String findId(final Collection<AccountExternalId> ids) {
@@ -227,7 +212,7 @@
         ParameterizedString filter = ParameterizedString.asis(
             schema.groupPattern.replace(GROUPNAME, name).toString());
         Set<String> returnAttrs =
-            new HashSet<String>(schema.groupName.getParameterNames());
+            new HashSet<>(schema.groupName.getParameterNames());
         Map<String, String> params = Collections.emptyMap();
         for (String groupBase : schema.groupBases) {
           LdapQuery query = new LdapQuery(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupMembership.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupMembership.java
new file mode 100644
index 0000000..7853320
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupMembership.java
@@ -0,0 +1,76 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.auth.ldap;
+
+import com.google.common.cache.LoadingCache;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.account.GroupMembership;
+import com.google.gerrit.server.account.ListGroupMembership;
+import com.google.gerrit.server.project.ProjectCache;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+
+class LdapGroupMembership implements GroupMembership {
+  private final LoadingCache<String, Set<AccountGroup.UUID>> membershipCache;
+  private final ProjectCache projectCache;
+  private final String id;
+  private GroupMembership membership;
+
+  LdapGroupMembership(
+      LoadingCache<String, Set<AccountGroup.UUID>> membershipCache,
+      ProjectCache projectCache,
+      String id) {
+    this.membershipCache = membershipCache;
+    this.projectCache = projectCache;
+    this.id = id;
+  }
+
+  @Override
+  public boolean contains(AccountGroup.UUID groupId) {
+    return get().contains(groupId);
+  }
+
+  @Override
+  public boolean containsAnyOf(Iterable<AccountGroup.UUID> groupIds) {
+    return get().containsAnyOf(groupIds);
+  }
+
+  @Override
+  public Set<AccountGroup.UUID> intersection(Iterable<AccountGroup.UUID> groupIds) {
+    return get().intersection(groupIds);
+  }
+
+  @Override
+  public Set<AccountGroup.UUID> getKnownGroups() {
+    Set<AccountGroup.UUID> g = new HashSet<>(get().getKnownGroups());
+    g.retainAll(projectCache.guessRelevantGroupUUIDs());
+    return g;
+  }
+
+  private synchronized GroupMembership get() {
+    if (membership == null) {
+      try {
+        membership = new ListGroupMembership(membershipCache.get(id));
+      } catch (ExecutionException e) {
+        LdapGroupBackend.log.warn(String.format(
+            "Cannot lookup membershipsOf %s in LDAP", id), e);
+        membership = GroupMembership.EMPTY;
+      }
+    }
+    return membership;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java
index 88cf45b..eaaafd6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java
@@ -33,7 +33,7 @@
   static final String USERNAME_CACHE = "ldap_usernames";
   static final String GROUP_CACHE = "ldap_groups";
   static final String GROUP_EXIST_CACHE = "ldap_group_existence";
-  static final String GROUPS_BYINCLUDE_CACHE = "ldap_groups_byinclude";
+  static final String PARENT_GROUPS_CACHE = "ldap_groups_byinclude";
 
 
   @Override
@@ -55,7 +55,7 @@
       .expireAfterWrite(1, HOURS)
       .loader(LdapRealm.ExistenceLoader.class);
 
-    cache(GROUPS_BYINCLUDE_CACHE,
+    cache(PARENT_GROUPS_CACHE,
         String.class,
         new TypeLiteral<ImmutableSet<String>>() {})
       .expireAfterWrite(1, HOURS);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapQuery.java
index 8a6dfeb..1f68011 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapQuery.java
@@ -69,7 +69,7 @@
     sc.setReturningAttributes(returnAttributes);
     res = ctx.search(base, pattern.getRawPattern(), pattern.bind(params), sc);
     try {
-      final List<Result> r = new ArrayList<Result>();
+      final List<Result> r = new ArrayList<>();
       try {
         while (res.hasMore()) {
           r.add(new Result(res.next()));
@@ -83,7 +83,7 @@
   }
 
   class Result {
-    private final Map<String, Attribute> atts = new HashMap<String, Attribute>();
+    private final Map<String, Attribute> atts = new HashMap<>();
 
     Result(final SearchResult sr) {
       if (returnAttributes != null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
index da7ebdb..7b79add 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
@@ -69,6 +69,7 @@
   private final EmailExpander emailExpander;
   private final LoadingCache<String, Optional<Account.Id>> usernameCache;
   private final Set<Account.FieldName> readOnlyAccountFields;
+  private final boolean fetchMemberOfEagerly;
   private final Config config;
 
   private final LoadingCache<String, Set<AccountGroup.UUID>> membershipCache;
@@ -88,7 +89,7 @@
     this.membershipCache = membershipCache;
     this.config = config;
 
-    this.readOnlyAccountFields = new HashSet<Account.FieldName>();
+    this.readOnlyAccountFields = new HashSet<>();
 
     if (optdef(config, "accountFullName", "DEFAULT") != null) {
       readOnlyAccountFields.add(Account.FieldName.FULL_NAME);
@@ -96,6 +97,8 @@
     if (optdef(config, "accountSshUserName", "DEFAULT") != null) {
       readOnlyAccountFields.add(Account.FieldName.USER_NAME);
     }
+
+    fetchMemberOfEagerly = optional(config, "fetchMemberOfEagerly", true);
   }
 
   static SearchScope scope(final Config c, final String setting) {
@@ -106,6 +109,22 @@
     return config.getString("ldap", null, name);
   }
 
+  static int optional(Config config, String name, int defaultValue) {
+    return config.getInt("ldap", name, defaultValue);
+  }
+
+  static String optional(Config config, String name, String defaultValue) {
+    final String v = optional(config, name);
+    if (Strings.isNullOrEmpty(v)) {
+      return defaultValue;
+    }
+    return v;
+  }
+
+  static boolean optional(Config config, String name, boolean defaultValue) {
+    return config.getBoolean("ldap", name, defaultValue);
+  }
+
   static String required(final Config config, final String name) {
     final String v = optional(config, name);
     if (v == null || "".equals(v)) {
@@ -174,7 +193,7 @@
       return null;
     }
 
-    final Map<String, String> values = new HashMap<String, String>();
+    final Map<String, String> values = new HashMap<>();
     for (final String name : m.attributes()) {
       values.put(name, m.get(name));
     }
@@ -200,7 +219,8 @@
       }
       try {
         final Helper.LdapSchema schema = helper.getSchema(ctx);
-        final LdapQuery.Result m = helper.findAccount(schema, ctx, username);
+        final LdapQuery.Result m = helper.findAccount(schema, ctx, username,
+            fetchMemberOfEagerly);
 
         if (authConfig.getAuthType() == AuthType.LDAP && !who.isSkipAuthentication()) {
           // We found the user account, but we need to verify
@@ -229,7 +249,9 @@
         // in the middle of authenticating the user, its likely we will
         // need to know what access rights they have soon.
         //
-        membershipCache.put(username, helper.queryForGroups(ctx, username, m));
+        if (fetchMemberOfEagerly) {
+          membershipCache.put(username, helper.queryForGroups(ctx, username, m));
+        }
         return who;
       } finally {
         try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheModule.java
index c093380..2276def 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheModule.java
@@ -83,8 +83,7 @@
     @SuppressWarnings("unchecked")
     Key<Cache<K, V>> key = (Key<Cache<K, V>>) Key.get(type, Names.named(name));
 
-    CacheProvider<K, V> m =
-        new CacheProvider<K, V>(this, name, keyType, valType);
+    CacheProvider<K, V> m = new CacheProvider<>(this, name, keyType, valType);
     bind(key).toProvider(m).asEagerSingleton();
     bind(ANY_CACHE).annotatedWith(Exports.named(name)).to(key);
     return m.maximumWeight(1024);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/PersistentCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/PersistentCache.java
new file mode 100644
index 0000000..62623ea
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/PersistentCache.java
@@ -0,0 +1,50 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.cache;
+
+public interface PersistentCache {
+
+  DiskStats diskStats();
+
+  public static class DiskStats {
+    private final long size;
+    private final long space;
+    private final long hitCount;
+    private final long missCount;
+
+    public DiskStats(long size, long space, long hitCount, long missCount) {
+      this.size = size;
+      this.space = space;
+      this.hitCount = hitCount;
+      this.missCount = missCount;
+    }
+
+    public long size() {
+      return size;
+    }
+
+    public long space() {
+      return space;
+    }
+
+    public long hitCount() {
+      return hitCount;
+    }
+
+    public long requestCount() {
+      return hitCount + missCount;
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/PersistentCacheFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/PersistentCacheFactory.java
index 983e956..0769b2a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/PersistentCacheFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/PersistentCacheFactory.java
@@ -17,6 +17,7 @@
 import com.google.common.cache.Cache;
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
+import com.google.gerrit.server.plugins.Plugin;
 
 public interface PersistentCacheFactory {
   <K, V> Cache<K, V> build(CacheBinding<K, V> def);
@@ -24,4 +25,6 @@
   <K, V> LoadingCache<K, V> build(
       CacheBinding<K, V> def,
       CacheLoader<K, V> loader);
+
+  void onStop(Plugin plugin);
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
index b5ded54..bb9a40b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
@@ -25,24 +25,27 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
 import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.mail.AbandonedSender;
 import com.google.gerrit.server.mail.ReplyToChangeSender;
+import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
-import java.util.Collections;
 
+@Singleton
 public class Abandon implements RestModifyView<ChangeResource, AbandonInput>,
     UiAction<ChangeResource> {
   private static final Logger log = LoggerFactory.getLogger(Abandon.class);
@@ -52,18 +55,24 @@
   private final Provider<ReviewDb> dbProvider;
   private final ChangeJson json;
   private final ChangeIndexer indexer;
+  private final ChangeUpdate.Factory updateFactory;
+  private final ChangeMessagesUtil cmUtil;
 
   @Inject
   Abandon(ChangeHooks hooks,
       AbandonedSender.Factory abandonedSenderFactory,
       Provider<ReviewDb> dbProvider,
       ChangeJson json,
-      ChangeIndexer indexer) {
+      ChangeIndexer indexer,
+      ChangeUpdate.Factory updateFactory,
+      ChangeMessagesUtil cmUtil) {
     this.hooks = hooks;
     this.abandonedSenderFactory = abandonedSenderFactory;
     this.dbProvider = dbProvider;
     this.json = json;
     this.indexer = indexer;
+    this.updateFactory = updateFactory;
+    this.cmUtil = cmUtil;
   }
 
   @Override
@@ -82,6 +91,7 @@
     }
 
     ChangeMessage message;
+    ChangeUpdate update;
     ReviewDb db = dbProvider.get();
     db.changes().beginTransaction(change.getId());
     try {
@@ -102,12 +112,16 @@
         throw new ResourceConflictException("change is "
             + status(db.changes().get(req.getChange().getId())));
       }
+
+      //TODO(yyonas): atomic update was not propagated
+      update = updateFactory.create(control, change.getLastUpdatedOn());
       message = newMessage(input, caller, change);
-      db.changeMessages().insert(Collections.singleton(message));
+      cmUtil.addChangeMessage(db, update, message);
       db.commit();
     } finally {
       db.rollback();
     }
+    update.commit();
 
     CheckedFuture<?, IOException> indexFuture =
         indexer.indexAsync(change.getId());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ArchiveFormat.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ArchiveFormat.java
index e7d05df..a5054f3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ArchiveFormat.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ArchiveFormat.java
@@ -14,20 +14,13 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.common.collect.Maps;
-
 import org.eclipse.jgit.api.ArchiveCommand;
 import org.eclipse.jgit.archive.TarFormat;
 import org.eclipse.jgit.archive.Tbz2Format;
 import org.eclipse.jgit.archive.TgzFormat;
 import org.eclipse.jgit.archive.TxzFormat;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
-import java.util.Collections;
-import java.util.Map;
-
-enum ArchiveFormat {
+public enum ArchiveFormat {
   TGZ("application/x-gzip", new TgzFormat()),
   TAR("application/x-tar", new TarFormat()),
   TBZ2("application/x-bzip2", new Tbz2Format()),
@@ -35,8 +28,6 @@
   // Zip is not supported because it may be interpreted by a Java plugin as a
   // valid JAR file, whose code would have access to cookies on the domain.
 
-  static final Logger log = LoggerFactory.getLogger(ArchiveFormat.class);
-
   private final ArchiveCommand.Format<?> format;
   private final String mimeType;
 
@@ -46,7 +37,7 @@
     ArchiveCommand.registerFormat(name(), format);
   }
 
-  String getShortName() {
+  public String getShortName() {
     return name().toLowerCase();
   }
 
@@ -61,24 +52,4 @@
   Iterable<String> getSuffixes() {
     return format.suffixes();
   }
-
-  static Map<String, ArchiveFormat> init() {
-    String[] formats = new String[values().length];
-    for (int i = 0; i < values().length; i++) {
-      formats[i] = values()[i].name();
-    }
-
-    Map<String, ArchiveFormat> exts = Maps.newLinkedHashMap();
-    for (String name : formats) {
-      try {
-        ArchiveFormat format = valueOf(name.toUpperCase());
-        for (String ext : format.getSuffixes()) {
-          exts.put(ext, format);
-        }
-      } catch (IllegalArgumentException e) {
-        log.warn("Invalid archive.format {}", name);
-      }
-    }
-    return Collections.unmodifiableMap(exts);
-  }
 }
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 9fd315a..d0c2b98 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
@@ -27,11 +27,13 @@
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.mail.CreateChangeSender;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.RefControl;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -45,6 +47,7 @@
 
 import java.io.IOException;
 import java.util.Collections;
+import java.util.Map;
 import java.util.Set;
 
 public class ChangeInserter {
@@ -60,6 +63,7 @@
   private final GitReferenceUpdated gitRefUpdated;
   private final ChangeHooks hooks;
   private final ApprovalsUtil approvalsUtil;
+  private final ChangeMessagesUtil cmUtil;
   private final MergeabilityChecker mergeabilityChecker;
   private final CreateChangeSender.Factory createChangeSenderFactory;
 
@@ -72,17 +76,18 @@
   private ChangeMessage changeMessage;
   private Set<Account.Id> reviewers;
   private Set<Account.Id> extraCC;
+  private Map<String, Short> approvals;
   private boolean runHooks;
   private boolean sendMail;
 
   @Inject
   ChangeInserter(Provider<ReviewDb> dbProvider,
       ChangeUpdate.Factory updateFactory,
-      Provider<ApprovalsUtil> approvals,
       PatchSetInfoFactory patchSetInfoFactory,
       GitReferenceUpdated gitRefUpdated,
       ChangeHooks hooks,
       ApprovalsUtil approvalsUtil,
+      ChangeMessagesUtil cmUtil,
       MergeabilityChecker mergeabilityChecker,
       CreateChangeSender.Factory createChangeSenderFactory,
       @Assisted RefControl refControl,
@@ -93,6 +98,7 @@
     this.gitRefUpdated = gitRefUpdated;
     this.hooks = hooks;
     this.approvalsUtil = approvalsUtil;
+    this.cmUtil = cmUtil;
     this.mergeabilityChecker = mergeabilityChecker;
     this.createChangeSenderFactory = createChangeSenderFactory;
     this.refControl = refControl;
@@ -100,6 +106,7 @@
     this.commit = commit;
     this.reviewers = Collections.emptySet();
     this.extraCC = Collections.emptySet();
+    this.approvals = Collections.emptyMap();
     this.runHooks = true;
     this.sendMail = true;
 
@@ -152,14 +159,20 @@
     return patchSet;
   }
 
+  public ChangeInserter setApprovals(Map<String, Short> approvals) {
+    this.approvals = approvals;
+    return this;
+  }
+
   public PatchSetInfo getPatchSetInfo() {
     return patchSetInfo;
   }
 
   public Change insert() throws OrmException, IOException {
     ReviewDb db = dbProvider.get();
+    ChangeControl ctl = refControl.getProjectControl().controlFor(change);
     ChangeUpdate update = updateFactory.create(
-        refControl.getProjectControl().controlFor(change),
+        ctl,
         change.getCreatedOn());
     db.changes().beginTransaction(change.getId());
     try {
@@ -169,25 +182,23 @@
       LabelTypes labelTypes = refControl.getProjectControl().getLabelTypes();
       approvalsUtil.addReviewers(db, update, labelTypes, change,
           patchSet, patchSetInfo, reviewers, Collections.<Account.Id> emptySet());
+      approvalsUtil.addApprovals(db, update, labelTypes, patchSet, patchSetInfo,
+          change, ctl, approvals);
       if (messageIsForChange()) {
-        insertMessage(db);
+        cmUtil.addChangeMessage(db, update, changeMessage);
       }
       db.commit();
     } finally {
       db.rollback();
     }
-
     update.commit();
     CheckedFuture<?, IOException> f = mergeabilityChecker.newCheck()
         .addChange(change)
         .reindex()
         .runAsync();
-    if (!messageIsForChange()) {
-      insertMessage(db);
-      if (changeMessage != null) {
-        ChangeUtil.bumpRowVersionNotLastUpdatedOn(
-            changeMessage.getKey().getParentKey(), db);
-      }
+
+    if(!messageIsForChange()) {
+      commitMessageNotForChange();
     }
 
     if (sendMail) {
@@ -215,6 +226,23 @@
     return change;
   }
 
+  private void commitMessageNotForChange() throws OrmException,
+      IOException {
+    ReviewDb db = dbProvider.get();
+    if (changeMessage != null) {
+      Change otherChange =
+          db.changes().get(changeMessage.getPatchSetId().getParentKey());
+      ChangeUtil.bumpRowVersionNotLastUpdatedOn(
+          changeMessage.getKey().getParentKey(), db);
+      ChangeControl otherControl =
+          refControl.getProjectControl().controlFor(otherChange);
+      ChangeUpdate updateForOtherChange =
+          updateFactory.create(otherControl, change.getLastUpdatedOn());
+      cmUtil.addChangeMessage(db, updateForOtherChange, changeMessage);
+      updateForOtherChange.commit();
+    }
+  }
+
   private boolean messageIsForChange() {
     if (changeMessage == null) {
       return false;
@@ -223,10 +251,4 @@
     Change.Id msgId = changeMessage.getKey().getParentKey();
     return msgId.equals(id);
   }
-
-  private void insertMessage(ReviewDb db) throws OrmException {
-    if (changeMessage != null) {
-      db.changeMessages().insert(Collections.singleton(changeMessage));
-    }
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
index ad6b804..586c294 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
@@ -28,19 +28,16 @@
 import static com.google.gerrit.extensions.common.ListChangesOption.LABELS;
 import static com.google.gerrit.extensions.common.ListChangesOption.MESSAGES;
 import static com.google.gerrit.extensions.common.ListChangesOption.REVIEWED;
+import static com.google.gerrit.extensions.common.ListChangesOption.WEB_LINKS;
 
 import com.google.common.base.Joiner;
 import com.google.common.base.Objects;
 import com.google.common.base.Optional;
-import com.google.common.cache.CacheBuilder;
-import com.google.common.cache.CacheLoader;
-import com.google.common.cache.LoadingCache;
 import com.google.common.collect.HashBasedTable;
 import com.google.common.collect.HashMultimap;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.LinkedHashMultimap;
-import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Multimap;
@@ -59,6 +56,7 @@
 import com.google.gerrit.extensions.common.GitPerson;
 import com.google.gerrit.extensions.common.ListChangesOption;
 import com.google.gerrit.extensions.common.RevisionInfo;
+import com.google.gerrit.extensions.common.WebLinkInfo;
 import com.google.gerrit.extensions.config.DownloadCommand;
 import com.google.gerrit.extensions.config.DownloadScheme;
 import com.google.gerrit.extensions.registration.DynamicMap;
@@ -73,85 +71,64 @@
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.PatchSetInfo;
 import com.google.gerrit.reviewdb.client.PatchSetInfo.ParentInfo;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.UserIdentity;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.WebLinks;
 import com.google.gerrit.server.account.AccountInfo;
 import com.google.gerrit.server.extensions.webui.UiActions;
 import com.google.gerrit.server.git.LabelNormalizer;
+import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeData.ChangedLines;
 import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.EnumSet;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.TreeMap;
-import java.util.concurrent.ExecutionException;
 
 public class ChangeJson {
   private static final Logger log = LoggerFactory.getLogger(ChangeJson.class);
-  private static final ResultSet<ChangeMessage> NO_MESSAGES =
-      new ResultSet<ChangeMessage>() {
-        @Override
-        public Iterator<ChangeMessage> iterator() {
-          return toList().iterator();
-        }
-
-        @Override
-        public List<ChangeMessage> toList() {
-          return Collections.emptyList();
-        }
-
-        @Override
-        public void close() {
-        }
-      };
+  private static final List<ChangeMessage> NO_MESSAGES =
+      ImmutableList.of();
 
   private final Provider<ReviewDb> db;
   private final LabelNormalizer labelNormalizer;
   private final Provider<CurrentUser> userProvider;
   private final AnonymousUser anonymous;
   private final IdentifiedUser.GenericFactory userFactory;
-  private final ProjectControl.GenericFactory projectControlFactory;
   private final ChangeData.Factory changeDataFactory;
   private final PatchSetInfoFactory patchSetInfoFactory;
-  private final ChangesCollection changes;
   private final FileInfoJson fileInfoJson;
   private final AccountInfo.Loader.Factory accountLoaderFactory;
   private final DynamicMap<DownloadScheme> downloadSchemes;
   private final DynamicMap<DownloadCommand> downloadCommands;
   private final DynamicMap<RestView<ChangeResource>> changeViews;
   private final Revisions revisions;
+  private final Provider<WebLinks> webLinks;
+  private final EnumSet<ListChangesOption> options;
+  private final ChangeMessagesUtil cmUtil;
 
-  private EnumSet<ListChangesOption> options;
   private AccountInfo.Loader accountLoader;
-  private ChangeControl lastControl;
-  private Set<Change.Id> reviewed;
-  private LoadingCache<Project.NameKey, ProjectControl> projectControls;
 
   @Inject
   ChangeJson(
@@ -163,39 +140,30 @@
       ProjectControl.GenericFactory pcf,
       ChangeData.Factory cdf,
       PatchSetInfoFactory psi,
-      ChangesCollection changes,
       FileInfoJson fileInfoJson,
       AccountInfo.Loader.Factory ailf,
       DynamicMap<DownloadScheme> downloadSchemes,
       DynamicMap<DownloadCommand> downloadCommands,
       DynamicMap<RestView<ChangeResource>> changeViews,
-      Revisions revisions) {
+      Revisions revisions,
+      Provider<WebLinks> webLinks,
+      ChangeMessagesUtil cmUtil) {
     this.db = db;
     this.labelNormalizer = ln;
     this.userProvider = user;
     this.anonymous = au;
     this.userFactory = uf;
-    this.projectControlFactory = pcf;
     this.changeDataFactory = cdf;
     this.patchSetInfoFactory = psi;
-    this.changes = changes;
     this.fileInfoJson = fileInfoJson;
     this.accountLoaderFactory = ailf;
     this.downloadSchemes = downloadSchemes;
     this.downloadCommands = downloadCommands;
     this.changeViews = changeViews;
     this.revisions = revisions;
-
+    this.webLinks = webLinks;
+    this.cmUtil = cmUtil;
     options = EnumSet.noneOf(ListChangesOption.class);
-    projectControls = CacheBuilder.newBuilder()
-      .concurrencyLevel(1)
-      .build(new CacheLoader<Project.NameKey, ProjectControl>() {
-        @Override
-        public ProjectControl load(Project.NameKey key)
-            throws NoSuchProjectException, IOException {
-          return projectControlFactory.controlFor(key, userProvider.get());
-        }
-      });
   }
 
   public ChangeJson addOption(ListChangesOption o) {
@@ -227,10 +195,11 @@
   private ChangeInfo format(ChangeData cd, Optional<PatchSet.Id> limitToPsId)
       throws OrmException {
     accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
+    Set<Change.Id> reviewed = Sets.newHashSet();
     if (has(REVIEWED)) {
-      ensureReviewedLoaded(Collections.singleton(cd));
+      reviewed = loadReviewed(Collections.singleton(cd));
     }
-    ChangeInfo res = toChangeInfo(cd, limitToPsId);
+    ChangeInfo res = toChangeInfo(cd, reviewed, limitToPsId);
     accountLoader.fill();
     return res;
   }
@@ -250,15 +219,16 @@
     } else {
       ChangeData.ensureCurrentPatchSetLoaded(all);
     }
+    Set<Change.Id> reviewed = Sets.newHashSet();
     if (has(REVIEWED)) {
-      ensureReviewedLoaded(all);
+      reviewed = loadReviewed(all);
     }
     ChangeData.ensureCurrentApprovalsLoaded(all);
 
     List<List<ChangeInfo>> res = Lists.newArrayListWithCapacity(in.size());
     Map<Change.Id, ChangeInfo> out = Maps.newHashMap();
     for (List<ChangeData> changes : in) {
-      res.add(toChangeInfo(out, changes));
+      res.add(toChangeInfo(out, changes, reviewed));
     }
     accountLoader.fill();
     return res;
@@ -269,12 +239,18 @@
   }
 
   private List<ChangeInfo> toChangeInfo(Map<Change.Id, ChangeInfo> out,
-      List<ChangeData> changes) throws OrmException {
+      List<ChangeData> changes, Set<Change.Id> reviewed) throws OrmException {
     List<ChangeInfo> info = Lists.newArrayListWithCapacity(changes.size());
     for (ChangeData cd : changes) {
       ChangeInfo i = out.get(cd.getId());
       if (i == null) {
-        i = toChangeInfo(cd, Optional.<PatchSet.Id> absent());
+        try {
+          i = toChangeInfo(cd, reviewed, Optional.<PatchSet.Id> absent());
+        } catch (OrmException e) {
+          log.warn(
+              "Omitting corrupt change " + cd.getId() + " from results", e);
+          continue;
+        }
         out.put(cd.getId(), i);
       }
       info.add(i);
@@ -282,15 +258,16 @@
     return info;
   }
 
-  private ChangeInfo toChangeInfo(ChangeData cd,
+  private ChangeInfo toChangeInfo(ChangeData cd, Set<Change.Id> reviewed,
       Optional<PatchSet.Id> limitToPsId) throws OrmException {
+    ChangeControl ctl = cd.changeControl().forUser(userProvider.get());
     ChangeInfo out = new ChangeInfo();
     Change in = cd.change();
     out.project = in.getProject().get();
     out.branch = in.getDest().getShortName();
     out.topic = in.getTopic();
     out.changeId = in.getKey().get();
-    out.mergeable = in.getStatus() != Change.Status.MERGED ? in.isMergeable() : null;
+    out.mergeable = isMergeable(in);
     ChangedLines changedLines = cd.changedLines();
     if (changedLines != null) {
       out.insertions = changedLines.insertions;
@@ -309,26 +286,28 @@
     out.reviewed = in.getStatus().isOpen()
         && has(REVIEWED)
         && reviewed.contains(cd.getId()) ? true : null;
-    out.labels = labelsFor(cd, has(LABELS), has(DETAILED_LABELS));
+    out.labels = labelsFor(ctl, cd, has(LABELS), has(DETAILED_LABELS));
 
     if (out.labels != null && has(DETAILED_LABELS)) {
       // If limited to specific patch sets but not the current patch set, don't
       // list permitted labels, since users can't vote on those patch sets.
       if (!limitToPsId.isPresent()
           || limitToPsId.get().equals(in.currentPatchSetId())) {
-        out.permittedLabels = permittedLabels(cd);
+        out.permittedLabels = permittedLabels(ctl, cd);
       }
-      out.removableReviewers = removableReviewers(cd, out.labels.values());
+      out.removableReviewers = removableReviewers(ctl, cd, out.labels.values());
     }
+
+    Map<PatchSet.Id, PatchSet> src = loadPatchSets(cd, limitToPsId);
     if (has(MESSAGES)) {
-      out.messages = messages(cd);
+      out.messages = messages(ctl, cd, src);
     }
     out.finish();
 
     if (has(ALL_REVISIONS)
         || has(CURRENT_REVISION)
         || limitToPsId.isPresent()) {
-      out.revisions = revisions(cd, limitToPsId);
+      out.revisions = revisions(ctl, cd, limitToPsId, out.project, src);
       if (out.revisions != null) {
         for (Map.Entry<String, RevisionInfo> entry : out.revisions.entrySet()) {
           if (entry.getValue().isCurrent) {
@@ -343,40 +322,27 @@
       out.actions = Maps.newTreeMap();
       for (UiAction.Description d : UiActions.from(
           changeViews,
-          changes.parse(control(cd)),
+          new ChangeResource(ctl),
           userProvider)) {
         out.actions.put(d.getId(), new ActionInfo(d));
       }
     }
-    lastControl = null;
     return out;
   }
 
-  private ChangeControl control(ChangeData cd) throws OrmException {
-    if (lastControl != null
-        && cd.getId().equals(lastControl.getChange().getId())) {
-      return lastControl;
+  private Boolean isMergeable(Change c) {
+    if (c.getStatus() == Change.Status.MERGED
+        || c.getLastSha1MergeTested() == null) {
+      return null;
     }
-    ChangeControl ctrl;
-    try {
-      if (cd.hasChangeControl()) {
-        ctrl = cd.changeControl().forUser(userProvider.get());
-      } else {
-        ctrl = projectControls.get(cd.change().getProject())
-            .controlFor(cd.change());
-      }
-    } catch (NoSuchChangeException | ExecutionException e) {
-      throw new OrmException(e);
-    }
-    lastControl = ctrl;
-    return ctrl;
+    return c.isMergeable();
   }
 
-  private List<SubmitRecord> submitRecords(ChangeData cd) throws OrmException {
+  private List<SubmitRecord> submitRecords(ChangeControl ctl, ChangeData cd)
+      throws OrmException {
     if (cd.getSubmitRecords() != null) {
       return cd.getSubmitRecords();
     }
-    ChangeControl ctl = control(cd);
     if (ctl == null) {
       return ImmutableList.of();
     }
@@ -388,31 +354,30 @@
     return cd.getSubmitRecords();
   }
 
-  private Map<String, LabelInfo> labelsFor(ChangeData cd, boolean standard,
+  private Map<String, LabelInfo> labelsFor(ChangeControl ctl, ChangeData cd, boolean standard,
       boolean detailed) throws OrmException {
     if (!standard && !detailed) {
       return null;
     }
 
-    ChangeControl ctl = control(cd);
     if (ctl == null) {
       return null;
     }
 
     LabelTypes labelTypes = ctl.getLabelTypes();
     if (cd.change().getStatus().isOpen()) {
-      return labelsForOpenChange(cd, labelTypes, standard, detailed);
+      return labelsForOpenChange(ctl, cd, labelTypes, standard, detailed);
     } else {
       return labelsForClosedChange(cd, labelTypes, standard, detailed);
     }
   }
 
-  private Map<String, LabelInfo> labelsForOpenChange(ChangeData cd,
-      LabelTypes labelTypes, boolean standard, boolean detailed)
+  private Map<String, LabelInfo> labelsForOpenChange(ChangeControl ctl,
+      ChangeData cd, LabelTypes labelTypes, boolean standard, boolean detailed)
       throws OrmException {
-    Map<String, LabelInfo> labels = initLabels(cd, labelTypes, standard);
+    Map<String, LabelInfo> labels = initLabels(ctl, cd, labelTypes, standard);
     if (detailed) {
-      setAllApprovals(cd, labels);
+      setAllApprovals(ctl, cd, labels);
     }
     for (Map.Entry<String, LabelInfo> e : labels.entrySet()) {
       LabelType type = labelTypes.byLabel(e.getKey());
@@ -435,12 +400,11 @@
     return labels;
   }
 
-  private Map<String, LabelInfo> initLabels(ChangeData cd,
+  private Map<String, LabelInfo> initLabels(ChangeControl ctl, ChangeData cd,
       LabelTypes labelTypes, boolean standard) throws OrmException {
     // Don't use Maps.newTreeMap(Comparator) due to OpenJDK bug 100167.
-    Map<String, LabelInfo> labels =
-        new TreeMap<String, LabelInfo>(labelTypes.nameComparator());
-    for (SubmitRecord rec : submitRecords(cd)) {
+    Map<String, LabelInfo> labels = new TreeMap<>(labelTypes.nameComparator());
+    for (SubmitRecord rec : submitRecords(ctl, cd)) {
       if (rec.labels == null) {
         continue;
       }
@@ -498,29 +462,20 @@
     }
   }
 
-  private void setAllApprovals(ChangeData cd,
+  private void setAllApprovals(ChangeControl baseCtrl, ChangeData cd,
       Map<String, LabelInfo> labels) throws OrmException {
-    ChangeControl baseCtrl = control(cd);
-    if (baseCtrl == null) {
-      return;
-    }
-
-    // All users ever added, even if they can't vote on one or all labels.
+    // Include a user in the output for this label if either:
+    //  - They are an explicit reviewer.
+    //  - They ever voted on this change.
     Set<Account.Id> allUsers = Sets.newHashSet();
-    ListMultimap<PatchSet.Id, PatchSetApproval> allApprovals =
-        cd.approvals();
-    for (PatchSetApproval psa : allApprovals.values()) {
+    allUsers.addAll(cd.reviewers().values());
+    for (PatchSetApproval psa : cd.approvals().values()) {
       allUsers.add(psa.getAccountId());
     }
 
-    List<PatchSetApproval> currentList =
-        allApprovals.get(baseCtrl.getChange().currentPatchSetId());
-    // Most recent, normalized vote on each label for the current patch set by
-    // each user (may be 0).
     Table<Account.Id, String, PatchSetApproval> current = HashBasedTable.create(
         allUsers.size(), baseCtrl.getLabelTypes().getLabelTypes().size());
-    for (PatchSetApproval psa :
-        labelNormalizer.normalize(baseCtrl, currentList).getNormalized()) {
+    for (PatchSetApproval psa : cd.currentApprovals()) {
       current.put(psa.getAccountId(), psa.getLabel(), psa);
     }
 
@@ -541,9 +496,9 @@
           value = Integer.valueOf(psa.getValue());
           date = psa.getGranted();
         } else {
-          // Either the user cannot vote on this label, or there just wasn't a
-          // dummy approval for this label. Explicitly check whether the user
-          // can vote on this label.
+          // Either the user cannot vote on this label, or they were added as a
+          // reviewer but have not responded yet. Explicitly check whether the
+          // user can vote on this label.
           value = labelNormalizer.canVote(ctl, lt, accountId) ? 0 : null;
         }
         e.getValue().addApproval(approvalInfo(accountId, value, date));
@@ -559,22 +514,22 @@
       allUsers.add(psa.getAccountId());
     }
 
+    // We can only approximately reconstruct what the submit rule evaluator
+    // would have done. These should really come from a stored submit record.
     Set<String> labelNames = Sets.newHashSet();
     Multimap<Account.Id, PatchSetApproval> current = HashMultimap.create();
     for (PatchSetApproval a : cd.currentApprovals()) {
       LabelType type = labelTypes.byLabel(a.getLabelId());
-      if (type != null && a.getValue() != 0) {
+      if (type != null) {
         labelNames.add(type.getName());
+        // Not worth the effort to distinguish between votable/non-votable for 0
+        // values on closed changes, since they can't vote anyway.
         current.put(a.getAccountId(), a);
       }
     }
 
-    // We can only approximately reconstruct what the submit rule evaluator
-    // would have done. These should really come from a stored submit record.
-    //
     // Don't use Maps.newTreeMap(Comparator) due to OpenJDK bug 100167.
-    Map<String, LabelInfo> labels =
-        new TreeMap<String, LabelInfo>(labelTypes.nameComparator());
+    Map<String, LabelInfo> labels = new TreeMap<>(labelTypes.nameComparator());
     for (String name : labelNames) {
       LabelType type = labelTypes.byLabel(name);
       LabelInfo li = new LabelInfo();
@@ -632,6 +587,7 @@
   }
 
   private void setLabelValues(LabelType type, LabelInfo label) {
+    label.defaultValue = type.getDefaultValue();
     label.values = Maps.newLinkedHashMap();
     for (LabelValue v : type.getValues()) {
       label.values.put(v.formatValue(), v.getText());
@@ -641,16 +597,15 @@
     }
   }
 
-  private Map<String, Collection<String>> permittedLabels(ChangeData cd)
+  private Map<String, Collection<String>> permittedLabels(ChangeControl ctl, ChangeData cd)
       throws OrmException {
-    ChangeControl ctl = control(cd);
     if (ctl == null) {
       return null;
     }
 
     LabelTypes labelTypes = ctl.getLabelTypes();
     SetMultimap<String, String> permitted = LinkedHashMultimap.create();
-    for (SubmitRecord rec : submitRecords(cd)) {
+    for (SubmitRecord rec : submitRecords(ctl, cd)) {
       if (rec.labels == null) {
         continue;
       }
@@ -681,10 +636,10 @@
     return permitted.asMap();
   }
 
-  private Collection<ChangeMessageInfo> messages(ChangeData cd)
+  private Collection<ChangeMessageInfo> messages(ChangeControl ctl, ChangeData cd,
+      Map<PatchSet.Id, PatchSet> map)
       throws OrmException {
-    List<ChangeMessage> messages =
-        db.get().changeMessages().byChange(cd.getId()).toList();
+    List<ChangeMessage> messages = cmUtil.byChange(db.get(), cd.notes());
     if (messages.isEmpty()) {
       return Collections.emptyList();
     }
@@ -701,25 +656,22 @@
         Lists.newArrayListWithCapacity(messages.size());
     for (ChangeMessage message : messages) {
       PatchSet.Id patchNum = message.getPatchSetId();
-
-      ChangeMessageInfo cmi = new ChangeMessageInfo();
-      cmi.id = message.getKey().get();
-      cmi.author = accountLoader.get(message.getAuthor());
-      cmi.date = message.getWrittenOn();
-      cmi.message = message.getMessage();
-      cmi._revisionNumber = patchNum != null ? patchNum.get() : null;
-      result.add(cmi);
+      PatchSet ps = patchNum != null ? map.get(patchNum) : null;
+      if (patchNum == null || ctl.isPatchVisible(ps, db.get())) {
+        ChangeMessageInfo cmi = new ChangeMessageInfo();
+        cmi.id = message.getKey().get();
+        cmi.author = accountLoader.get(message.getAuthor());
+        cmi.date = message.getWrittenOn();
+        cmi.message = message.getMessage();
+        cmi._revisionNumber = patchNum != null ? patchNum.get() : null;
+        result.add(cmi);
+      }
     }
     return result;
   }
 
-  private Collection<AccountInfo> removableReviewers(ChangeData cd,
+  private Collection<AccountInfo> removableReviewers(ChangeControl ctl, ChangeData cd,
       Collection<LabelInfo> labels) throws OrmException {
-    ChangeControl ctl = control(cd);
-    if (ctl == null) {
-      return null;
-    }
-
     Set<Account.Id> fixed = Sets.newHashSetWithExpectedSize(labels.size());
     Set<Account.Id> removable = Sets.newHashSetWithExpectedSize(labels.size());
     for (LabelInfo label : labels) {
@@ -743,40 +695,37 @@
     return result;
   }
 
-  private void ensureReviewedLoaded(Iterable<ChangeData> all)
+  private Set<Change.Id> loadReviewed(Iterable<ChangeData> all)
       throws OrmException {
-    reviewed = Sets.newHashSet();
+    Set<Change.Id> reviewed = Sets.newHashSet();
     if (userProvider.get().isIdentifiedUser()) {
       Account.Id self = ((IdentifiedUser) userProvider.get()).getAccountId();
       for (List<ChangeData> batch : Iterables.partition(all, 50)) {
-        List<ResultSet<ChangeMessage>> m =
+        List<List<ChangeMessage>> m =
             Lists.newArrayListWithCapacity(batch.size());
         for (ChangeData cd : batch) {
           PatchSet.Id ps = cd.change().currentPatchSetId();
           if (ps != null && cd.change().getStatus().isOpen()) {
-            m.add(db.get().changeMessages().byPatchSet(ps));
+            m.add(cmUtil.byPatchSet(db.get(), cd.notes(), ps));
           } else {
             m.add(NO_MESSAGES);
           }
         }
         for (int i = 0; i < m.size(); i++) {
-          if (isChangeReviewed(self, batch.get(i), m.get(i).toList())) {
+          if (isChangeReviewed(self, batch.get(i), m.get(i))) {
             reviewed.add(batch.get(i).getId());
           }
         }
       }
     }
+    return reviewed;
   }
 
   private boolean isChangeReviewed(Account.Id self, ChangeData cd,
       List<ChangeMessage> msgs) throws OrmException {
     // Sort messages to keep the most recent ones at the beginning.
-    Collections.sort(msgs, new Comparator<ChangeMessage>() {
-      @Override
-      public int compare(ChangeMessage a, ChangeMessage b) {
-        return b.getWrittenOn().compareTo(a.getWrittenOn());
-      }
-    });
+    msgs = ChangeNotes.MESSAGE_BY_TIME.sortedCopy(msgs);
+    Collections.reverse(msgs);
 
     Account.Id changeOwnerId = cd.change().getOwner();
     for (ChangeMessage cm : msgs) {
@@ -789,15 +738,24 @@
     return false;
   }
 
-  private Map<String, RevisionInfo> revisions(ChangeData cd,
-      Optional<PatchSet.Id> limitToPsId) throws OrmException {
-    ChangeControl ctl = control(cd);
-    if (ctl == null) {
-      return null;
+  private Map<String, RevisionInfo> revisions(ChangeControl ctl, ChangeData cd,
+      Optional<PatchSet.Id> limitToPsId, String project,
+      Map<PatchSet.Id, PatchSet> map) throws OrmException {
+    Map<String, RevisionInfo> res = Maps.newLinkedHashMap();
+    for (PatchSet in : map.values()) {
+      if ((has(ALL_REVISIONS)
+          || in.getId().equals(cd.change().currentPatchSetId()))
+          && ctl.isPatchVisible(in, db.get())) {
+        res.put(in.getRevision().get(), toRevisionInfo(ctl, cd, in, project));
+      }
     }
+    return res;
+  }
 
+  private Map<PatchSet.Id, PatchSet> loadPatchSets(ChangeData cd,
+      Optional<PatchSet.Id> limitToPsId) throws OrmException {
     Collection<PatchSet> src;
-    if (has(ALL_REVISIONS)) {
+    if (has(ALL_REVISIONS) || has(MESSAGES)) {
       src = cd.patches();
     } else {
       PatchSet ps;
@@ -815,23 +773,20 @@
       }
       src = Collections.singletonList(ps);
     }
-
-    Map<String, RevisionInfo> res = Maps.newLinkedHashMap();
-    for (PatchSet in : src) {
-      if (ctl.isPatchVisible(in, db.get())) {
-        res.put(in.getRevision().get(), toRevisionInfo(cd, in));
-      }
+    Map<PatchSet.Id, PatchSet> map = Maps.newHashMapWithExpectedSize(src.size());
+    for (PatchSet patchSet : src) {
+      map.put(patchSet.getId(), patchSet);
     }
-    return res;
+    return map;
   }
 
-  private RevisionInfo toRevisionInfo(ChangeData cd, PatchSet in)
-      throws OrmException {
+  private RevisionInfo toRevisionInfo(ChangeControl ctl, ChangeData cd,
+      PatchSet in, String project) throws OrmException {
     RevisionInfo out = new RevisionInfo();
     out.isCurrent = in.getId().equals(cd.change().currentPatchSetId());
     out._number = in.getId().get();
     out.draft = in.isDraft() ? true : null;
-    out.fetch = makeFetchMap(cd, in);
+    out.fetch = makeFetchMap(ctl, cd, in);
 
     if (has(ALL_COMMITS) || (out.isCurrent && has(CURRENT_COMMIT))) {
       try {
@@ -856,7 +811,7 @@
       out.actions = Maps.newTreeMap();
       for (UiAction.Description d : UiActions.from(
           revisions,
-          new RevisionResource(changes.parse(control(cd)), in),
+          new RevisionResource(new ChangeResource(ctl), in),
           userProvider)) {
         out.actions.put(d.getId(), new ActionInfo(d));
       }
@@ -873,6 +828,13 @@
           : null;
     }
 
+    if (has(WEB_LINKS)) {
+      out.webLinks = Lists.newArrayList();
+      for (WebLinkInfo link : webLinks.get().getPatchSetLinks(
+          project, in.getRevision().get())) {
+        out.webLinks.add(link);
+      }
+    }
     return out;
   }
 
@@ -885,7 +847,6 @@
     commit.committer = toGitPerson(info.getCommitter());
     commit.subject = info.getSubject();
     commit.message = info.getMessage();
-
     for (ParentInfo parent : info.getParents()) {
       CommitInfo i = new CommitInfo();
       i.commit = parent.id.get();
@@ -895,7 +856,7 @@
     return commit;
   }
 
-  private Map<String, FetchInfo> makeFetchMap(ChangeData cd, PatchSet in)
+  private Map<String, FetchInfo> makeFetchMap(ChangeControl ctl, ChangeData cd, PatchSet in)
       throws OrmException {
     Map<String, FetchInfo> r = Maps.newLinkedHashMap();
 
@@ -907,7 +868,6 @@
         continue;
       }
 
-      ChangeControl ctl = control(cd);
       if (!scheme.isAuthSupported()
           && !ctl.forUser(anonymous).isPatchVisible(in, db.get())) {
         continue;
@@ -951,7 +911,6 @@
   }
 
   public static class ChangeInfo {
-    public final String kind = "gerritcodereview#change";
     public String id;
     public String project;
     public String branch;
@@ -1002,6 +961,7 @@
     public Map<String, String> values;
 
     public Short value;
+    public Short defaultValue;
     public Boolean optional;
     public Boolean blocking;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCache.java
index 2c3f4fd..7b55b63 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCache.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2014 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -14,32 +14,13 @@
 
 package com.google.gerrit.server.change;
 
-import static com.google.common.base.Preconditions.checkNotNull;
-
-import com.google.common.base.Objects;
-import com.google.common.cache.CacheLoader;
-import com.google.common.cache.LoadingCache;
-import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.git.MergeUtil;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.project.ProjectState;
-import com.google.inject.Inject;
-import com.google.inject.Module;
-import com.google.inject.Singleton;
-import com.google.inject.name.Named;
 
-import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.merge.ThreeWayMerger;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.io.Serializable;
-import java.util.concurrent.ExecutionException;
 
 /**
  * Cache of {@link ChangeKind} per commit.
@@ -47,149 +28,9 @@
  * This is immutable conditioned on the merge strategy (unless the JGit strategy
  * implementation changes, which might invalidate old entries).
  */
-public class ChangeKindCache {
-  private static final Logger log =
-      LoggerFactory.getLogger(ChangeKindCache.class);
-
-  private static final String ID_CACHE = "change_kind";
-
-  public static Module module() {
-    return new CacheModule() {
-      @Override
-      protected void configure() {
-        cache(ID_CACHE,
-            Key.class,
-            ChangeKind.class)
-          .maximumWeight(0)
-          .loader(Loader.class);
-      }
-    };
-  }
-
-  public static class Key implements Serializable {
-    private static final long serialVersionUID = 1L;
-
-    private final ObjectId prior;
-    private final ObjectId next;
-    private final String strategyName;
-    private transient Repository repo;
-
-    private Key(ObjectId prior, ObjectId next, String strategyName,
-        Repository repo) {
-      this.prior = prior.copy();
-      this.next = next.copy();
-      this.strategyName = strategyName;
-      this.repo = repo;
-    }
-
-    public ObjectId getPrior() {
-      return prior;
-    }
-
-    public ObjectId getNext() {
-      return next;
-    }
-
-    public String getStrategyName() {
-      return strategyName;
-    }
-
-    @Override
-    public boolean equals(Object o) {
-      if (o instanceof Key) {
-        Key k = (Key) o;
-        return Objects.equal(prior, k.prior)
-            && Objects.equal(next, k.next)
-            && Objects.equal(strategyName, k.strategyName);
-      }
-      return false;
-    }
-
-    @Override
-    public int hashCode() {
-      return Objects.hashCode(prior, next, strategyName);
-    }
-  }
-
-  @Singleton
-  private static class Loader extends CacheLoader<Key, ChangeKind> {
-    @Override
-    public ChangeKind load(Key key) throws IOException {
-      RevWalk walk = new RevWalk(key.repo);
-      try {
-        RevCommit prior = walk.parseCommit(key.prior);
-        walk.parseBody(prior);
-        RevCommit next = walk.parseCommit(key.next);
-        walk.parseBody(next);
-
-        if (!next.getFullMessage().equals(prior.getFullMessage())) {
-          if (next.getTree() == prior.getTree() && isSameParents(prior, next)) {
-            return ChangeKind.NO_CODE_CHANGE;
-          } else {
-            return ChangeKind.REWORK;
-          }
-        }
-
-        if (prior.getParentCount() != 1 || next.getParentCount() != 1) {
-          // Trivial rebases done by machine only work well on 1 parent.
-          return ChangeKind.REWORK;
-        }
-
-        if (next.getTree() == prior.getTree() &&
-           isSameParents(prior, next)) {
-          return ChangeKind.TRIVIAL_REBASE;
-        }
-
-        // A trivial rebase can be detected by looking for the next commit
-        // having the same tree as would exist when the prior commit is
-        // cherry-picked onto the next commit's new first parent.
-        ThreeWayMerger merger = MergeUtil.newThreeWayMerger(
-            key.repo, MergeUtil.createDryRunInserter(key.repo), key.strategyName);
-        merger.setBase(prior.getParent(0));
-        if (merger.merge(next.getParent(0), prior)
-            && merger.getResultTreeId().equals(next.getTree())) {
-          return ChangeKind.TRIVIAL_REBASE;
-        } else {
-          return ChangeKind.REWORK;
-        }
-      } finally {
-        key.repo = null;
-        walk.close();
-      }
-    }
-
-    private static boolean isSameParents(RevCommit prior, RevCommit next) {
-      if (prior.getParentCount() != next.getParentCount()) {
-        return false;
-      } else if (prior.getParentCount() == 0) {
-        return true;
-      }
-      return prior.getParent(0).equals(next.getParent(0));
-    }
-  }
-
-  private final LoadingCache<Key, ChangeKind> cache;
-  private final boolean useRecursiveMerge;
-
-  @Inject
-  ChangeKindCache(
-      @GerritServerConfig Config serverConfig,
-      @Named(ID_CACHE) LoadingCache<Key, ChangeKind> cache) {
-    this.cache = cache;
-    this.useRecursiveMerge = MergeUtil.useRecursiveMerge(serverConfig);
-  }
-
+public interface ChangeKindCache {
   public ChangeKind getChangeKind(ProjectState project, Repository repo,
-      ObjectId prior, ObjectId next) {
-    checkNotNull(next, "next");
-    String strategyName = MergeUtil.mergeStrategyName(
-        project.isUseContentMerge(), useRecursiveMerge);
-    try {
-      return cache.get(new Key(prior, next, strategyName, repo));
-    } catch (ExecutionException e) {
-      log.warn("Cannot check trivial rebase of new patch set " + next.name()
-          + " in " + project.getProject().getName(), e);
-      return ChangeKind.REWORK;
-    }
-  }
+      ObjectId prior, ObjectId next);
+
+  public ChangeKind getChangeKind(ReviewDb db, Change change, PatchSet patch);
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
new file mode 100644
index 0000000..82d783c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
@@ -0,0 +1,344 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.eclipse.jgit.lib.ObjectIdSerialization.readNotNull;
+import static org.eclipse.jgit.lib.ObjectIdSerialization.writeNotNull;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Objects;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.cache.Weigher;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MergeUtil;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.merge.ThreeWayMerger;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.concurrent.ExecutionException;
+
+public class ChangeKindCacheImpl implements ChangeKindCache {
+  private static final Logger log =
+      LoggerFactory.getLogger(ChangeKindCacheImpl.class);
+
+  private static final String ID_CACHE = "change_kind";
+
+  public static Module module() {
+    return new CacheModule() {
+      @Override
+      protected void configure() {
+        bind(ChangeKindCache.class).to(ChangeKindCacheImpl.class);
+        persist(ID_CACHE, Key.class, ChangeKind.class)
+            .maximumWeight(2 << 20)
+            .weigher(ChangeKindWeigher.class)
+            .loader(Loader.class);
+      }
+    };
+  }
+
+  @VisibleForTesting
+  public static class NoCache implements ChangeKindCache {
+    private final boolean useRecursiveMerge;
+    private final ChangeData.Factory changeDataFactory;
+    private final ProjectCache projectCache;
+    private final GitRepositoryManager repoManager;
+
+
+    @Inject
+    NoCache(
+        @GerritServerConfig Config serverConfig,
+        ChangeData.Factory changeDataFactory,
+        ProjectCache projectCache,
+        GitRepositoryManager repoManager) {
+      this.useRecursiveMerge = MergeUtil.useRecursiveMerge(serverConfig);
+      this.changeDataFactory = changeDataFactory;
+      this.projectCache = projectCache;
+      this.repoManager = repoManager;
+    }
+
+    @Override
+    public ChangeKind getChangeKind(ProjectState project, Repository repo,
+        ObjectId prior, ObjectId next) {
+      try {
+        return new Loader().load(
+            new Key(repo, prior, next, useRecursiveMerge));
+      } catch (IOException e) {
+        log.warn("Cannot check trivial rebase of new patch set " + next.name()
+            + " in " + project.getProject().getName(), e);
+        return ChangeKind.REWORK;
+      }
+    }
+
+    @Override
+    public ChangeKind getChangeKind(ReviewDb db, Change change, PatchSet patch) {
+      return getChangeKindInternal(this, db, change, patch, changeDataFactory,
+          projectCache, repoManager);
+    }
+  }
+
+  public static class Key implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private transient ObjectId prior;
+    private transient ObjectId next;
+    private transient String strategyName;
+
+    private transient Repository repo; // Passed through to loader on miss.
+
+    private Key(Repository repo, ObjectId prior,
+        ObjectId next, boolean useRecursiveMerge) {
+      checkNotNull(next, "next");
+      String strategyName = MergeUtil.mergeStrategyName(
+          true, useRecursiveMerge);
+      this.prior = prior.copy();
+      this.next = next.copy();
+      this.strategyName = strategyName;
+      this.repo = repo;
+    }
+
+    public Key(ObjectId prior, ObjectId next, String strategyName) {
+      this.prior = prior;
+      this.next = next;
+      this.strategyName = strategyName;
+    }
+
+    public ObjectId getPrior() {
+      return prior;
+    }
+
+    public ObjectId getNext() {
+      return next;
+    }
+
+    public String getStrategyName() {
+      return strategyName;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (o instanceof Key) {
+        Key k = (Key) o;
+        return Objects.equal(prior, k.prior)
+            && Objects.equal(next, k.next)
+            && Objects.equal(strategyName, k.strategyName);
+      }
+      return false;
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hashCode(prior, next, strategyName);
+    }
+
+    private void writeObject(ObjectOutputStream out) throws IOException {
+      writeNotNull(out, prior);
+      writeNotNull(out, next);
+      out.writeUTF(strategyName);
+    }
+
+    private void readObject(ObjectInputStream in) throws IOException {
+      prior = readNotNull(in);
+      next = readNotNull(in);
+      strategyName = in.readUTF();
+    }
+  }
+
+  @Singleton
+  private static class Loader extends CacheLoader<Key, ChangeKind> {
+    @Override
+    public ChangeKind load(Key key) throws IOException {
+      if (Objects.equal(key.prior, key.next)) {
+        return ChangeKind.NO_CODE_CHANGE;
+      }
+
+      RevWalk walk = new RevWalk(key.repo);
+      try {
+        RevCommit prior = walk.parseCommit(key.prior);
+        walk.parseBody(prior);
+        RevCommit next = walk.parseCommit(key.next);
+        walk.parseBody(next);
+
+        if (!next.getFullMessage().equals(prior.getFullMessage())) {
+          if (next.getTree() == prior.getTree() && isSameParents(prior, next)) {
+            return ChangeKind.NO_CODE_CHANGE;
+          } else {
+            return ChangeKind.REWORK;
+          }
+        }
+
+        if (prior.getParentCount() != 1 || next.getParentCount() != 1) {
+          // Trivial rebases done by machine only work well on 1 parent.
+          return ChangeKind.REWORK;
+        }
+
+        if (next.getTree() == prior.getTree() &&
+           isSameParents(prior, next)) {
+          return ChangeKind.TRIVIAL_REBASE;
+        }
+
+        // A trivial rebase can be detected by looking for the next commit
+        // having the same tree as would exist when the prior commit is
+        // cherry-picked onto the next commit's new first parent.
+        ThreeWayMerger merger = MergeUtil.newThreeWayMerger(
+            key.repo, MergeUtil.createDryRunInserter(key.repo), key.strategyName);
+        merger.setBase(prior.getParent(0));
+        if (merger.merge(next.getParent(0), prior)
+            && merger.getResultTreeId().equals(next.getTree())) {
+          return ChangeKind.TRIVIAL_REBASE;
+        } else {
+          return ChangeKind.REWORK;
+        }
+      } finally {
+        key.repo = null;
+        walk.close();
+      }
+    }
+
+    private static boolean isSameParents(RevCommit prior, RevCommit next) {
+      if (prior.getParentCount() != next.getParentCount()) {
+        return false;
+      } else if (prior.getParentCount() == 0) {
+        return true;
+      }
+      return prior.getParent(0).equals(next.getParent(0));
+    }
+  }
+
+  public static class ChangeKindWeigher implements Weigher<Key, ChangeKind> {
+    @Override
+    public int weigh(Key key, ChangeKind changeKind) {
+      return 16 + 2*36 + 2*key.strategyName.length() // Size of Key, 64 bit JVM
+          + 2*changeKind.name().length(); // Size of ChangeKind, 64 bit JVM
+    }
+  }
+
+  private final LoadingCache<Key, ChangeKind> cache;
+  private final boolean useRecursiveMerge;
+  private final ChangeData.Factory changeDataFactory;
+  private final ProjectCache projectCache;
+  private final GitRepositoryManager repoManager;
+
+  @Inject
+  ChangeKindCacheImpl(
+      @GerritServerConfig Config serverConfig,
+      @Named(ID_CACHE) LoadingCache<Key, ChangeKind> cache,
+      ChangeData.Factory changeDataFactory,
+      ProjectCache projectCache,
+      GitRepositoryManager repoManager) {
+    this.cache = cache;
+    this.useRecursiveMerge = MergeUtil.useRecursiveMerge(serverConfig);
+    this.changeDataFactory = changeDataFactory;
+    this.projectCache = projectCache;
+    this.repoManager = repoManager;
+  }
+
+  @Override
+  public ChangeKind getChangeKind(ProjectState project, Repository repo,
+      ObjectId prior, ObjectId next) {
+    try {
+      return cache.get(new Key(repo, prior, next, useRecursiveMerge));
+    } catch (ExecutionException e) {
+      log.warn("Cannot check trivial rebase of new patch set " + next.name()
+          + " in " + project.getProject().getName(), e);
+      return ChangeKind.REWORK;
+    }
+  }
+
+  @Override
+  public ChangeKind getChangeKind(ReviewDb db, Change change, PatchSet patch) {
+    return getChangeKindInternal(this, db, change, patch, changeDataFactory,
+        projectCache, repoManager);
+  }
+
+  private static ChangeKind getChangeKindInternal(
+      ChangeKindCache cache,
+      ReviewDb db,
+      Change change,
+      PatchSet patch,
+      ChangeData.Factory changeDataFactory,
+      ProjectCache projectCache,
+      GitRepositoryManager repoManager) {
+    Repository repo = null;
+    // TODO - dborowitz: add NEW_CHANGE type for default.
+    ChangeKind kind = ChangeKind.REWORK;
+    // Trivial case: if we're on the first patch, we don't need to open
+    // the repository.
+    if (patch.getId().get() > 1) {
+      try {
+        ProjectState projectState = projectCache.checkedGet(change.getProject());
+
+        repo = repoManager.openRepository(change.getProject());
+
+        ChangeData cd = changeDataFactory.create(db, change);
+        Collection<PatchSet> patchSetCollection = cd.patches();
+        PatchSet priorPs = patch;
+        for (PatchSet ps : patchSetCollection) {
+          if (ps.getId().get() < patch.getId().get() &&
+              (ps.getId().get() > priorPs.getId().get() ||
+                  priorPs == patch)) {
+            // We only want the previous patch set, so walk until the last one
+            priorPs = ps;
+          }
+        }
+
+        // If we still think the previous patch is the current patch,
+        // we only have one patch set.  Return the default.
+        // This can happen if a user creates a draft, uploads a second patch,
+        // and deletes the draft.
+        if (priorPs != patch) {
+          kind =
+              cache.getChangeKind(projectState, repo,
+                  ObjectId.fromString(priorPs.getRevision().get()),
+                  ObjectId.fromString(patch.getRevision().get()));
+        }
+      } catch (IOException | OrmException e) {
+        // Do nothing; assume we have a complex change
+        log.warn("Unable to get change kind for patchSet " + patch.getPatchSetId() +
+            "of change " + change.getChangeId(), e);
+      } finally {
+        if (repo != null) {
+          repo.close();
+        }
+      }
+    }
+    return kind;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java
index cfec2e5..d3e97ca 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java
@@ -17,8 +17,10 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AcceptsPost;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestCollection;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
@@ -32,18 +34,22 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import java.io.IOException;
 import java.util.Collections;
 import java.util.List;
 
+@Singleton
 public class ChangesCollection implements
-    RestCollection<TopLevelResource, ChangeResource> {
+    RestCollection<TopLevelResource, ChangeResource>,
+    AcceptsPost<TopLevelResource> {
   private final Provider<ReviewDb> db;
   private final Provider<CurrentUser> user;
   private final ChangeControl.GenericFactory changeControlFactory;
   private final Provider<QueryChanges> queryFactory;
   private final DynamicMap<RestView<ChangeResource>> views;
+  private final CreateChange createChange;
   private final ChangeIndexer changeIndexer;
 
   @Inject
@@ -53,12 +59,14 @@
       ChangeControl.GenericFactory changeControlFactory,
       Provider<QueryChanges> queryFactory,
       DynamicMap<RestView<ChangeResource>> views,
+      CreateChange createChange,
       ChangeIndexer changeIndexer) {
     this.db = dbProvider;
     this.user = user;
     this.changeControlFactory = changeControlFactory;
     this.queryFactory = queryFactory;
     this.views = views;
+    this.createChange = createChange;
     this.changeIndexer = changeIndexer;
   }
 
@@ -141,4 +149,10 @@
         triplet.getBranchNameKey(),
         triplet.getChangeKey()).toList();
   }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public CreateChange post(TopLevelResource parent) throws RestApiException {
+    return createChange;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
index fae8971..d034a10 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
@@ -34,18 +34,20 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import java.io.IOException;
 
+@Singleton
 public class CherryPick implements RestModifyView<RevisionResource, CherryPickInput>,
     UiAction<RevisionResource> {
   private final Provider<ReviewDb> dbProvider;
-  private final Provider<CherryPickChange> cherryPickChange;
+  private final CherryPickChange cherryPickChange;
   private final ChangeJson json;
 
   @Inject
   CherryPick(Provider<ReviewDb> dbProvider,
-      Provider<CherryPickChange> cherryPickChange,
+      CherryPickChange cherryPickChange,
       ChangeJson json) {
     this.dbProvider = dbProvider;
     this.cherryPickChange = cherryPickChange;
@@ -83,7 +85,7 @@
 
     final PatchSet.Id patchSetId = revision.getPatchSet().getId();
     try {
-      Change.Id cherryPickedChangeId = cherryPickChange.get().cherryPick(
+      Change.Id cherryPickedChangeId = cherryPickChange.cherryPick(
           patchSetId, input.message,
           input.destination, refControl);
       return json.format(cherryPickedChangeId);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
index ce025ef..fd23f33 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.events.CommitReceivedEvent;
@@ -39,6 +40,8 @@
 import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
@@ -57,30 +60,34 @@
 
 import java.io.IOException;
 import java.util.List;
+import java.util.TimeZone;
 
+@Singleton
 public class CherryPickChange {
 
   private static final FooterKey CHANGE_ID = new FooterKey("Change-Id");
 
-  private final ReviewDb db;
+  private final Provider<ReviewDb> db;
   private final GitRepositoryManager gitManager;
-  private final PersonIdent myIdent;
-  private final IdentifiedUser currentUser;
+  private final TimeZone serverTimeZone;
+  private final Provider<CurrentUser> currentUser;
   private final CommitValidators.Factory commitValidatorsFactory;
   private final ChangeInserter.Factory changeInserterFactory;
   private final PatchSetInserter.Factory patchSetInserterFactory;
   final MergeUtil.Factory mergeUtilFactory;
 
   @Inject
-  CherryPickChange(final ReviewDb db, @GerritPersonIdent final PersonIdent myIdent,
-      final GitRepositoryManager gitManager, final IdentifiedUser currentUser,
+  CherryPickChange(final Provider<ReviewDb> db,
+      @GerritPersonIdent final PersonIdent myIdent,
+      final GitRepositoryManager gitManager,
+      final Provider<CurrentUser> currentUser,
       final CommitValidators.Factory commitValidatorsFactory,
       final ChangeInserter.Factory changeInserterFactory,
       final PatchSetInserter.Factory patchSetInserterFactory,
       final MergeUtil.Factory mergeUtilFactory) {
     this.db = db;
     this.gitManager = gitManager;
-    this.myIdent = myIdent;
+    this.serverTimeZone = myIdent.getTimeZone();
     this.currentUser = currentUser;
     this.commitValidatorsFactory = commitValidatorsFactory;
     this.changeInserterFactory = changeInserterFactory;
@@ -96,7 +103,7 @@
       InvalidChangeOperationException, MergeException {
 
     final Change.Id changeId = patchSetId.getParentKey();
-    final PatchSet patch = db.patchSets().get(patchSetId);
+    final PatchSet patch = db.get().patchSets().get(patchSetId);
     if (patch == null) {
       throw new NoSuchChangeException(changeId);
     }
@@ -105,7 +112,8 @@
           "Cherry Pick: Destination branch cannot be null or empty");
     }
 
-    Project.NameKey project = db.changes().get(changeId).getProject();
+    Project.NameKey project = db.get().changes().get(changeId).getProject();
+    IdentifiedUser identifiedUser = (IdentifiedUser) currentUser.get();
     final Repository git;
     try {
       git = gitManager.openRepository(project);
@@ -128,13 +136,13 @@
             revWalk.parseCommit(ObjectId.fromString(patch.getRevision().get()));
 
         PersonIdent committerIdent =
-            currentUser.newCommitterIdent(myIdent.getWhen(),
-                myIdent.getTimeZone());
+            identifiedUser.newCommitterIdent(TimeUtil.nowTs(),
+                serverTimeZone);
 
         final ObjectId computedChangeId =
             ChangeIdUtil
                 .computeChangeId(commitToCherryPick.getTree(), mergeTip,
-                    commitToCherryPick.getAuthorIdent(), myIdent, message);
+                    commitToCherryPick.getAuthorIdent(), committerIdent, message);
         String commitMessage =
             ChangeIdUtil.insertId(message, computedChangeId).trim() + '\n';
 
@@ -163,9 +171,9 @@
         }
 
         List<Change> destChanges =
-            db.changes()
+            db.get().changes()
                 .byBranchKey(
-                    new Branch.NameKey(db.changes().get(changeId).getProject(),
+                    new Branch.NameKey(db.get().changes().get(changeId).getProject(),
                         destRef.getName()), changeKey).toList();
 
         if (destChanges.size() > 1) {
@@ -176,12 +184,12 @@
           // The change key exists on the destination branch. The cherry pick
           // will be added as a new patch set.
           return insertPatchSet(git, revWalk, destChanges.get(0), patchSetId,
-              cherryPickCommit, refControl, currentUser);
+              cherryPickCommit, refControl, identifiedUser);
         } else {
           // Change key not found on destination branch. We can create a new
           // change.
           return createNewChange(git, revWalk, changeKey, project, patchSetId, destRef,
-              cherryPickCommit, refControl);
+              cherryPickCommit, refControl, identifiedUser);
         }
       } finally {
         revWalk.close();
@@ -193,7 +201,7 @@
 
   private Change.Id insertPatchSet(Repository git, RevWalk revWalk, Change change,
       PatchSet.Id patchSetId, RevCommit cherryPickCommit,
-      RefControl refControl, IdentifiedUser uploader)
+      RefControl refControl, IdentifiedUser identifiedUser)
       throws InvalidChangeOperationException, IOException, OrmException,
       NoSuchChangeException {
     final ChangeControl changeControl =
@@ -201,11 +209,11 @@
     final PatchSetInserter inserter = patchSetInserterFactory
         .create(git, revWalk, changeControl, cherryPickCommit);
     final PatchSet.Id newPatchSetId = inserter.getPatchSetId();
-    final PatchSet current = db.patchSets().get(change.currentPatchSetId());
+    PatchSet current = db.get().patchSets().get(change.currentPatchSetId());
     inserter
       .setMessage("Uploaded patch set " + newPatchSetId.get() + ".")
       .setDraft(current.isDraft())
-      .setUploader(uploader.getAccountId())
+      .setUploader(identifiedUser.getAccountId())
       .setCopyLabels(true)
       .insert();
     return change.getId();
@@ -213,11 +221,12 @@
 
   private Change.Id createNewChange(Repository git, RevWalk revWalk,
       Change.Key changeKey, Project.NameKey project, PatchSet.Id patchSetId,
-      Ref destRef, RevCommit cherryPickCommit, RefControl refControl)
+      Ref destRef, RevCommit cherryPickCommit, RefControl refControl,
+      IdentifiedUser identifiedUser)
       throws OrmException, InvalidChangeOperationException, IOException {
     Change change =
-        new Change(changeKey, new Change.Id(db.nextChangeId()),
-            currentUser.getAccountId(), new Branch.NameKey(project,
+        new Change(changeKey, new Change.Id(db.get().nextChangeId()),
+            identifiedUser.getAccountId(), new Branch.NameKey(project,
                 destRef.getName()), TimeUtil.nowTs());
     ChangeInserter ins =
         changeInserterFactory.create(refControl, change, cherryPickCommit);
@@ -229,7 +238,7 @@
         new CommitReceivedEvent(new ReceiveCommand(ObjectId.zeroId(),
             cherryPickCommit.getId(), newPatchSet.getRefName()), refControl
             .getProjectControl().getProject(), refControl.getRefName(),
-            cherryPickCommit, currentUser);
+            cherryPickCommit, identifiedUser);
 
     try {
       commitValidators.validateForGerritCommits(commitReceivedEvent);
@@ -247,18 +256,20 @@
           change.getDest().getParentKey().get(), ru.getResult()));
     }
 
-    ins.setMessage(buildChangeMessage(patchSetId, change, cherryPickCommit))
+    ins.setMessage(buildChangeMessage(patchSetId, change, cherryPickCommit,
+        identifiedUser))
         .insert();
 
     return change.getId();
   }
 
   private ChangeMessage buildChangeMessage(PatchSet.Id patchSetId, Change dest,
-      RevCommit cherryPickCommit) throws OrmException {
+      RevCommit cherryPickCommit, IdentifiedUser identifiedUser)
+      throws OrmException {
     ChangeMessage cmsg = new ChangeMessage(
         new ChangeMessage.Key(
-            patchSetId.getParentKey(), ChangeUtil.messageUUID(db)),
-        currentUser.getAccountId(), TimeUtil.nowTs(), patchSetId);
+            patchSetId.getParentKey(), ChangeUtil.messageUUID(db.get())),
+            identifiedUser.getAccountId(), TimeUtil.nowTs(), patchSetId);
     String destBranchName = dest.getDest().get();
     StringBuilder msgBuf = new StringBuilder("Patch Set ")
         .append(patchSetId.get())
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentInfo.java
index f4c148a..adc1644 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentInfo.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentInfo.java
@@ -24,8 +24,6 @@
 import java.sql.Timestamp;
 
 public class CommentInfo {
-
-  final String kind = "gerritcodereview#comment";
   String id;
   String path;
   Side side;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Comments.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Comments.java
index 91cfbf8..c987ce8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Comments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Comments.java
@@ -21,22 +21,28 @@
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.PatchLineCommentsUtil;
+import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
+@Singleton
 class Comments implements ChildCollection<RevisionResource, CommentResource> {
   private final DynamicMap<RestView<CommentResource>> views;
-  private final Provider<ListComments> list;
+  private final ListComments list;
   private final Provider<ReviewDb> dbProvider;
+  private final PatchLineCommentsUtil plcUtil;
 
   @Inject
   Comments(DynamicMap<RestView<CommentResource>> views,
-      Provider<ListComments> list,
-      Provider<ReviewDb> dbProvider) {
+      ListComments list, Provider<ReviewDb> dbProvider,
+      PatchLineCommentsUtil plcUtil) {
     this.views = views;
     this.list = list;
     this.dbProvider = dbProvider;
+    this.plcUtil = plcUtil;
   }
 
   @Override
@@ -46,15 +52,17 @@
 
   @Override
   public RestView<RevisionResource> list() {
-    return list.get();
+    return list;
   }
 
   @Override
   public CommentResource parse(RevisionResource rev, IdString id)
       throws ResourceNotFoundException, OrmException {
     String uuid = id.get();
-    for (PatchLineComment c : dbProvider.get().patchComments()
-        .publishedByPatchSet(rev.getPatchSet().getId())) {
+    ChangeNotes notes = rev.getNotes();
+
+    for (PatchLineComment c : plcUtil.publishedByPatchSet(dbProvider.get(),
+        notes, rev.getPatchSet().getId())) {
       if (uuid.equals(c.getKey().get())) {
         return new CommentResource(rev, c);
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
new file mode 100644
index 0000000..04c12c3
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
@@ -0,0 +1,265 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.common.data.Capable;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeStatus;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.api.changes.ChangeInfoMapper;
+import com.google.gerrit.server.events.CommitReceivedEvent;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MergeUtil;
+import com.google.gerrit.server.git.validators.CommitValidationException;
+import com.google.gerrit.server.git.validators.CommitValidators;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.project.ProjectsCollection;
+import com.google.gerrit.server.project.RefControl;
+import com.google.gerrit.server.ssh.NoSshInfo;
+import com.google.gerrit.server.util.TimeUtil;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.util.ChangeIdUtil;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.sql.Timestamp;
+import java.util.List;
+import java.util.TimeZone;
+
+@Singleton
+public class CreateChange implements
+    RestModifyView<TopLevelResource, ChangeInfo> {
+
+  private final Provider<ReviewDb> db;
+  private final GitRepositoryManager gitManager;
+  private final TimeZone serverTimeZone;
+  private final Provider<CurrentUser> userProvider;
+  private final ProjectsCollection projectsCollection;
+  private final CommitValidators.Factory commitValidatorsFactory;
+  private final ChangeInserter.Factory changeInserterFactory;
+  private final ChangeJson json;
+
+  @Inject
+  CreateChange(Provider<ReviewDb> db,
+      GitRepositoryManager gitManager,
+      @GerritPersonIdent PersonIdent myIdent,
+      Provider<CurrentUser> userProvider,
+      ProjectsCollection projectsCollection,
+      CommitValidators.Factory commitValidatorsFactory,
+      ChangeInserter.Factory changeInserterFactory,
+      ChangeJson json) {
+    this.db = db;
+    this.gitManager = gitManager;
+    this.serverTimeZone = myIdent.getTimeZone();
+    this.userProvider = userProvider;
+    this.projectsCollection = projectsCollection;
+    this.commitValidatorsFactory = commitValidatorsFactory;
+    this.changeInserterFactory = changeInserterFactory;
+    this.json = json;
+  }
+
+  @Override
+  public Response<ChangeJson.ChangeInfo> apply(TopLevelResource parent,
+      ChangeInfo input) throws AuthException, OrmException,
+      BadRequestException, UnprocessableEntityException, IOException,
+      InvalidChangeOperationException {
+
+    if (Strings.isNullOrEmpty(input.project)) {
+      throw new BadRequestException("project must be non-empty");
+    }
+
+    if (Strings.isNullOrEmpty(input.branch)) {
+      throw new BadRequestException("branch must be non-empty");
+    }
+
+    if (Strings.isNullOrEmpty(input.subject)) {
+      throw new BadRequestException("commit message must be non-empty");
+    }
+
+    if (input.status != null) {
+      if (input.status != ChangeStatus.NEW
+          && input.status != ChangeStatus.DRAFT) {
+        throw new BadRequestException("unsupported change status");
+      }
+    }
+
+    String refName = input.branch;
+    if (!refName.startsWith(Constants.R_REFS)) {
+      refName = Constants.R_HEADS + input.branch;
+    }
+
+    ProjectResource rsrc = projectsCollection.parse(input.project);
+
+    Capable r = rsrc.getControl().canPushToAtLeastOneRef();
+    if (r != Capable.OK) {
+      throw new AuthException(r.getMessage());
+    }
+
+    RefControl refControl = rsrc.getControl().controlForRef(refName);
+    if (!refControl.canUpload() || !refControl.canRead()) {
+      throw new AuthException("cannot upload review");
+    }
+
+    Project.NameKey project = rsrc.getNameKey();
+    Repository git = gitManager.openRepository(project);
+
+    try {
+      RevWalk rw = new RevWalk(git);
+      try {
+        Ref destRef = git.getRef(refName);
+        if (destRef == null) {
+          throw new UnprocessableEntityException(String.format(
+              "Branch %s does not exist.", refName));
+        }
+
+        Timestamp now = TimeUtil.nowTs();
+        IdentifiedUser me = (IdentifiedUser) userProvider.get();
+        PersonIdent author = me.newCommitterIdent(now, serverTimeZone);
+
+        RevCommit mergeTip = rw.parseCommit(destRef.getObjectId());
+        ObjectId id = ChangeIdUtil.computeChangeId(mergeTip.getTree(),
+            mergeTip, author, author, input.subject);
+        String commitMessage = ChangeIdUtil.insertId(input.subject, id);
+
+        RevCommit c = newCommit(git, rw, author, mergeTip, commitMessage);
+
+        Change change = new Change(
+            getChangeId(id, c),
+            new Change.Id(db.get().nextChangeId()),
+            me.getAccountId(),
+            new Branch.NameKey(project, destRef.getName()),
+            now);
+
+        ChangeInserter ins =
+            changeInserterFactory.create(refControl, change, c);
+
+        validateCommit(git, refControl, c, me, ins);
+        updateRef(git, rw, c, change, ins.getPatchSet());
+
+        change.setTopic(input.topic);
+        change.setStatus(ChangeInfoMapper.changeStatus2Status(input.status));
+        ins.insert();
+
+        return Response.created(json.format(change.getId()));
+      } finally {
+        rw.close();
+      }
+    } finally {
+      git.close();
+    }
+  }
+
+  private void validateCommit(Repository git, RefControl refControl,
+      RevCommit c, IdentifiedUser me, ChangeInserter ins)
+      throws InvalidChangeOperationException {
+    PatchSet newPatchSet = ins.getPatchSet();
+    CommitValidators commitValidators =
+        commitValidatorsFactory.create(refControl, new NoSshInfo(), git);
+    CommitReceivedEvent commitReceivedEvent =
+        new CommitReceivedEvent(new ReceiveCommand(
+            ObjectId.zeroId(),
+            c.getId(),
+            newPatchSet.getRefName()),
+            refControl.getProjectControl().getProject(),
+            refControl.getRefName(),
+            c,
+            me);
+
+    try {
+      commitValidators.validateForGerritCommits(commitReceivedEvent);
+    } catch (CommitValidationException e) {
+      throw new InvalidChangeOperationException(e.getMessage());
+    }
+  }
+
+  private static void updateRef(Repository git, RevWalk rw, RevCommit c,
+      Change change, PatchSet newPatchSet) throws IOException {
+    RefUpdate ru = git.updateRef(newPatchSet.getRefName());
+    ru.setExpectedOldObjectId(ObjectId.zeroId());
+    ru.setNewObjectId(c);
+    ru.disableRefLog();
+    if (ru.update(rw) != RefUpdate.Result.NEW) {
+      throw new IOException(String.format(
+          "Failed to create ref %s in %s: %s", newPatchSet.getRefName(),
+          change.getDest().getParentKey().get(), ru.getResult()));
+    }
+  }
+
+  private static Change.Key getChangeId(ObjectId id, RevCommit emptyCommit) {
+    List<String> idList = emptyCommit.getFooterLines(
+        MergeUtil.CHANGE_ID);
+    Change.Key changeKey = !idList.isEmpty()
+        ? new Change.Key(idList.get(idList.size() - 1).trim())
+        : new Change.Key("I" + id.name());
+    return changeKey;
+  }
+
+  private static RevCommit newCommit(Repository git, RevWalk rw,
+      PersonIdent authorIdent, RevCommit mergeTip, String commitMessage)
+      throws IOException {
+    RevCommit emptyCommit;
+    ObjectInserter oi = git.newObjectInserter();
+    try {
+      CommitBuilder commit = new CommitBuilder();
+      commit.setTreeId(mergeTip.getTree().getId());
+      commit.setParentId(mergeTip);
+      commit.setAuthor(authorIdent);
+      commit.setCommitter(authorIdent);
+      commit.setMessage(commitMessage);
+      emptyCommit = rw.parseCommit(insert(oi, commit));
+    } finally {
+      oi.close();
+    }
+    return emptyCommit;
+  }
+
+  private static ObjectId insert(ObjectInserter inserter,
+      CommitBuilder commit) throws IOException,
+      UnsupportedEncodingException {
+    ObjectId id = inserter.insert(commit);
+    inserter.flush();
+    return id;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraft.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraft.java
index afd0b85..1d2fa406 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraft.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraft.java
@@ -29,9 +29,11 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import java.util.Collections;
 
+@Singleton
 class CreateDraft implements RestModifyView<RevisionResource, Input> {
   private final Provider<ReviewDb> db;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraft.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraft.java
index 588c372..46ae834 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraft.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraft.java
@@ -21,9 +21,11 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import java.util.Collections;
 
+@Singleton
 class DeleteDraft implements RestModifyView<DraftResource, Input> {
   static class Input {
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChange.java
index 59b52a1..28885db 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChange.java
@@ -30,11 +30,13 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import org.eclipse.jgit.lib.Config;
 
 import java.io.IOException;
 
+@Singleton
 public class DeleteDraftChange implements
     RestModifyView<ChangeResource, Input>, UiAction<ChangeResource> {
   public static class Input {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
index 7b9b3d9..46d0ed9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
@@ -34,11 +34,13 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import org.eclipse.jgit.lib.Config;
 
 import java.io.IOException;
 
+@Singleton
 public class DeleteDraftPatchSet implements RestModifyView<RevisionResource, Input>,
     UiAction<RevisionResource> {
   public static class Input {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
index b4dcdd7..e64d3f4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.change.DeleteReviewer.Input;
@@ -37,11 +38,12 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import java.io.IOException;
-import java.util.Collections;
 import java.util.List;
 
+@Singleton
 public class DeleteReviewer implements RestModifyView<ReviewerResource, Input> {
   public static class Input {
   }
@@ -49,6 +51,7 @@
   private final Provider<ReviewDb> dbProvider;
   private final ChangeUpdate.Factory updateFactory;
   private final ApprovalsUtil approvalsUtil;
+  private final ChangeMessagesUtil cmUtil;
   private final ChangeIndexer indexer;
   private final IdentifiedUser.GenericFactory userFactory;
 
@@ -56,11 +59,13 @@
   DeleteReviewer(Provider<ReviewDb> dbProvider,
       ChangeUpdate.Factory updateFactory,
       ApprovalsUtil approvalsUtil,
+      ChangeMessagesUtil cmUtil,
       ChangeIndexer indexer,
       IdentifiedUser.GenericFactory userFactory) {
     this.dbProvider = dbProvider;
     this.updateFactory = updateFactory;
     this.approvalsUtil = approvalsUtil;
+    this.cmUtil = cmUtil;
     this.indexer = indexer;
     this.userFactory = userFactory;
   }
@@ -84,7 +89,7 @@
           if (a.getPatchSetId().equals(control.getChange().currentPatchSetId())
               && a.getValue() != 0) {
             if (msg.length() == 0) {
-              msg.append("Removed the following approvals:\n\n");
+              msg.append("Removed the following votes:\n\n");
             }
             msg.append("* ")
                 .append(a.getLabel()).append(formatLabelValue(a.getValue()))
@@ -109,7 +114,7 @@
                 ((IdentifiedUser) control.getCurrentUser()).getAccountId(),
                 TimeUtil.nowTs(), rsrc.getChange().currentPatchSetId());
         changeMessage.setMessage(msg.toString());
-        db.changeMessages().insert(Collections.singleton(changeMessage));
+        cmUtil.addChangeMessage(db, update, changeMessage);
       }
 
       db.commit();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Drafts.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Drafts.java
index 8fc05be..322faea 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Drafts.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Drafts.java
@@ -26,17 +26,19 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
+@Singleton
 class Drafts implements ChildCollection<RevisionResource, DraftResource> {
   private final DynamicMap<RestView<DraftResource>> views;
   private final Provider<CurrentUser> user;
-  private final Provider<ListDrafts> list;
+  private final ListDrafts list;
   private final Provider<ReviewDb> dbProvider;
 
   @Inject
   Drafts(DynamicMap<RestView<DraftResource>> views,
       Provider<CurrentUser> user,
-      Provider<ListDrafts> list,
+      ListDrafts list,
       Provider<ReviewDb> dbProvider) {
     this.views = views;
     this.user = user;
@@ -52,7 +54,7 @@
   @Override
   public RestView<RevisionResource> list() throws AuthException {
     checkIdentifiedUser();
-    return list.get();
+    return list;
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/EditMessage.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/EditMessage.java
index b1a3290..af81627 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/EditMessage.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/EditMessage.java
@@ -30,8 +30,10 @@
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
@@ -39,6 +41,7 @@
 
 import java.io.IOException;
 
+@Singleton
 class EditMessage implements RestModifyView<RevisionResource, Input>,
     UiAction<RevisionResource> {
   private final ChangeUtil changeUtil;
@@ -76,7 +79,7 @@
           rsrc.getControl(),
           rsrc.getPatchSet().getId(),
           input.message,
-          myIdent));
+          new PersonIdent(myIdent, TimeUtil.nowTs())));
     } catch (InvalidChangeOperationException e) {
       throw new BadRequestException(e.getMessage());
     } catch (NoSuchChangeException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java
index c6b3b5d..6bb7236 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java
@@ -27,11 +27,13 @@
 import com.google.gerrit.server.patch.PatchListKey;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
 import org.eclipse.jgit.lib.ObjectId;
 
 import java.util.Map;
 
+@Singleton
 public class FileInfoJson {
   private final PatchListCache patchListCache;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
index c4a979d..0689aec 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
@@ -41,6 +41,7 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.Repository;
@@ -59,7 +60,8 @@
 import java.util.SortedSet;
 import java.util.concurrent.TimeUnit;
 
-class Files implements ChildCollection<RevisionResource, FileResource> {
+@Singleton
+public class Files implements ChildCollection<RevisionResource, FileResource> {
   private final DynamicMap<RestView<FileResource>> views;
   private final Provider<ListFiles> list;
 
@@ -85,7 +87,7 @@
     return new FileResource(rev, id.get());
   }
 
-  private static final class ListFiles implements RestReadView<RevisionResource> {
+  public static final class ListFiles implements RestReadView<RevisionResource> {
     private static final Logger log = LoggerFactory.getLogger(ListFiles.class);
 
     @Option(name = "--base", metaVar = "revision-id")
@@ -97,7 +99,7 @@
     private final Provider<ReviewDb> db;
     private final Provider<CurrentUser> self;
     private final FileInfoJson fileInfoJson;
-    private final Provider<Revisions> revisions;
+    private final Revisions revisions;
     private final GitRepositoryManager gitManager;
     private final PatchListCache patchListCache;
 
@@ -105,7 +107,7 @@
     ListFiles(Provider<ReviewDb> db,
         Provider<CurrentUser> self,
         FileInfoJson fileInfoJson,
-        Provider<Revisions> revisions,
+        Revisions revisions,
         GitRepositoryManager gitManager,
         PatchListCache patchListCache) {
       this.db = db;
@@ -116,6 +118,11 @@
       this.patchListCache = patchListCache;
     }
 
+    public ListFiles setReviewed(boolean r) {
+      this.reviewed = r;
+      return this;
+    }
+
     @Override
     public Response<?> apply(RevisionResource resource) throws AuthException,
         BadRequestException, ResourceNotFoundException, OrmException {
@@ -127,7 +134,7 @@
 
       PatchSet basePatchSet = null;
       if (base != null) {
-        RevisionResource baseResource = revisions.get().parse(
+        RevisionResource baseResource = revisions.parse(
             resource.getChangeResource(), IdString.fromDecoded(base));
         basePatchSet = baseResource.getPatchSet();
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetArchive.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetArchive.java
index b0fea3c..913f69e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetArchive.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetArchive.java
@@ -15,14 +15,19 @@
 package com.google.gerrit.server.change;
 
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
 import org.eclipse.jgit.api.ArchiveCommand;
 import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -31,18 +36,60 @@
 
 import java.io.IOException;
 import java.io.OutputStream;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
 import java.util.Map;
+import java.util.Set;
 
-class GetArchive implements RestReadView<RevisionResource> {
-  private static final Map<String, ArchiveFormat> formats = ArchiveFormat.init();
+public class GetArchive implements RestReadView<RevisionResource> {
+  @Singleton
+  public static class AllowedFormats {
+    final ImmutableMap<String, ArchiveFormat> extensions;
+    final Set<ArchiveFormat> allowed;
+
+    @Inject
+    AllowedFormats(@GerritServerConfig Config cfg) {
+      Collection<ArchiveFormat> enabled;
+      String v = cfg.getString("download", null, "archive");
+      if (v == null) {
+        enabled = Arrays.asList(ArchiveFormat.values());
+      } else if (v.isEmpty() || "off".equalsIgnoreCase(v)) {
+        enabled = Collections.emptyList();
+      } else {
+        enabled = ConfigUtil.getEnumList(cfg,
+            "download", null, "archive",
+            ArchiveFormat.TGZ);
+      }
+
+      Map<String, ArchiveFormat> exts = new HashMap<>();
+      for (ArchiveFormat format : enabled) {
+        for (String ext : format.getSuffixes()) {
+          exts.put(ext, format);
+        }
+        exts.put(format.name().toLowerCase(), format);
+      }
+      extensions = ImmutableMap.copyOf(exts);
+      allowed = Collections.unmodifiableSet(new LinkedHashSet<>(enabled));
+    }
+
+    public Set<ArchiveFormat> getAllowed() {
+      return allowed;
+    }
+  }
+
   private final GitRepositoryManager repoManager;
+  private final AllowedFormats allowedFormats;
 
   @Option(name = "--format")
   private String format;
 
   @Inject
-  GetArchive(GitRepositoryManager repoManager) {
+  GetArchive(GitRepositoryManager repoManager, AllowedFormats allowedFormats) {
     this.repoManager = repoManager;
+    this.allowedFormats = allowedFormats;
   }
 
   @Override
@@ -51,7 +98,7 @@
     if (Strings.isNullOrEmpty(format)) {
       throw new BadRequestException("format is not specified");
     }
-    final ArchiveFormat f = formats.get("." + format);
+    final ArchiveFormat f = allowedFormats.extensions.get("." + format);
     if (f == null) {
       throw new BadRequestException("unknown archive format");
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetComment.java
index 3606eed..27de91c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetComment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetComment.java
@@ -18,7 +18,9 @@
 import com.google.gerrit.server.account.AccountInfo;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
+@Singleton
 class GetComment implements RestReadView<CommentResource> {
 
   private final AccountInfo.Loader.Factory accountLoaderFactory;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetCommit.java
index 679466a..2cd948e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetCommit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetCommit.java
@@ -22,9 +22,11 @@
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
 import java.util.concurrent.TimeUnit;
 
+@Singleton
 public class GetCommit implements RestReadView<RevisionResource> {
   private final ChangeJson json;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetContent.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetContent.java
index f9b9bf6..62458cd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetContent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetContent.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
 import org.eclipse.jgit.lib.ObjectLoader;
 import org.eclipse.jgit.lib.Repository;
@@ -30,6 +31,7 @@
 import java.io.IOException;
 import java.io.OutputStream;
 
+@Singleton
 public class GetContent implements RestReadView<FileResource> {
   private final GitRepositoryManager repoManager;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
index 7267f7d..0f68182 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
@@ -31,14 +31,16 @@
 import com.google.gerrit.prettify.common.SparseFileContent;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.Patch.ChangeType;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.git.LargeObjectException;
 import com.google.gerrit.server.patch.PatchScriptFactory;
 import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gerrit.server.git.LargeObjectException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 
 import org.eclipse.jgit.diff.Edit;
 import org.eclipse.jgit.diff.ReplaceEdit;
@@ -55,8 +57,9 @@
 import java.util.concurrent.TimeUnit;
 
 public class GetDiff implements RestReadView<FileResource> {
+  private final ProjectCache projectCache;
   private final PatchScriptFactory.Factory patchScriptFactoryFactory;
-  private final Provider<Revisions> revisions;
+  private final Revisions revisions;
 
   @Option(name = "--base", metaVar = "REVISION")
   String base;
@@ -71,8 +74,10 @@
   boolean intraline;
 
   @Inject
-  GetDiff(PatchScriptFactory.Factory patchScriptFactoryFactory,
-      Provider<Revisions> revisions) {
+  GetDiff(ProjectCache projectCache,
+      PatchScriptFactory.Factory patchScriptFactoryFactory,
+      Revisions revisions) {
+    this.projectCache = projectCache;
     this.patchScriptFactoryFactory = patchScriptFactoryFactory;
     this.revisions = revisions;
   }
@@ -82,7 +87,7 @@
       throws ResourceConflictException, ResourceNotFoundException, OrmException {
     PatchSet.Id basePatchSet = null;
     if (base != null) {
-      RevisionResource baseResource = revisions.get().parse(
+      RevisionResource baseResource = revisions.parse(
           resource.getRevision().getChangeResource(), IdString.fromDecoded(base));
       basePatchSet = baseResource.getPatchSet().getId();
     }
@@ -92,14 +97,15 @@
     prefs.setIntralineDifference(intraline);
 
     try {
-      PatchScript ps = patchScriptFactoryFactory.create(
+      PatchScriptFactory psf = patchScriptFactoryFactory.create(
           resource.getRevision().getControl(),
           resource.getPatchKey().getFileName(),
           basePatchSet,
           resource.getPatchKey().getParentKey(),
-          prefs)
-            .call();
-
+          prefs);
+      psf.setLoadHistory(false);
+      psf.setLoadComments(context != AccountDiffPreference.WHOLE_FILE_CONTEXT);
+      PatchScript ps = psf.call();
       Content content = new Content(ps);
       for (Edit edit : ps.getEdits()) {
         if (edit.getType() == Edit.Type.EMPTY) {
@@ -127,18 +133,21 @@
       }
       content.addCommon(ps.getA().size());
 
+      ProjectState state =
+          projectCache.get(resource.getRevision().getChange().getProject());
+
       Result result = new Result();
       if (ps.getDisplayMethodA() != DisplayMethod.NONE) {
         result.metaA = new FileMeta();
         result.metaA.name = Objects.firstNonNull(ps.getOldName(), ps.getNewName());
-        result.metaA.setContentType(ps.getFileModeA(), ps.getMimeTypeA());
+        setContentType(result.metaA, state, ps.getFileModeA(), ps.getMimeTypeA());
         result.metaA.lines = ps.getA().size();
       }
 
       if (ps.getDisplayMethodB() != DisplayMethod.NONE) {
         result.metaB = new FileMeta();
         result.metaB.name = ps.getNewName();
-        result.metaB.setContentType(ps.getFileModeB(), ps.getMimeTypeB());
+        setContentType(result.metaB, state, ps.getFileModeB(), ps.getMimeTypeB());
         result.metaB.lines = ps.getB().size();
       }
 
@@ -182,21 +191,33 @@
     String name;
     String contentType;
     Integer lines;
+  }
 
-    void setContentType(FileMode fileMode, String mimeType) {
-      switch (fileMode) {
-        case FILE:
-          contentType = mimeType;
-          break;
-        case GITLINK:
-          contentType = "x-git/gitlink";
-          break;
-        case SYMLINK:
-          contentType = "x-git/symlink";
-          break;
-        default:
-          throw new IllegalStateException("file mode: " + fileMode);
-      }
+  private void setContentType(FileMeta meta, ProjectState project,
+      FileMode fileMode, String mimeType) {
+    switch (fileMode) {
+      case FILE:
+        if (Patch.COMMIT_MSG.equals(meta.name)) {
+          mimeType = "text/x-gerrit-commit-message";
+        } else if (project != null) {
+          for (ProjectState p : project.tree()) {
+            String t = p.getConfig().getMimeTypes().getMimeType(meta.name);
+            if (t != null) {
+              mimeType = t;
+              break;
+            }
+          }
+        }
+        meta.contentType = mimeType;
+        break;
+      case GITLINK:
+        meta.contentType = "x-git/gitlink";
+        break;
+      case SYMLINK:
+        meta.contentType = "x-git/symlink";
+        break;
+      default:
+        throw new IllegalStateException("file mode: " + fileMode);
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDraft.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDraft.java
index c8a2d43..12c50ae 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDraft.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDraft.java
@@ -15,10 +15,26 @@
 package com.google.gerrit.server.change;
 
 import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.account.AccountInfo;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
+@Singleton
 class GetDraft implements RestReadView<DraftResource> {
+
+  private final AccountInfo.Loader.Factory accountLoaderFactory;
+
+  @Inject
+  GetDraft(AccountInfo.Loader.Factory accountLoaderFactory) {
+    this.accountLoaderFactory = accountLoaderFactory;
+  }
+
   @Override
-  public CommentInfo apply(DraftResource rsrc) {
-    return new CommentInfo(rsrc.getComment(), null);
+  public CommentInfo apply(DraftResource rsrc) throws OrmException {
+    AccountInfo.Loader accountLoader = accountLoaderFactory.create(true);
+    CommentInfo ci = new CommentInfo(rsrc.getComment(), accountLoader);
+    accountLoader.fill();
+    return ci;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java
index 1a6b515..b58572c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java
@@ -34,6 +34,7 @@
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -57,6 +58,7 @@
 import java.util.Map;
 import java.util.Set;
 
+@Singleton
 public class GetRelated implements RestReadView<RevisionResource> {
   private static final Logger log = LoggerFactory.getLogger(GetRelated.class);
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReview.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReview.java
index b646dd6..9f98590 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReview.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReview.java
@@ -20,7 +20,9 @@
 import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
+@Singleton
 public class GetReview implements RestReadView<RevisionResource> {
   private final GetChange delegate;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReviewer.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReviewer.java
index fac4618..c90b3bc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReviewer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReviewer.java
@@ -18,9 +18,11 @@
 import com.google.gerrit.server.change.ReviewerJson.ReviewerInfo;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
 import java.util.List;
 
+@Singleton
 public class GetReviewer implements RestReadView<ReviewerResource> {
   private final ReviewerJson json;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetTopic.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetTopic.java
index 53f71fd..3a2f7e7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetTopic.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetTopic.java
@@ -16,7 +16,9 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.inject.Singleton;
 
+@Singleton
 class GetTopic implements RestReadView<ChangeResource> {
   @Override
   public String apply(ChangeResource rsrc) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedIn.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedIn.java
index e2c8c65..410bcab 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedIn.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedIn.java
@@ -24,6 +24,8 @@
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
@@ -35,13 +37,14 @@
 import java.io.IOException;
 import java.util.Collection;
 
+@Singleton
 class IncludedIn implements RestReadView<ChangeResource> {
 
-  private final ReviewDb db;
+  private final Provider<ReviewDb> db;
   private final GitRepositoryManager repoManager;
 
   @Inject
-  IncludedIn(ReviewDb db, GitRepositoryManager repoManager) {
+  IncludedIn(Provider<ReviewDb> db, GitRepositoryManager repoManager) {
     this.db = db;
     this.repoManager = repoManager;
   }
@@ -51,7 +54,7 @@
       ResourceConflictException, OrmException, IOException {
     ChangeControl ctl = rsrc.getControl();
     PatchSet ps =
-        db.patchSets().get(ctl.getChange().currentPatchSetId());
+        db.get().patchSets().get(ctl.getChange().currentPatchSetId());
     Repository r =
         repoManager.openRepository(ctl.getProject().getNameKey());
     try {
@@ -76,7 +79,6 @@
   }
 
   static class IncludedInInfo {
-    String kind = "gerritcodereview#includedininfo";
     Collection<String> branches;
     Collection<String> tags;
 
@@ -85,4 +87,4 @@
       tags = in.getTags();
     }
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Index.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Index.java
index f773f2f..6cf3e00 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Index.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Index.java
@@ -23,10 +23,12 @@
 import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import java.io.IOException;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@Singleton
 public class Index implements RestModifyView<ChangeResource, Input> {
   public static class Input {
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListComments.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListComments.java
index 23a71e8..f4d7b49 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListComments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListComments.java
@@ -16,15 +16,23 @@
 
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.PatchLineCommentsUtil;
 import com.google.gerrit.server.account.AccountInfo;
+import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
+@Singleton
 class ListComments extends ListDrafts {
+  private final PatchLineCommentsUtil plcUtil;
+
   @Inject
-  ListComments(Provider<ReviewDb> db, AccountInfo.Loader.Factory alf) {
+  ListComments(Provider<ReviewDb> db, AccountInfo.Loader.Factory alf,
+      PatchLineCommentsUtil plcUtil) {
     super(db, alf);
+    this.plcUtil = plcUtil;
   }
 
   @Override
@@ -34,7 +42,7 @@
 
   protected Iterable<PatchLineComment> listComments(RevisionResource rsrc)
       throws OrmException {
-    return db.get().patchComments()
-        .publishedByPatchSet(rsrc.getPatchSet().getId());
+    ChangeNotes notes = rsrc.getNotes();
+    return plcUtil.publishedByPatchSet(db.get(), notes, rsrc.getPatchSet().getId());
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListDrafts.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListDrafts.java
index cb03724..bd3aa04 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListDrafts.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListDrafts.java
@@ -26,12 +26,14 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
 
+@Singleton
 class ListDrafts implements RestReadView<RevisionResource> {
   protected final Provider<ReviewDb> db;
   private final AccountInfo.Loader.Factory accountLoaderFactory;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListReviewers.java
index 25d4e2c..ca5a55b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListReviewers.java
@@ -23,10 +23,12 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import java.util.List;
 import java.util.Map;
 
+@Singleton
 class ListReviewers implements RestReadView<ChangeResource> {
   private final Provider<ReviewDb> dbProvider;
   private final ApprovalsUtil approvalsUtil;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityChecker.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityChecker.java
index 481e535..0a6db2b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityChecker.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityChecker.java
@@ -303,6 +303,12 @@
     }
 
     @Override
+    public String toString() {
+      return "mergeability-check-change-" + change.getId().get() + "-project-"
+          + change.getDest().getParentKey();
+    }
+
+    @Override
     public Boolean call() throws Exception {
       mergeabilityCheckQueue.updatingMergeabilityFlag(change, force);
 
@@ -342,7 +348,7 @@
         m.setForce(force);
 
         ChangeControl control =
-            changeControlFactory.controlFor(change.getId(), context.getCurrentUser());
+            changeControlFactory.controlFor(change, context.getCurrentUser());
         MergeableInfo info = m.apply(
             new RevisionResource(new ChangeResource(control), ps));
         return change.isMergeable() != info.mergeable;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityChecksExecutorModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityChecksExecutorModule.java
index 96c83d3..e5bcabe 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityChecksExecutorModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityChecksExecutorModule.java
@@ -36,7 +36,7 @@
       @GerritServerConfig Config config,
       WorkQueue queues) {
     int poolSize = config.getInt("changeMerge", null, "threadPoolSize", 1);
-    return queues.createQueue(poolSize, "MergeabilityChecks");
+    return queues.createQueue(poolSize, "MergeabilityChecks-Background");
   }
 
   @Provides
@@ -52,6 +52,6 @@
     if (poolSize <= 0) {
       return backgroundExecutor;
     }
-    return queues.createQueue(poolSize, "InteractiveMergeabilityChecks");
+    return queues.createQueue(poolSize, "MergeabilityChecks-Interactive");
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java
index a4de1dc..b9daaf4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java
@@ -15,21 +15,23 @@
 package com.google.gerrit.server.change;
 
 import com.google.common.collect.Sets;
+import com.google.gerrit.extensions.common.SubmitType;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.git.BranchOrderSection;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MergeException;
 import com.google.gerrit.server.git.strategy.SubmitStrategyFactory;
 import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -46,12 +48,15 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevFlag;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.kohsuke.args4j.Option;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
@@ -59,12 +64,24 @@
   private static final Logger log = LoggerFactory.getLogger(Mergeable.class);
 
   public static class MergeableInfo {
-    public Project.SubmitType submitType;
+    public SubmitType submitType;
     public boolean mergeable;
+    public List<String> mergeableInto;
+  }
+
+  @Option(name = "--other-branches", aliases = {"-o"},
+      usage = "test mergeability for other branches too")
+  private boolean otherBranches;
+
+  @Option(name = "--force", aliases = {"-f"},
+      usage = "force recheck of mergeable field")
+  public void setForce(boolean force) {
+    this.force = force;
   }
 
   private final TestSubmitType.Get submitType;
   private final GitRepositoryManager gitManager;
+  private final ProjectCache projectCache;
   private final SubmitStrategyFactory submitStrategyFactory;
   private final Provider<ReviewDb> db;
   private final ChangeIndexer indexer;
@@ -74,20 +91,18 @@
   @Inject
   Mergeable(TestSubmitType.Get submitType,
       GitRepositoryManager gitManager,
+      ProjectCache projectCache,
       SubmitStrategyFactory submitStrategyFactory,
       Provider<ReviewDb> db,
       ChangeIndexer indexer) {
     this.submitType = submitType;
     this.gitManager = gitManager;
+    this.projectCache = projectCache;
     this.submitStrategyFactory = submitStrategyFactory;
     this.db = db;
     this.indexer = indexer;
   }
 
-  public void setForce(boolean force) {
-    this.force = force;
-  }
-
   @Override
   public MergeableInfo apply(RevisionResource resource) throws AuthException,
       ResourceConflictException, BadRequestException, OrmException, IOException {
@@ -113,6 +128,24 @@
         result.mergeable =
             refresh(change, ps, result.submitType, git, refs, ref);
       }
+
+      if (otherBranches) {
+        result.mergeableInto = new ArrayList<>();
+        BranchOrderSection branchOrder =
+            projectCache.get(change.getProject()).getBranchOrderSection();
+        if (branchOrder != null) {
+          int prefixLen = Constants.R_HEADS.length();
+          for (String n : branchOrder.getMoreStable(ref.getName())) {
+            Ref other = refs.get(n);
+            if (other == null) {
+              continue;
+            }
+            if (isMergeable(change, ps, SubmitType.CHERRY_PICK, git, refs, other)) {
+              result.mergeableInto.add(other.getName().substring(prefixLen));
+            }
+          }
+        }
+      }
     } finally {
       git.close();
     }
@@ -132,7 +165,37 @@
 
   private boolean refresh(Change change,
       final PatchSet ps,
-      Project.SubmitType type,
+      SubmitType type,
+      Repository git,
+      Map<String, Ref> refs,
+      final Ref ref) throws IOException, OrmException {
+
+    final boolean mergeable = isMergeable(change, ps, type, git, refs, ref);
+
+    Change c = db.get().changes().atomicUpdate(
+        change.getId(),
+        new AtomicUpdate<Change>() {
+          @Override
+          public Change update(Change c) {
+            if (c.getStatus().isOpen()
+                && ps.getId().equals(c.currentPatchSetId())) {
+              c.setMergeable(mergeable);
+              c.setLastSha1MergeTested(toRevId(ref));
+              return c;
+            } else {
+              return null;
+            }
+          }
+        });
+    if (c != null) {
+      indexer.index(db.get(), c);
+    }
+    return mergeable;
+  }
+
+  private boolean isMergeable(Change change,
+      final PatchSet ps,
+      SubmitType type,
       Repository git,
       Map<String, Ref> refs,
       final Ref ref) throws IOException, OrmException {
@@ -176,25 +239,6 @@
             accepted,
             change.getDest()).dryRun(tip, rev);
       }
-
-      Change c = db.get().changes().atomicUpdate(
-        change.getId(),
-        new AtomicUpdate<Change>() {
-          @Override
-          public Change update(Change c) {
-            if (c.getStatus().isOpen()
-                && ps.getId().equals(c.currentPatchSetId())) {
-              c.setMergeable(mergeable);
-              c.setLastSha1MergeTested(toRevId(ref));
-              return c;
-            } else {
-              return null;
-            }
-          }
-        });
-      if (c != null) {
-        indexer.index(db.get(), c);
-      }
       return mergeable;
     } catch (MergeException | IOException | NoSuchProjectException e) {
       log.error(String.format(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
index 85da2c5..635421a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalCopier;
 import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.events.CommitReceivedEvent;
@@ -40,6 +41,7 @@
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.ssh.NoSshInfo;
 import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gerrit.server.util.TimeUtil;
@@ -82,12 +84,14 @@
   private final PatchSetInfoFactory patchSetInfoFactory;
   private final ReviewDb db;
   private final ChangeUpdate.Factory updateFactory;
+  private final ChangeControl.GenericFactory ctlFactory;
   private final GitReferenceUpdated gitRefUpdated;
   private final CommitValidators.Factory commitValidatorsFactory;
   private final MergeabilityChecker mergeabilityChecker;
   private final ReplacePatchSetSender.Factory replacePatchSetFactory;
   private final ApprovalsUtil approvalsUtil;
   private final ApprovalCopier approvalCopier;
+  private final ChangeMessagesUtil cmUtil;
 
   private final Repository git;
   private final RevWalk revWalk;
@@ -109,8 +113,10 @@
   public PatchSetInserter(ChangeHooks hooks,
       ReviewDb db,
       ChangeUpdate.Factory updateFactory,
+      ChangeControl.GenericFactory ctlFactory,
       ApprovalsUtil approvalsUtil,
       ApprovalCopier approvalCopier,
+      ChangeMessagesUtil cmUtil,
       PatchSetInfoFactory patchSetInfoFactory,
       GitReferenceUpdated gitRefUpdated,
       CommitValidators.Factory commitValidatorsFactory,
@@ -126,8 +132,10 @@
     this.hooks = hooks;
     this.db = db;
     this.updateFactory = updateFactory;
+    this.ctlFactory = ctlFactory;
     this.approvalsUtil = approvalsUtil;
     this.approvalCopier = approvalCopier;
+    this.cmUtil = cmUtil;
     this.patchSetInfoFactory = patchSetInfoFactory;
     this.gitRefUpdated = gitRefUpdated;
     this.commitValidatorsFactory = commitValidatorsFactory;
@@ -211,7 +219,7 @@
   }
 
   public Change insert() throws InvalidChangeOperationException, OrmException,
-      IOException {
+      IOException, NoSuchChangeException {
     init();
     validate();
 
@@ -272,17 +280,19 @@
       }
 
       if (messageIsForChange()) {
-        insertMessage(db);
+        cmUtil.addChangeMessage(db, update, changeMessage);
       }
 
       if (copyLabels) {
         approvalCopier.copy(db, ctl, patchSet);
       }
       db.commit();
-      update.commit();
+      if (messageIsForChange()) {
+        update.commit();
+      }
 
       if (!messageIsForChange()) {
-        insertMessage(db);
+        commitMessageNotForChange(updatedChange);
       }
 
       if (sendMail) {
@@ -315,6 +325,20 @@
     return updatedChange;
   }
 
+  private void commitMessageNotForChange(Change updatedChange)
+      throws OrmException, NoSuchChangeException, IOException {
+    if (changeMessage != null) {
+      Change otherChange =
+          db.changes().get(changeMessage.getPatchSetId().getParentKey());
+      ChangeControl otherControl =
+          ctlFactory.controlFor(otherChange, user);
+      ChangeUpdate updateForOtherChange =
+          updateFactory.create(otherControl, updatedChange.getLastUpdatedOn());
+      cmUtil.addChangeMessage(db, updateForOtherChange, changeMessage);
+      updateForOtherChange.commit();
+    }
+  }
+
   private void init() throws IOException {
     if (sshInfo == null) {
       sshInfo = new NoSshInfo();
@@ -366,12 +390,6 @@
         .equals(patchSet.getId().getParentKey());
   }
 
-  private void insertMessage(ReviewDb db) throws OrmException {
-    if (changeMessage != null) {
-      db.changeMessages().insert(Collections.singleton(changeMessage));
-    }
-  }
-
   public class ChangeModifiedException extends InvalidChangeOperationException {
     private static final long serialVersionUID = 1L;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
index 473e0bc..38070c0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
@@ -20,6 +20,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
 import com.google.common.util.concurrent.CheckedFuture;
 import com.google.common.util.concurrent.Futures;
 import com.google.gerrit.common.ChangeHooks;
@@ -28,10 +29,10 @@
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
-import com.google.gerrit.extensions.api.changes.ReviewInput.Comment;
+import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
 import com.google.gerrit.extensions.api.changes.ReviewInput.NotifyHandling;
-import com.google.gerrit.extensions.api.changes.ReviewInput.Side;
+import com.google.gerrit.extensions.common.Comment.Side;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
@@ -43,20 +44,28 @@
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.PatchLineCommentsUtil;
 import com.google.gerrit.server.account.AccountsCollection;
 import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.patch.PatchList;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.util.LabelVote;
 import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
+import org.eclipse.jgit.lib.ObjectId;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -66,6 +75,7 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 public class PostReview implements RestModifyView<RevisionResource, ReviewInput> {
   private static final Logger log = LoggerFactory.getLogger(PostReview.class);
@@ -76,8 +86,12 @@
 
   private final Provider<ReviewDb> db;
   private final ChangesCollection changes;
+  private final ChangeData.Factory changeDataFactory;
   private final ChangeUpdate.Factory updateFactory;
   private final ApprovalsUtil approvalsUtil;
+  private final ChangeMessagesUtil cmUtil;
+  private final PatchLineCommentsUtil plcUtil;
+  private final PatchListCache patchListCache;
   private final ChangeIndexer indexer;
   private final AccountsCollection accounts;
   private final EmailReviewComments.Factory email;
@@ -93,16 +107,24 @@
   @Inject
   PostReview(Provider<ReviewDb> db,
       ChangesCollection changes,
+      ChangeData.Factory changeDataFactory,
       ChangeUpdate.Factory updateFactory,
       ApprovalsUtil approvalsUtil,
+      ChangeMessagesUtil cmUtil,
+      PatchLineCommentsUtil plcUtil,
+      PatchListCache patchListCache,
       ChangeIndexer indexer,
       AccountsCollection accounts,
       EmailReviewComments.Factory email,
       ChangeHooks hooks) {
     this.db = db;
     this.changes = changes;
+    this.changeDataFactory = changeDataFactory;
     this.updateFactory = updateFactory;
+    this.plcUtil = plcUtil;
+    this.patchListCache = patchListCache;
     this.approvalsUtil = approvalsUtil;
+    this.cmUtil = cmUtil;
     this.indexer = indexer;
     this.accounts = accounts;
     this.email = email;
@@ -120,7 +142,7 @@
       checkLabels(revision, input.strictLabels, input.labels);
     }
     if (input.comments != null) {
-      checkComments(input.comments);
+      checkComments(revision, input.comments);
     }
     if (input.notify == null) {
       log.warn("notify = null; assuming notify = NONE");
@@ -136,9 +158,10 @@
       timestamp = change.getLastUpdatedOn();
 
       update = updateFactory.create(revision.getControl(), timestamp);
-      dirty |= insertComments(revision, input.comments, input.drafts);
+      update.setPatchSetId(revision.getPatchSet().getId());
+      dirty |= insertComments(revision, update, input.comments, input.drafts);
       dirty |= updateLabels(revision, update, input.labels);
-      dirty |= insertMessage(revision, input.message);
+      dirty |= insertMessage(revision, input.message, update);
       if (dirty) {
         db.get().changes().update(Collections.singleton(change));
         db.get().commit();
@@ -266,22 +289,32 @@
     }
   }
 
-  private void checkComments(Map<String, List<Comment>> in)
-      throws BadRequestException {
-    Iterator<Map.Entry<String, List<Comment>>> mapItr =
+  private void checkComments(RevisionResource revision, Map<String, List<CommentInput>> in)
+      throws BadRequestException, OrmException {
+    Iterator<Map.Entry<String, List<CommentInput>>> mapItr =
         in.entrySet().iterator();
+    Set<String> filePaths =
+        Sets.newHashSet(changeDataFactory.create(
+            db.get(), revision.getChange()).filePaths(
+                revision.getPatchSet()));
     while (mapItr.hasNext()) {
-      Map.Entry<String, List<Comment>> ent = mapItr.next();
+      Map.Entry<String, List<CommentInput>> ent = mapItr.next();
       String path = ent.getKey();
-      List<Comment> list = ent.getValue();
+      if (!filePaths.contains(path) && !Patch.COMMIT_MSG.equals(path)) {
+        throw new BadRequestException(String.format(
+            "file %s not found in revision %s",
+            path, revision.getChange().currentPatchSetId()));
+      }
+
+      List<CommentInput> list = ent.getValue();
       if (list == null) {
         mapItr.remove();
         continue;
       }
 
-      Iterator<Comment> listItr = list.iterator();
+      Iterator<CommentInput> listItr = list.iterator();
       while (listItr.hasNext()) {
-        Comment c = listItr.next();
+        CommentInput c = listItr.next();
         if (c == null) {
           listItr.remove();
           continue;
@@ -303,8 +336,8 @@
   }
 
   private boolean insertComments(RevisionResource rsrc,
-      Map<String, List<Comment>> in, DraftHandling draftsHandling)
-      throws OrmException {
+      ChangeUpdate update, Map<String, List<CommentInput>> in, DraftHandling draftsHandling)
+      throws OrmException, IOException {
     if (in == null) {
       in = Collections.emptyMap();
     }
@@ -317,9 +350,18 @@
     List<PatchLineComment> del = Lists.newArrayList();
     List<PatchLineComment> ups = Lists.newArrayList();
 
-    for (Map.Entry<String, List<Comment>> ent : in.entrySet()) {
+    PatchList patchList = null;
+    try {
+      patchList = patchListCache.get(rsrc.getChange(), rsrc.getPatchSet());
+    } catch (PatchListNotAvailableException e) {
+      throw new OrmException("could not load PatchList for this patchset", e);
+    }
+    RevId patchSetCommit = new RevId(ObjectId.toString(patchList.getNewId()));
+    RevId baseCommit = new RevId(ObjectId.toString(patchList.getOldId()));;
+
+    for (Map.Entry<String, List<CommentInput>> ent : in.entrySet()) {
       String path = ent.getKey();
-      for (Comment c : ent.getValue()) {
+      for (CommentInput c : ent.getValue()) {
         String parent = Url.decode(c.inReplyTo);
         PatchLineComment e = drafts.remove(Url.decode(c.id));
         if (e == null) {
@@ -336,6 +378,7 @@
         e.setStatus(PatchLineComment.Status.PUBLISHED);
         e.setWrittenOn(timestamp);
         e.setSide(c.side == Side.PARENT ? (short) 0 : (short) 1);
+        e.setRevId(c.side == Side.PARENT ? baseCommit : patchSetCommit);
         e.setMessage(c.message);
         if (c.range != null) {
           e.setRange(new CommentRange(
@@ -360,12 +403,13 @@
         for (PatchLineComment e : drafts.values()) {
           e.setStatus(PatchLineComment.Status.PUBLISHED);
           e.setWrittenOn(timestamp);
+          e.setRevId(e.getSide() == (short) 0 ? baseCommit : patchSetCommit);
           ups.add(e);
         }
         break;
     }
     db.get().patchComments().delete(del);
-    db.get().patchComments().upsert(ups);
+    plcUtil.addPublishedComments(db.get(), update, ups);
     comments.addAll(ups);
     return !del.isEmpty() || !ups.isEmpty();
   }
@@ -474,7 +518,7 @@
     Map<String, PatchSetApproval> current = Maps.newHashMap();
 
     for (PatchSetApproval a : approvalsUtil.byPatchSetUser(
-        db.get(), rsrc.getNotes(), rsrc.getPatchSet().getId(),
+        db.get(), rsrc.getControl(), rsrc.getPatchSet().getId(),
         rsrc.getAccountId())) {
       if (a.isSubmit()) {
         continue;
@@ -494,8 +538,8 @@
     labelDelta.add(new LabelVote(name, value).format());
   }
 
-  private boolean insertMessage(RevisionResource rsrc, String msg)
-      throws OrmException {
+  private boolean insertMessage(RevisionResource rsrc, String msg,
+      ChangeUpdate update) throws OrmException {
     msg = Strings.nullToEmpty(msg).trim();
 
     StringBuilder buf = new StringBuilder();
@@ -523,7 +567,7 @@
         "Patch Set %d:%s",
         rsrc.getPatchSet().getPatchSetId(),
         buf.toString()));
-    db.get().changeMessages().insert(Collections.singleton(message));
+    cmUtil.addChangeMessage(db.get(), update, message);
     return true;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
index 83a1492..5b95ab2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
@@ -36,6 +36,7 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountInfo;
@@ -54,6 +55,7 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import org.eclipse.jgit.lib.Config;
 import org.slf4j.Logger;
@@ -65,6 +67,7 @@
 import java.util.Map;
 import java.util.Set;
 
+@Singleton
 public class PostReviewers implements RestModifyView<ChangeResource, AddReviewerInput> {
   private static final Logger log = LoggerFactory
       .getLogger(PostReviewers.class);
@@ -76,12 +79,12 @@
   private final ReviewerResource.Factory reviewerFactory;
   private final ApprovalsUtil approvalsUtil;
   private final AddReviewerSender.Factory addReviewerSenderFactory;
-  private final Provider<GroupsCollection> groupsCollection;
+  private final GroupsCollection groupsCollection;
   private final GroupMembers.Factory groupMembersFactory;
   private final AccountInfo.Loader.Factory accountLoaderFactory;
   private final Provider<ReviewDb> dbProvider;
   private final ChangeUpdate.Factory updateFactory;
-  private final IdentifiedUser currentUser;
+  private final Provider<CurrentUser> currentUser;
   private final IdentifiedUser.GenericFactory identifiedUserFactory;
   private final Config cfg;
   private final ChangeHooks hooks;
@@ -94,12 +97,12 @@
       ReviewerResource.Factory reviewerFactory,
       ApprovalsUtil approvalsUtil,
       AddReviewerSender.Factory addReviewerSenderFactory,
-      Provider<GroupsCollection> groupsCollection,
+      GroupsCollection groupsCollection,
       GroupMembers.Factory groupMembersFactory,
       AccountInfo.Loader.Factory accountLoaderFactory,
       Provider<ReviewDb> db,
       ChangeUpdate.Factory updateFactory,
-      IdentifiedUser currentUser,
+      Provider<CurrentUser> currentUser,
       IdentifiedUser.GenericFactory identifiedUserFactory,
       @GerritServerConfig Config cfg,
       ChangeHooks hooks,
@@ -148,18 +151,19 @@
 
   private PostResult putAccount(ReviewerResource rsrc) throws OrmException,
       EmailException, IOException {
-    Account.Id id = rsrc.getUser().getAccountId();
-    ChangeControl control = rsrc.getControl().forUser(
-        identifiedUserFactory.create(id));
+    Account member = rsrc.getUser().getAccount();
+    ChangeControl control = rsrc.getControl();
     PostResult result = new PostResult();
-    addReviewers(rsrc, result, ImmutableMap.of(id, control));
+    if (isValidReviewer(member, control)) {
+      addReviewers(rsrc, result, ImmutableMap.of(member.getId(), control));
+    }
     return result;
   }
 
   private PostResult putGroup(ChangeResource rsrc, AddReviewerInput input)
       throws BadRequestException,
       UnprocessableEntityException, OrmException, EmailException, IOException {
-    GroupDescription.Basic group = groupsCollection.get().parseInternal(input.reviewer);
+    GroupDescription.Basic group = groupsCollection.parseInternal(input.reviewer);
     PostResult result = new PostResult();
     if (!isLegalReviewerGroup(group.getGroupUUID())) {
       result.error = MessageFormat.format(
@@ -203,13 +207,8 @@
     }
 
     for (Account member : members) {
-      if (member.isActive()) {
-        IdentifiedUser user = identifiedUserFactory.create(member.getId());
-        // Does not account for draft status as a user might want to let a
-        // reviewer see a draft.
-        if (control.forUser(user).isRefVisible()) {
-          reviewers.put(user.getAccountId(), control);
-        }
+      if (isValidReviewer(member, control)) {
+        reviewers.put(member.getId(), control);
       }
     }
 
@@ -217,6 +216,16 @@
     return result;
   }
 
+  private boolean isValidReviewer(Account member, ChangeControl control) {
+    if (member.isActive()) {
+      IdentifiedUser user = identifiedUserFactory.create(member.getId());
+      // Does not account for draft status as a user might want to let a
+      // reviewer see a draft.
+      return control.forUser(user).isRefVisible();
+    }
+    return false;
+  }
+
   private void addReviewers(ChangeResource rsrc, PostResult result,
       Map<Account.Id, ChangeControl> reviewers)
       throws OrmException, EmailException, IOException {
@@ -239,6 +248,7 @@
         indexer.indexAsync(rsrc.getChange().getId());
     result.reviewers = Lists.newArrayListWithCapacity(added.size());
     for (PatchSetApproval psa : added) {
+      // New reviewers have value 0, don't bother normalizing.
       result.reviewers.add(json.format(
           new ReviewerInfo(psa.getAccountId()),
           reviewers.get(psa.getAccountId()),
@@ -266,15 +276,16 @@
     //
     // The user knows they added themselves, don't bother emailing them.
     List<Account.Id> toMail = Lists.newArrayListWithCapacity(added.size());
+    IdentifiedUser identifiedUser = (IdentifiedUser) currentUser.get();
     for (PatchSetApproval psa : added) {
-      if (!psa.getAccountId().equals(currentUser.getAccountId())) {
+      if (!psa.getAccountId().equals(identifiedUser.getAccountId())) {
         toMail.add(psa.getAccountId());
       }
     }
     if (!toMail.isEmpty()) {
       try {
         AddReviewerSender cm = addReviewerSenderFactory.create(change);
-        cm.setFrom(currentUser.getAccountId());
+        cm.setFrom(identifiedUser.getAccountId());
         cm.addReviewers(toMail);
         cm.send();
       } catch (Exception err) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Publish.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Publish.java
index eb493e5..88da094 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Publish.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Publish.java
@@ -36,11 +36,13 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import org.eclipse.jgit.lib.Config;
 
 import java.io.IOException;
 
+@Singleton
 public class Publish implements RestModifyView<RevisionResource, Input>,
     UiAction<RevisionResource> {
   public static class Input {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraft.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraft.java
index 881b876..c1fb304 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraft.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraft.java
@@ -29,13 +29,14 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import java.sql.Timestamp;
 import java.util.Collections;
 
+@Singleton
 class PutDraft implements RestModifyView<DraftResource, Input> {
   static class Input {
-    String kind;
     String id;
     String path;
     Side side;
@@ -49,10 +50,10 @@
   }
 
   private final Provider<ReviewDb> db;
-  private final Provider<DeleteDraft> delete;
+  private final DeleteDraft delete;
 
   @Inject
-  PutDraft(Provider<ReviewDb> db, Provider<DeleteDraft> delete) {
+  PutDraft(Provider<ReviewDb> db, DeleteDraft delete) {
     this.db = db;
     this.delete = delete;
   }
@@ -62,9 +63,7 @@
       BadRequestException, OrmException {
     PatchLineComment c = rsrc.getComment();
     if (in == null || in.message == null || in.message.trim().isEmpty()) {
-      return delete.get().apply(rsrc, null);
-    } else if (in.kind != null && !"gerritcodereview#comment".equals(in.kind)) {
-      throw new BadRequestException("expected kind gerritcodereview#comment");
+      return delete.apply(rsrc, null);
     } else if (in.id != null && !rsrc.getId().equals(in.id)) {
       throw new BadRequestException("id must match URL");
     } else if (in.line != null && in.line < 0) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java
index 5c07682..7f7c5fb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java
@@ -24,25 +24,30 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.change.PutTopic.Input;
 import com.google.gerrit.server.index.ChangeIndexer;
+import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import java.io.IOException;
-import java.util.Collections;
 
+@Singleton
 class PutTopic implements RestModifyView<ChangeResource, Input>,
     UiAction<ChangeResource> {
   private final Provider<ReviewDb> dbProvider;
   private final ChangeIndexer indexer;
   private final ChangeHooks hooks;
+  private final ChangeUpdate.Factory updateFactory;
+  private final ChangeMessagesUtil cmUtil;
 
   static class Input {
     @DefaultInput
@@ -51,10 +56,13 @@
 
   @Inject
   PutTopic(Provider<ReviewDb> dbProvider, ChangeIndexer indexer,
-      ChangeHooks hooks) {
+      ChangeHooks hooks, ChangeUpdate.Factory updateFactory,
+      ChangeMessagesUtil cmUtil) {
     this.dbProvider = dbProvider;
     this.indexer = indexer;
     this.hooks = hooks;
+    this.updateFactory = updateFactory;
+    this.cmUtil = cmUtil;
   }
 
   @Override
@@ -91,6 +99,7 @@
           currentUser.getAccountId(), TimeUtil.nowTs(),
           change.currentPatchSetId());
       cmsg.setMessage(summary);
+      ChangeUpdate update;
 
       db.changes().beginTransaction(change.getId());
       try {
@@ -103,11 +112,16 @@
               return change;
             }
           });
-        db.changeMessages().insert(Collections.singleton(cmsg));
+
+        //TODO(yyonas): atomic update was not propagated
+        update = updateFactory.create(control);
+        cmUtil.addChangeMessage(db, update, cmsg);
+
         db.commit();
       } finally {
         db.rollback();
       }
+      update.commit();
       indexer.index(db, change);
       hooks.doTopicChangedHook(change, currentUser.getAccount(),
           oldTopicName, db);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
index e1a019f..295189b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
@@ -33,9 +33,11 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import java.io.IOException;
 
+@Singleton
 public class Rebase implements RestModifyView<RevisionResource, Input>,
     UiAction<RevisionResource> {
   public static class Input {
@@ -47,7 +49,9 @@
   @Inject
   public Rebase(Provider<RebaseChange> rebaseChange, ChangeJson json) {
     this.rebaseChange = rebaseChange;
-    this.json = json;
+    this.json = json
+        .addOption(ListChangesOption.CURRENT_REVISION)
+        .addOption(ListChangesOption.CURRENT_COMMIT);
   }
 
   @Override
@@ -64,7 +68,8 @@
     }
 
     try {
-      rebaseChange.get().rebase(rsrc.getPatchSet().getId(), rsrc.getUser());
+      rebaseChange.get().rebase(rsrc.getChange(), rsrc.getPatchSet().getId(),
+          rsrc.getUser());
     } catch (InvalidChangeOperationException e) {
       throw new ResourceConflictException(e.getMessage());
     } catch (IOException e) {
@@ -73,8 +78,6 @@
       throw new ResourceNotFoundException(change.getId().toString());
     }
 
-    json.addOption(ListChangesOption.CURRENT_REVISION)
-        .addOption(ListChangesOption.CURRENT_COMMIT);
     return json.format(change.getId());
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
index 1c1c614..88e2137 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
@@ -26,23 +26,26 @@
 import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
 import com.google.gerrit.server.mail.ReplyToChangeSender;
 import com.google.gerrit.server.mail.RestoredSender;
+import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
-import java.util.Collections;
 
+@Singleton
 public class Restore implements RestModifyView<ChangeResource, RestoreInput>,
     UiAction<ChangeResource> {
   private static final Logger log = LoggerFactory.getLogger(Restore.class);
@@ -52,18 +55,24 @@
   private final Provider<ReviewDb> dbProvider;
   private final ChangeJson json;
   private final MergeabilityChecker mergeabilityChecker;
+  private final ChangeMessagesUtil cmUtil;
+  private final ChangeUpdate.Factory updateFactory;
 
   @Inject
   Restore(ChangeHooks hooks,
       RestoredSender.Factory restoredSenderFactory,
       Provider<ReviewDb> dbProvider,
       ChangeJson json,
-      MergeabilityChecker mergeabilityChecker) {
+      MergeabilityChecker mergeabilityChecker,
+      ChangeMessagesUtil cmUtil,
+      ChangeUpdate.Factory updateFactory) {
     this.hooks = hooks;
     this.restoredSenderFactory = restoredSenderFactory;
     this.dbProvider = dbProvider;
     this.json = json;
     this.mergeabilityChecker = mergeabilityChecker;
+    this.cmUtil = cmUtil;
+    this.updateFactory = updateFactory;
   }
 
   @Override
@@ -80,6 +89,7 @@
     }
 
     ChangeMessage message;
+    ChangeUpdate update;
     ReviewDb db = dbProvider.get();
     db.changes().beginTransaction(change.getId());
     try {
@@ -100,12 +110,16 @@
         throw new ResourceConflictException("change is "
             + status(db.changes().get(req.getChange().getId())));
       }
+
+      //TODO(yyonas): atomic update was not propagated
+      update = updateFactory.create(control);
       message = newMessage(input, caller, change);
-      db.changeMessages().insert(Collections.singleton(message));
+      cmUtil.addChangeMessage(db, update, message);
       db.commit();
     } finally {
       db.rollback();
     }
+    update.commit();
 
     CheckedFuture<?, IOException> f = mergeabilityChecker.newCheck()
         .addChange(change)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
index 022c12c..326c872 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
@@ -32,13 +32,16 @@
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.ssh.NoSshInfo;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
 import org.eclipse.jgit.lib.PersonIdent;
 
 import java.io.IOException;
 
+@Singleton
 public class Revert implements RestModifyView<ChangeResource, RevertInput>,
     UiAction<ChangeResource> {
   private final ChangeJson json;
@@ -70,7 +73,7 @@
       Change.Id revertedChangeId =
           changeUtil.revert(control, change.currentPatchSetId(),
               Strings.emptyToNull(input.message),
-              myIdent, new NoSshInfo());
+              new PersonIdent(myIdent, TimeUtil.nowTs()), new NoSshInfo());
 
       return json.format(revertedChangeId);
     } catch (InvalidChangeOperationException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewed.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewed.java
index dd27139..120a414 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewed.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewed.java
@@ -22,14 +22,16 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import java.util.Collections;
 
-class Reviewed {
-  static class Input {
+public class Reviewed {
+  public static class Input {
   }
 
-  static class PutReviewed implements RestModifyView<FileResource, Input> {
+  @Singleton
+  public static class PutReviewed implements RestModifyView<FileResource, Input> {
     private final Provider<ReviewDb> dbProvider;
 
     @Inject
@@ -57,7 +59,8 @@
     }
   }
 
-  static class DeleteReviewed implements RestModifyView<FileResource, Input> {
+  @Singleton
+  public static class DeleteReviewed implements RestModifyView<FileResource, Input> {
     private final Provider<ReviewDb> dbProvider;
 
     @Inject
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
index 5008d02..4612c96 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
@@ -29,36 +29,34 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.account.AccountInfo;
-import com.google.gerrit.server.git.LabelNormalizer;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 import java.util.TreeMap;
 
+@Singleton
 public class ReviewerJson {
   private final Provider<ReviewDb> db;
   private final ChangeData.Factory changeDataFactory;
   private final ApprovalsUtil approvalsUtil;
-  private final LabelNormalizer labelNormalizer;
   private final AccountInfo.Loader.Factory accountLoaderFactory;
 
   @Inject
   ReviewerJson(Provider<ReviewDb> db,
       ChangeData.Factory changeDataFactory,
       ApprovalsUtil approvalsUtil,
-      LabelNormalizer labelNormalizer,
       AccountInfo.Loader.Factory accountLoaderFactory) {
     this.db = db;
     this.changeDataFactory = changeDataFactory;
     this.approvalsUtil = approvalsUtil;
-    this.labelNormalizer = labelNormalizer;
     this.accountLoaderFactory = accountLoaderFactory;
   }
 
@@ -86,17 +84,16 @@
       ChangeNotes changeNotes) throws OrmException {
     PatchSet.Id psId = ctl.getChange().currentPatchSetId();
     return format(out, ctl,
-        approvalsUtil.byPatchSetUser(db.get(), changeNotes, psId, out._id));
+        approvalsUtil.byPatchSetUser(db.get(), ctl, psId, out._id));
   }
 
   public ReviewerInfo format(ReviewerInfo out, ChangeControl ctl,
-      List<PatchSetApproval> approvals) throws OrmException {
+      Iterable<PatchSetApproval> approvals) throws OrmException {
     LabelTypes labelTypes = ctl.getLabelTypes();
 
     // Don't use Maps.newTreeMap(Comparator) due to OpenJDK bug 100167.
-    out.approvals = new TreeMap<String,String>(labelTypes.nameComparator());
-    for (PatchSetApproval ca :
-        labelNormalizer.normalize(ctl, approvals).getNormalized()) {
+    out.approvals = new TreeMap<>(labelTypes.nameComparator());
+    for (PatchSetApproval ca : approvals) {
       for (PermissionRange pr : ctl.getLabelRanges()) {
         if (!pr.isEmpty()) {
           LabelType at = labelTypes.byLabel(ca.getLabelId());
@@ -135,7 +132,6 @@
   }
 
   public static class ReviewerInfo extends AccountInfo {
-    final String kind = "gerritcodereview#reviewer";
     Map<String, String> approvals;
 
     protected ReviewerInfo(Account.Id id) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewers.java
index e7c913a..74a8866 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewers.java
@@ -28,9 +28,11 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import java.util.Collection;
 
+@Singleton
 public class Reviewers implements
     ChildCollection<ChangeResource, ReviewerResource> {
   private final DynamicMap<RestView<ReviewerResource>> views;
@@ -38,7 +40,7 @@
   private final ApprovalsUtil approvalsUtil;
   private final AccountsCollection accounts;
   private final ReviewerResource.Factory resourceFactory;
-  private final Provider<ListReviewers> list;
+  private final ListReviewers list;
 
   @Inject
   Reviewers(Provider<ReviewDb> dbProvider,
@@ -46,7 +48,7 @@
       AccountsCollection accounts,
       ReviewerResource.Factory resourceFactory,
       DynamicMap<RestView<ReviewerResource>> views,
-      Provider<ListReviewers> list) {
+      ListReviewers list) {
     this.dbProvider = dbProvider;
     this.approvalsUtil = approvalsUtil;
     this.accounts = accounts;
@@ -62,7 +64,7 @@
 
   @Override
   public RestView<ChangeResource> list() {
-    return list.get();
+    return list;
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java
index 19c6d3a..239aae2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java
@@ -27,10 +27,12 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import java.util.Collections;
 import java.util.List;
 
+@Singleton
 public class Revisions implements ChildCollection<ChangeResource, RevisionResource> {
   private final DynamicMap<RestView<RevisionResource>> views;
   private final Provider<ReviewDb> dbProvider;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
index cd1793c..3719028 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
@@ -16,15 +16,18 @@
 
 import static com.google.gerrit.common.data.SubmitRecord.Status.OK;
 
+import com.google.common.base.Objects;
 import com.google.common.base.Optional;
 import com.google.common.base.Predicate;
 import com.google.common.base.Strings;
+import com.google.common.collect.FluentIterable;
 import com.google.common.collect.HashBasedTable;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Table;
+import com.google.gerrit.common.data.ParameterizedString;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -38,14 +41,17 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.PatchSetApproval.LabelId;
+import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.ProjectUtil;
 import com.google.gerrit.server.account.AccountsCollection;
 import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.LabelNormalizer;
 import com.google.gerrit.server.git.MergeQueue;
@@ -58,9 +64,12 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
 
 import java.io.IOException;
@@ -69,8 +78,12 @@
 import java.util.List;
 import java.util.Map;
 
+@Singleton
 public class Submit implements RestModifyView<RevisionResource, SubmitInput>,
     UiAction<RevisionResource> {
+  private static final String DEFAULT_TOOLTIP =
+      "Submit patch set ${patchSet} into ${branch}";
+
   public enum Status {
     SUBMITTED, MERGED
   }
@@ -91,11 +104,14 @@
   private final IdentifiedUser.GenericFactory userFactory;
   private final ChangeUpdate.Factory updateFactory;
   private final ApprovalsUtil approvalsUtil;
+  private final ChangeMessagesUtil cmUtil;
   private final MergeQueue mergeQueue;
   private final ChangeIndexer indexer;
   private final LabelNormalizer labelNormalizer;
   private final AccountsCollection accounts;
   private final ChangesCollection changes;
+  private final String label;
+  private final ParameterizedString titlePattern;
 
   @Inject
   Submit(@GerritPersonIdent PersonIdent serverIdent,
@@ -104,22 +120,31 @@
       IdentifiedUser.GenericFactory userFactory,
       ChangeUpdate.Factory updateFactory,
       ApprovalsUtil approvalsUtil,
+      ChangeMessagesUtil cmUtil,
       MergeQueue mergeQueue,
       AccountsCollection accounts,
       ChangesCollection changes,
       ChangeIndexer indexer,
-      LabelNormalizer labelNormalizer) {
+      LabelNormalizer labelNormalizer,
+      @GerritServerConfig Config cfg) {
     this.serverIdent = serverIdent;
     this.dbProvider = dbProvider;
     this.repoManager = repoManager;
     this.userFactory = userFactory;
     this.updateFactory = updateFactory;
     this.approvalsUtil = approvalsUtil;
+    this.cmUtil = cmUtil;
     this.mergeQueue = mergeQueue;
     this.accounts = accounts;
     this.changes = changes;
     this.indexer = indexer;
     this.labelNormalizer = labelNormalizer;
+    this.label = Objects.firstNonNull(
+        Strings.emptyToNull(cfg.getString("change", null, "submitLabel")),
+        "Submit");
+    this.titlePattern = new ParameterizedString(Objects.firstNonNull(
+        cfg.getString("change", null, "submitTooltip"),
+        DEFAULT_TOOLTIP));
   }
 
   @Override
@@ -183,11 +208,15 @@
   @Override
   public UiAction.Description getDescription(RevisionResource resource) {
     PatchSet.Id current = resource.getChange().currentPatchSetId();
+    RevId revId = resource.getPatchSet().getRevision();
+    Map<String, String> params = ImmutableMap.of(
+        "patchSet", String.valueOf(resource.getPatchSet().getPatchSetId()),
+        "branch", resource.getChange().getDest().getShortName(),
+        "commit", ObjectId.fromString(revId.get()).abbreviate(7).name());
+
     return new UiAction.Description()
-      .setTitle(String.format(
-          "Submit patch set %d into %s",
-          resource.getPatchSet().getPatchSetId(),
-          resource.getChange().getDest().getShortName()))
+      .setLabel(label)
+      .setTitle(Strings.emptyToNull(titlePattern.replace(params)))
       .setVisible(!resource.getPatchSet().isDraft()
           && resource.getChange().getStatus().isOpen()
           && resource.getPatchSet().getId().equals(current)
@@ -201,16 +230,16 @@
    */
   public ChangeMessage getConflictMessage(RevisionResource rsrc)
       throws OrmException {
-    return Iterables.getFirst(Iterables.filter(
-      Lists.reverse(dbProvider.get().changeMessages()
-          .byChange(rsrc.getChange().getId())
-          .toList()),
-      new Predicate<ChangeMessage>() {
-        @Override
-        public boolean apply(ChangeMessage input) {
-          return input.getAuthor() == null;
-        }
-      }), null);
+    return FluentIterable.from(cmUtil.byPatchSet(dbProvider.get(), rsrc.getNotes(),
+        rsrc.getPatchSet().getId()))
+        .filter(new Predicate<ChangeMessage>() {
+          @Override
+          public boolean apply(ChangeMessage input) {
+            return input.getAuthor() == null;
+          }
+        })
+        .last()
+        .orNull();
   }
 
   public Change submit(RevisionResource rsrc, IdentifiedUser caller,
@@ -258,12 +287,9 @@
       ChangeUpdate update, IdentifiedUser caller, Timestamp timestamp)
       throws OrmException {
     PatchSet.Id psId = rsrc.getPatchSet().getId();
-    List<PatchSetApproval> approvals =
-        approvalsUtil.byPatchSet(dbProvider.get(), rsrc.getNotes(), psId);
-
-    Map<PatchSetApproval.Key, PatchSetApproval> byKey =
-        Maps.newHashMapWithExpectedSize(approvals.size());
-    for (PatchSetApproval psa : approvals) {
+    Map<PatchSetApproval.Key, PatchSetApproval> byKey = Maps.newHashMap();
+    for (PatchSetApproval psa :
+        approvalsUtil.byPatchSet(dbProvider.get(), rsrc.getControl(), psId)) {
       if (!byKey.containsKey(psa.getKey())) {
         byKey.put(psa.getKey(), psa);
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java
index 4510616..b95d664 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java
@@ -269,7 +269,6 @@
   }
 
   public static class SuggestedReviewerInfo implements Comparable<SuggestedReviewerInfo> {
-    String kind = "gerritcodereview#suggestedreviewer";
     public AccountInfo account;
     public GroupBaseInfo group;
 
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 4f739e6..25070ab 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
@@ -37,6 +37,7 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 
 import com.googlecode.prolog_cafe.lang.Term;
 
@@ -57,7 +58,7 @@
     public Filters filters;
   }
 
-  private final ReviewDb db;
+  private final Provider<ReviewDb> db;
   private final ChangeData.Factory changeDataFactory;
   private final RulesCache rules;
   private final AccountInfo.Loader.Factory accountInfoFactory;
@@ -66,7 +67,7 @@
   private Filters filters = Filters.RUN;
 
   @Inject
-  TestSubmitRule(ReviewDb db,
+  TestSubmitRule(Provider<ReviewDb> db,
       ChangeData.Factory changeDataFactory,
       RulesCache rules,
       AccountInfo.Loader.Factory infoFactory) {
@@ -88,12 +89,12 @@
     input.filters = Objects.firstNonNull(input.filters, filters);
 
     SubmitRuleEvaluator evaluator = new SubmitRuleEvaluator(
-        db,
+        db.get(),
         rsrc.getPatchSet(),
         rsrc.getControl().getProjectControl(),
         rsrc.getControl(),
         rsrc.getChange(),
-        changeDataFactory.create(db, rsrc.getChange()),
+        changeDataFactory.create(db.get(), rsrc.getChange()),
         false,
         "locate_submit_rule", "can_submit",
         "locate_submit_filter", "filter_submit_results",
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 c5b8b8a..3b7f419 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
@@ -17,11 +17,11 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.base.Objects;
+import com.google.gerrit.extensions.common.SubmitType;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.Project.SubmitType;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.rules.RulesCache;
 import com.google.gerrit.server.change.TestSubmitRule.Filters;
@@ -30,6 +30,7 @@
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 
 import com.googlecode.prolog_cafe.lang.SymbolTerm;
 import com.googlecode.prolog_cafe.lang.Term;
@@ -40,7 +41,7 @@
 import java.util.List;
 
 public class TestSubmitType implements RestModifyView<RevisionResource, Input> {
-  private final ReviewDb db;
+  private final Provider<ReviewDb> db;
   private final ChangeData.Factory changeDataFactory;
   private final RulesCache rules;
 
@@ -48,7 +49,7 @@
   private Filters filters = Filters.RUN;
 
   @Inject
-  TestSubmitType(ReviewDb db,
+  TestSubmitType(Provider<ReviewDb> db,
       ChangeData.Factory changeDataFactory,
       RulesCache rules) {
     this.db = db;
@@ -68,12 +69,12 @@
     input.filters = Objects.firstNonNull(input.filters, filters);
 
     SubmitRuleEvaluator evaluator = new SubmitRuleEvaluator(
-        db,
+        db.get(),
         rsrc.getPatchSet(),
         rsrc.getControl().getProjectControl(),
         rsrc.getControl(),
         rsrc.getChange(),
-        changeDataFactory.create(db, rsrc.getChange()),
+        changeDataFactory.create(db.get(), rsrc.getChange()),
         false,
         "locate_submit_type", "get_submit_type",
         "locate_submit_type_filter", "filter_submit_type_results",
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
index f87619e..77fb178 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.server.changedetail;
 
-import static com.google.gerrit.server.change.PatchSetInserter.ValidatePolicy;
-
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
@@ -29,6 +27,7 @@
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.change.PatchSetInserter;
+import com.google.gerrit.server.change.PatchSetInserter.ValidatePolicy;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MergeUtil;
@@ -38,6 +37,8 @@
 import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.CommitBuilder;
@@ -52,18 +53,20 @@
 
 import java.io.IOException;
 import java.util.List;
+import java.util.TimeZone;
 
+@Singleton
 public class RebaseChange {
   private final ChangeControl.GenericFactory changeControlFactory;
-  private final ReviewDb db;
+  private final Provider<ReviewDb> db;
   private final GitRepositoryManager gitManager;
-  private final PersonIdent myIdent;
+  private final TimeZone serverTimeZone;
   private final MergeUtil.Factory mergeUtilFactory;
   private final PatchSetInserter.Factory patchSetInserterFactory;
 
   @Inject
   RebaseChange(final ChangeControl.GenericFactory changeControlFactory,
-      final ReviewDb db,
+      final Provider<ReviewDb> db,
       @GerritPersonIdent final PersonIdent myIdent,
       final GitRepositoryManager gitManager,
       final MergeUtil.Factory mergeUtilFactory,
@@ -71,7 +74,7 @@
     this.changeControlFactory = changeControlFactory;
     this.db = db;
     this.gitManager = gitManager;
-    this.myIdent = myIdent;
+    this.serverTimeZone = myIdent.getTimeZone();
     this.mergeUtilFactory = mergeUtilFactory;
     this.patchSetInserterFactory = patchSetInserterFactory;
   }
@@ -92,6 +95,7 @@
    * E-mail notification and triggering of hooks happens for the creation of the
    * new patch set.
    *
+   * @param change the change to perform the rebase for
    * @param patchSetId the id of the patch set
    * @param uploader the user that creates the rebased patch set
    * @throws NoSuchChangeException thrown if the change to which the patch set
@@ -102,18 +106,17 @@
    * @throws IOException thrown if rebase is not possible or not needed
    * @throws InvalidChangeOperationException thrown if rebase is not allowed
    */
-  public void rebase(final PatchSet.Id patchSetId, final IdentifiedUser uploader)
+  public void rebase(Change change, PatchSet.Id patchSetId, final IdentifiedUser uploader)
       throws NoSuchChangeException, EmailException, OrmException, IOException,
       InvalidChangeOperationException {
     final Change.Id changeId = patchSetId.getParentKey();
     final ChangeControl changeControl =
-        changeControlFactory.validateFor(changeId, uploader);
+        changeControlFactory.validateFor(change, uploader);
     if (!changeControl.canRebase()) {
       throw new InvalidChangeOperationException(
           "Cannot rebase: New patch sets are not allowed to be added to change: "
               + changeId.toString());
     }
-    final Change change = changeControl.getChange();
     Repository git = null;
     RevWalk rw = null;
     ObjectInserter inserter = null;
@@ -122,14 +125,14 @@
       rw = new RevWalk(git);
       inserter = git.newObjectInserter();
 
-      final String baseRev = findBaseRevision(patchSetId, db,
+      final String baseRev = findBaseRevision(patchSetId, db.get(),
           change.getDest(), git, null, null, null);
       final RevCommit baseCommit =
           rw.parseCommit(ObjectId.fromString(baseRev));
 
       PersonIdent committerIdent =
-          uploader.newCommitterIdent(myIdent.getWhen(),
-              myIdent.getTimeZone());
+          uploader.newCommitterIdent(TimeUtil.nowTs(),
+              serverTimeZone);
 
       rebase(git, rw, inserter, patchSetId, change,
           uploader, baseCommit, mergeUtilFactory.create(
@@ -282,7 +285,7 @@
     if (!change.currentPatchSetId().equals(patchSetId)) {
       throw new InvalidChangeOperationException("patch set is not current");
     }
-    final PatchSet originalPatchSet = db.patchSets().get(patchSetId);
+    final PatchSet originalPatchSet = db.get().patchSets().get(patchSetId);
 
     final RevCommit rebasedCommit;
     ObjectId oldId = ObjectId.fromString(originalPatchSet.getRevision().get());
@@ -305,8 +308,9 @@
 
     final PatchSet.Id newPatchSetId = patchSetInserter.getPatchSetId();
     final ChangeMessage cmsg = new ChangeMessage(
-        new ChangeMessage.Key(change.getId(), ChangeUtil.messageUUID(db)),
-        uploader.getAccountId(), TimeUtil.nowTs(), patchSetId);
+        new ChangeMessage.Key(change.getId(),
+            ChangeUtil.messageUUID(db.get())), uploader.getAccountId(),
+            TimeUtil.nowTs(), patchSetId);
 
     cmsg.setMessage("Patch Set " + newPatchSetId.get()
         + ": Patch Set " + patchSetId.get() + " was rebased");
@@ -315,7 +319,7 @@
         .setMessage(cmsg)
         .insert();
 
-    return db.patchSets().get(newChange.currentPatchSetId());
+    return db.get().patchSets().get(newChange.currentPatchSetId());
   }
 
   /**
@@ -375,7 +379,7 @@
     try {
       findBaseRevision(
           r.getPatchSet().getId(),
-          db,
+          db.get(),
           r.getChange().getDest(),
           git,
           null,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_58.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/AllUsersName.java
similarity index 60%
copy from gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_58.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/config/AllUsersName.java
index 72b0c8b..ff28be4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_58.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/AllUsersName.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2011 The Android Open Source Project
+// Copyright (C) 2014 The Android Open 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,14 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.schema;
+package com.google.gerrit.server.config;
 
-import com.google.inject.Inject;
-import com.google.inject.Provider;
+import com.google.gerrit.reviewdb.client.Project;
 
-public class Schema_58 extends SchemaVersion {
-  @Inject
-  Schema_58(Provider<Schema_57> prior) {
-    super(prior);
+/** Special name of the project in which meta data for all users is stored. */
+@SuppressWarnings("serial")
+public class AllUsersName extends Project.NameKey {
+  public AllUsersName(String name) {
+    super(name);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/AllUsersNameProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/AllUsersNameProvider.java
new file mode 100644
index 0000000..e6ec095
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/AllUsersNameProvider.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.lib.Config;
+
+public class AllUsersNameProvider implements Provider<AllUsersName> {
+  public static final String DEFAULT = "All-Users";
+
+  private final AllUsersName name;
+
+  @Inject
+  AllUsersNameProvider(@GerritServerConfig Config cfg) {
+    String n = cfg.getString("gerrit", null, "allUsers");
+    if (n == null || n.isEmpty()) {
+      n = DEFAULT;
+    }
+    name = new AllUsersName(n);
+  }
+
+  public AllUsersName get() {
+    return name;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java
index 6d01e7c5..c2cf95e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java
@@ -54,8 +54,6 @@
   private final SignedToken emailReg;
   private final SignedToken restToken;
 
-  private final boolean allowGoogleAccountUpgrade;
-
   @Inject
   AuthConfig(@GerritServerConfig final Config cfg)
       throws XsrfException {
@@ -97,13 +95,6 @@
     } else {
       restToken = null;
     }
-
-    if (authType == AuthType.OPENID) {
-      allowGoogleAccountUpgrade =
-          cfg.getBoolean("auth", "allowgoogleaccountupgrade", false);
-    } else {
-      allowGoogleAccountUpgrade = false;
-    }
   }
 
   private static List<OpenIdProviderPattern> toPatterns(Config cfg, String name) {
@@ -112,7 +103,7 @@
       s = new String[] {"http://", "https://"};
     }
 
-    List<OpenIdProviderPattern> r = new ArrayList<OpenIdProviderPattern>();
+    List<OpenIdProviderPattern> r = new ArrayList<>();
     for (String pattern : s) {
       r.add(OpenIdProviderPattern.create(pattern));
     }
@@ -172,10 +163,6 @@
     return restToken;
   }
 
-  public boolean isAllowGoogleAccountUpgrade() {
-    return allowGoogleAccountUpgrade;
-  }
-
   /** OpenID identities which the server permits for authentication. */
   public List<OpenIdProviderPattern> getAllowedOpenIDs() {
     return allowedOpenIDs;
@@ -210,7 +197,7 @@
       case LDAP_BIND:
       case CLIENT_SSL_CERT_LDAP:
       case CUSTOM_EXTENSION:
-        // Its safe to assume yes for an HTTP authentication type, as the
+      case OAUTH:
         // only way in is through some external system that the admin trusts
         //
         return true;
@@ -237,14 +224,6 @@
   }
 
   private boolean isTrusted(final AccountExternalId id) {
-    if (id.isScheme(AccountExternalId.LEGACY_GAE)) {
-      // Assume this is a trusted token, its a legacy import from
-      // a fairly well respected provider and only takes effect if
-      // the administrator has the import still enabled
-      //
-      return isAllowGoogleAccountUpgrade();
-    }
-
     if (id.isScheme(AccountExternalId.SCHEME_MAILTO)) {
       // mailto identities are created by sending a unique validation
       // token to the address and asking them to come back to the site
@@ -277,4 +256,9 @@
   public String getRegisterPageUrl() {
     return registerPageUrl;
   }
+
+  public boolean isLdapAuthType() {
+    return authType == AuthType.LDAP ||
+        authType == AuthType.LDAP_BIND;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/CacheResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/CacheResource.java
new file mode 100644
index 0000000..afb972b6
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/CacheResource.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import com.google.common.cache.Cache;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.inject.Provider;
+import com.google.inject.TypeLiteral;
+
+public class CacheResource extends ConfigResource {
+  public static final TypeLiteral<RestView<CacheResource>> CACHE_KIND =
+      new TypeLiteral<RestView<CacheResource>>() {};
+
+  private final String name;
+  private final Provider<Cache<?, ?>> cacheProvider;
+
+  public CacheResource(String pluginName, String cacheName, Provider<Cache<?, ?>> cacheProvider) {
+    this.name = cacheNameOf(pluginName, cacheName);
+    this.cacheProvider = cacheProvider;
+  }
+
+  public CacheResource(String pluginName, String cacheName, final Cache<?, ?> cache) {
+    this(pluginName, cacheName, new Provider<Cache<?, ?>>() {
+      @Override
+      public Cache<?, ?> get() {
+        return cache;
+      }
+    });
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public Cache<?, ?> getCache() {
+    return cacheProvider.get();
+  }
+
+  public static String cacheNameOf(String plugin, String name) {
+    if ("gerrit".equals(plugin)) {
+      return name;
+    } else {
+      return plugin + "-" + name;
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/CachesCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/CachesCollection.java
new file mode 100644
index 0000000..5bff20c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/CachesCollection.java
@@ -0,0 +1,99 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import com.google.common.cache.Cache;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AcceptsPost;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.CurrentUser;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+@RequiresCapability(GlobalCapability.VIEW_CACHES)
+@Singleton
+public class CachesCollection implements
+    ChildCollection<ConfigResource, CacheResource>, AcceptsPost<ConfigResource> {
+
+  private final DynamicMap<RestView<CacheResource>> views;
+  private final Provider<ListCaches> list;
+  private final Provider<CurrentUser> self;
+  private final DynamicMap<Cache<?, ?>> cacheMap;
+  private final PostCaches postCaches;
+
+  @Inject
+  CachesCollection(DynamicMap<RestView<CacheResource>> views,
+      Provider<ListCaches> list, Provider<CurrentUser> self,
+      DynamicMap<Cache<?, ?>> cacheMap,
+      PostCaches postCaches) {
+    this.views = views;
+    this.list = list;
+    this.self = self;
+    this.cacheMap = cacheMap;
+    this.postCaches = postCaches;
+  }
+
+  @Override
+  public RestView<ConfigResource> list() {
+    return list.get();
+  }
+
+  @Override
+  public CacheResource parse(ConfigResource parent, IdString id)
+      throws AuthException, ResourceNotFoundException {
+    CurrentUser user = self.get();
+    if (user instanceof AnonymousUser) {
+      throw new AuthException("Authentication required");
+    } else if (!user.isIdentifiedUser()) {
+      throw new ResourceNotFoundException();
+    } else if (!user.getCapabilities().canViewCaches()) {
+      throw new AuthException("not allowed to view caches");
+    }
+
+    String cacheName = id.get();
+    String pluginName = "gerrit";
+    int i = cacheName.lastIndexOf('-');
+    if (i != -1) {
+      pluginName = cacheName.substring(0, i);
+      cacheName = cacheName.length() > i + 1 ? cacheName.substring(i + 1) : "";
+    }
+
+    Provider<Cache<?, ?>> cacheProvider = cacheMap.byPlugin(pluginName).get(cacheName);
+    if (cacheProvider == null) {
+      throw new ResourceNotFoundException(id);
+    }
+    return new CacheResource(pluginName, cacheName, cacheProvider);
+  }
+
+  @Override
+  public DynamicMap<RestView<CacheResource>> views() {
+    return views;
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public PostCaches post(ConfigResource parent) throws RestApiException {
+    return postCaches;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/CapabilitiesCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/CapabilitiesCollection.java
index 3a8bcc5..7ce5a88 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/CapabilitiesCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/CapabilitiesCollection.java
@@ -20,23 +20,24 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
+@Singleton
 public class CapabilitiesCollection implements
     ChildCollection<ConfigResource, CapabilityResource> {
   private final DynamicMap<RestView<CapabilityResource>> views;
-  private final Provider<ListCapabilities> list;
+  private final ListCapabilities list;
 
   @Inject
   CapabilitiesCollection(DynamicMap<RestView<CapabilityResource>> views,
-      Provider<ListCapabilities> list) {
+      ListCapabilities list) {
     this.views = views;
     this.list = list;
   }
 
   @Override
   public RestView<ConfigResource> list() throws ResourceNotFoundException {
-    return list.get();
+    return list;
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigCollection.java
index 5ed007a..72c4f2e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigCollection.java
@@ -21,7 +21,9 @@
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
+@Singleton
 public class ConfigCollection implements
     RestCollection<TopLevelResource, ConfigResource> {
   private final DynamicMap<RestView<ConfigResource>> views;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java
index e081b02..ab290cb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java
@@ -165,7 +165,7 @@
   public static <T extends Enum<?>> List<T> getEnumList(final Config config,
       final String section, final String subsection, final String setting,
       final T[] all, final T defaultValue) {
-    final List<T> list = new ArrayList<T>();
+    final List<T> list = new ArrayList<>();
     final String[] values = config.getStringList(section, subsection, setting);
     if (values.length == 0) {
       list.add(defaultValue);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/DeleteTask.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/DeleteTask.java
new file mode 100644
index 0000000..b029060
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/DeleteTask.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.config.DeleteTask.Input;
+import com.google.inject.Singleton;
+
+@Singleton
+@RequiresCapability(GlobalCapability.KILL_TASK)
+public class DeleteTask implements RestModifyView<TaskResource, Input> {
+  public static class Input {
+  }
+
+  @Override
+  public Response<?> apply(TaskResource rsrc, Input input) {
+    rsrc.getTask().cancel(true);
+    return Response.none();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/DownloadConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/DownloadConfig.java
index f259871..80031c2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/DownloadConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/DownloadConfig.java
@@ -40,13 +40,13 @@
         ConfigUtil.getEnumList(cfg, "download", null, "scheme",
             DownloadScheme.DEFAULT_DOWNLOADS);
     downloadSchemes =
-        Collections.unmodifiableSet(new HashSet<DownloadScheme>(allSchemes));
+        Collections.unmodifiableSet(new HashSet<>(allSchemes));
 
     List<DownloadCommand> allCommands =
         ConfigUtil.getEnumList(cfg, "download", null, "command",
             DownloadCommand.DEFAULT_DOWNLOADS);
     downloadCommands =
-        Collections.unmodifiableSet(new HashSet<DownloadCommand>(allCommands));
+        Collections.unmodifiableSet(new HashSet<>(allCommands));
   }
 
   /** Scheme used to download. */
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/FlushCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/FlushCache.java
new file mode 100644
index 0000000..a7c03be
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/FlushCache.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.FlushCache.Input;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+@RequiresCapability(GlobalCapability.FLUSH_CACHES)
+@Singleton
+public class FlushCache implements RestModifyView<CacheResource, Input> {
+  public static class Input {
+  }
+
+  public static final String WEB_SESSIONS = "web_sessions";
+
+  private final Provider<CurrentUser> self;
+
+  @Inject
+  public FlushCache(Provider<CurrentUser> self) {
+    this.self = self;
+  }
+
+  @Override
+  public Response<String> apply(CacheResource rsrc, Input input)
+      throws AuthException {
+    if (WEB_SESSIONS.equals(rsrc.getName())
+        && !self.get().getCapabilities().canAdministrateServer()) {
+      throw new AuthException(String.format(
+          "only site administrators can flush %s", WEB_SESSIONS));
+    }
+
+    rsrc.getCache().invalidateAll();
+    return Response.ok("");
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GcConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GcConfig.java
new file mode 100644
index 0000000..54096bb
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GcConfig.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ConfigConstants;
+
+@Singleton
+public class GcConfig {
+  private final ScheduleConfig scheduleConfig;
+
+  @Inject
+  GcConfig(@GerritServerConfig Config cfg) {
+    scheduleConfig = new ScheduleConfig(cfg, ConfigConstants.CONFIG_GC_SECTION);
+  }
+
+  public ScheduleConfig getScheduleConfig() {
+    return scheduleConfig;
+  }
+
+}
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 c6598d4..8c686d5 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
@@ -27,10 +27,13 @@
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.events.NewProjectCreatedListener;
 import com.google.gerrit.extensions.events.ProjectDeletedListener;
+import com.google.gerrit.extensions.events.UsageDataPublishedListener;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.systemstatus.MessageOfTheDay;
+import com.google.gerrit.extensions.webui.PatchSetWebLink;
+import com.google.gerrit.extensions.webui.ProjectWebLink;
 import com.google.gerrit.extensions.webui.TopMenu;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.rules.PrologModule;
@@ -42,6 +45,8 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.MimeUtilFileTypeRegistry;
 import com.google.gerrit.server.PluginUser;
+import com.google.gerrit.server.WebLinks;
+import com.google.gerrit.server.WebLinksProvider;
 import com.google.gerrit.server.account.AccountByEmailCacheImpl;
 import com.google.gerrit.server.account.AccountCacheImpl;
 import com.google.gerrit.server.account.AccountControl;
@@ -65,7 +70,7 @@
 import com.google.gerrit.server.auth.UniversalAuthBackend;
 import com.google.gerrit.server.avatar.AvatarProvider;
 import com.google.gerrit.server.cache.CacheRemovalListener;
-import com.google.gerrit.server.change.ChangeKindCache;
+import com.google.gerrit.server.change.ChangeKindCacheImpl;
 import com.google.gerrit.server.change.MergeabilityChecker;
 import com.google.gerrit.server.events.EventFactory;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
@@ -77,7 +82,6 @@
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.git.NotesBranchUtil;
 import com.google.gerrit.server.git.ReceivePackInitializer;
-import com.google.gerrit.server.git.ReloadSubmitQueueOp;
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.git.TransferConfig;
 import com.google.gerrit.server.git.validators.CommitValidationListener;
@@ -85,6 +89,8 @@
 import com.google.gerrit.server.git.validators.MergeValidationListener;
 import com.google.gerrit.server.git.validators.MergeValidators;
 import com.google.gerrit.server.git.validators.MergeValidators.ProjectConfigValidator;
+import com.google.gerrit.server.git.validators.UploadValidationListener;
+import com.google.gerrit.server.git.validators.UploadValidators;
 import com.google.gerrit.server.group.GroupModule;
 import com.google.gerrit.server.mail.AddReviewerSender;
 import com.google.gerrit.server.mail.CreateChangeSender;
@@ -127,6 +133,7 @@
 
 import org.apache.velocity.runtime.RuntimeInstance;
 import org.eclipse.jgit.transport.PostReceiveHook;
+import org.eclipse.jgit.transport.PreUploadHook;
 
 import java.util.List;
 import java.util.Set;
@@ -152,7 +159,7 @@
     install(AccountByEmailCacheImpl.module());
     install(AccountCacheImpl.module());
     install(ChangeCache.module());
-    install(ChangeKindCache.module());
+    install(ChangeKindCacheImpl.module());
     install(ConflictsCacheImpl.module());
     install(GroupCacheImpl.module());
     install(GroupIncludeCacheImpl.module());
@@ -214,16 +221,18 @@
     bind(EventFactory.class);
     bind(TransferConfig.class);
 
+    bind(GcConfig.class);
+
     bind(ApprovalsUtil.class);
     bind(ChangeMergeQueue.class).in(SINGLETON);
     bind(MergeQueue.class).to(ChangeMergeQueue.class).in(SINGLETON);
-    factory(ReloadSubmitQueueOp.Factory.class);
 
     bind(RuntimeInstance.class)
         .toProvider(VelocityRuntimeProvider.class)
         .in(SINGLETON);
     bind(FromAddressGenerator.class).toProvider(
         FromAddressGeneratorProvider.class).in(SINGLETON);
+    bind(WebLinks.class).toProvider(WebLinksProvider.class).in(SINGLETON);
 
     bind(PatchSetInfoFactory.class);
     bind(IdentifiedUser.GenericFactory.class).in(SINGLETON);
@@ -247,9 +256,11 @@
     DynamicSet.setOf(binder(), GitReferenceUpdatedListener.class);
     DynamicSet.setOf(binder(), ReceivePackInitializer.class);
     DynamicSet.setOf(binder(), PostReceiveHook.class);
+    DynamicSet.setOf(binder(), PreUploadHook.class);
     DynamicSet.setOf(binder(), NewProjectCreatedListener.class);
     DynamicSet.setOf(binder(), ProjectDeletedListener.class);
     DynamicSet.setOf(binder(), HeadUpdatedListener.class);
+    DynamicSet.setOf(binder(), UsageDataPublishedListener.class);
     DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(ChangeCache.class);
     DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(MergeabilityChecker.class);
     DynamicSet.bind(binder(), GitReferenceUpdatedListener.class)
@@ -266,6 +277,11 @@
     DynamicMap.mapOf(binder(), DownloadScheme.class);
     DynamicMap.mapOf(binder(), DownloadCommand.class);
     DynamicMap.mapOf(binder(), ProjectConfigEntry.class);
+    DynamicSet.setOf(binder(), PatchSetWebLink.class);
+    DynamicSet.setOf(binder(), ProjectWebLink.class);
+
+    factory(UploadValidators.Factory.class);
+    DynamicSet.setOf(binder(), UploadValidationListener.class);
 
     bind(AnonymousUser.class);
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
index 31fa329..872c473 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
@@ -18,13 +18,11 @@
 
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.RequestCleanup;
-import com.google.gerrit.server.git.BanCommit;
 import com.google.gerrit.server.git.MergeOp;
 import com.google.gerrit.server.git.SubmoduleOp;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.PerRequestProjectControlCache;
 import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.server.project.SuggestParentCandidates;
 import com.google.inject.servlet.RequestScoped;
 
 /** Bindings for {@link RequestScoped} entities. */
@@ -41,11 +39,5 @@
 
     factory(SubmoduleOp.Factory.class);
     factory(MergeOp.Factory.class);
-
-    // Not really per-request, but dammit, I don't know where else to
-    // easily park this stuff.
-    //
-    factory(SuggestParentCandidates.Factory.class);
-    factory(BanCommit.Factory.class);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetCache.java
new file mode 100644
index 0000000..53628cc
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetCache.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.config.ListCaches.CacheInfo;
+import com.google.inject.Singleton;
+
+@Singleton
+public class GetCache implements RestReadView<CacheResource> {
+
+  @Override
+  public CacheInfo apply(CacheResource rsrc) {
+    return new CacheInfo(rsrc.getName(), rsrc.getCache());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetPreferences.java
new file mode 100644
index 0000000..e915427
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetPreferences.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.account.GetPreferences.PreferenceInfo;
+import com.google.gerrit.server.account.VersionedAccountPreferences;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+
+@Singleton
+public class GetPreferences implements RestReadView<ConfigResource> {
+  private final AllUsersName allUsersName;
+  private final GitRepositoryManager gitMgr;
+
+  @Inject
+  public GetPreferences(AllUsersName allUsersName,
+      GitRepositoryManager gitMgr) {
+    this.allUsersName = allUsersName;
+    this.gitMgr = gitMgr;
+  }
+
+  @Override
+  public PreferenceInfo apply(ConfigResource rsrc)
+      throws IOException, ConfigInvalidException {
+    Repository git = gitMgr.openRepository(allUsersName);
+    try {
+      VersionedAccountPreferences p =
+          VersionedAccountPreferences.forDefault();
+      p.load(git);
+      return new PreferenceInfo(null, p, git);
+    } finally {
+      git.close();
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetSummary.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetSummary.java
new file mode 100644
index 0000000..9aa8590
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetSummary.java
@@ -0,0 +1,262 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.git.WorkQueue.Task;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.internal.storage.file.WindowCacheStatAccessor;
+import org.kohsuke.args4j.Option;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.management.ManagementFactory;
+import java.lang.management.OperatingSystemMXBean;
+import java.lang.management.RuntimeMXBean;
+import java.lang.management.ThreadInfo;
+import java.lang.management.ThreadMXBean;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+public class GetSummary implements RestReadView<ConfigResource> {
+
+  private final WorkQueue workQueue;
+  private final File sitePath;
+
+  @Option(name = "--gc", usage = "perform Java GC before retrieving memory stats")
+  private boolean gc;
+
+  public GetSummary setGc(boolean gc) {
+    this.gc = gc;
+    return this;
+  }
+
+  @Option(name = "--jvm", usage = "include details about the JVM")
+  private boolean jvm;
+
+  public GetSummary setJvm(boolean jvm) {
+    this.jvm = jvm;
+    return this;
+  }
+
+  @Inject
+  public GetSummary(WorkQueue workQueue, @SitePath File sitePath) {
+    this.workQueue = workQueue;
+    this.sitePath = sitePath;
+  }
+
+  @Override
+  public SummaryInfo apply(ConfigResource rsrc)  {
+    if (gc) {
+      System.gc();
+      System.runFinalization();
+      System.gc();
+    }
+
+    SummaryInfo summary = new SummaryInfo();
+    summary.taskSummary = getTaskSummary();
+    summary.memSummary = getMemSummary();
+    summary.threadSummary = getThreadSummary();
+    if (jvm) {
+      summary.jvmSummary = getJvmSummary();
+    }
+    return summary;
+  }
+
+  private TaskSummaryInfo getTaskSummary() {
+    Collection<Task<?>> pending = workQueue.getTasks();
+    int tasksTotal = pending.size();
+    int tasksRunning = 0, tasksReady = 0, tasksSleeping = 0;
+    for (Task<?> task : pending) {
+      switch (task.getState()) {
+        case RUNNING: tasksRunning++; break;
+        case READY: tasksReady++; break;
+        case SLEEPING: tasksSleeping++; break;
+        case CANCELLED:
+        case DONE:
+        case OTHER:
+          break;
+      }
+    }
+
+    TaskSummaryInfo taskSummary = new TaskSummaryInfo();
+    taskSummary.total = toInteger(tasksTotal);
+    taskSummary.running = toInteger(tasksRunning);
+    taskSummary.ready = toInteger(tasksReady);
+    taskSummary.sleeping = toInteger(tasksSleeping);
+    return taskSummary;
+  }
+
+  private MemSummaryInfo getMemSummary() {
+    Runtime r = Runtime.getRuntime();
+    long mMax = r.maxMemory();
+    long mFree = r.freeMemory();
+    long mTotal = r.totalMemory();
+    long mInuse = mTotal - mFree;
+
+    int jgitOpen = WindowCacheStatAccessor.getOpenFiles();
+    long jgitBytes = WindowCacheStatAccessor.getOpenBytes();
+
+    MemSummaryInfo memSummaryInfo = new MemSummaryInfo();
+    memSummaryInfo.total = bytes(mTotal);
+    memSummaryInfo.used = bytes(mInuse - jgitBytes);
+    memSummaryInfo.free = bytes(mFree);
+    memSummaryInfo.buffers = bytes(jgitBytes);
+    memSummaryInfo.max = bytes(mMax);
+    memSummaryInfo.openFiles = toInteger(jgitOpen);
+    return memSummaryInfo;
+  }
+
+  private ThreadSummaryInfo getThreadSummary() {
+    Runtime r = Runtime.getRuntime();
+    ThreadSummaryInfo threadInfo = new ThreadSummaryInfo();
+    threadInfo.cpus = r.availableProcessors();
+    threadInfo.threads = toInteger(ManagementFactory.getThreadMXBean().getThreadCount());
+
+    List<String> prefixes = Arrays.asList("HTTP", "IntraLineDiff", "ReceiveCommits",
+        "SSH git-receive-pack", "SSH git-upload-pack", "SSH-Interactive-Worker",
+        "SSH-Stream-Worker", "SshCommandStart");
+    String other = "Other";
+    ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
+
+    threadInfo.counts = new HashMap<>();
+    for (long id : threadMXBean.getAllThreadIds()) {
+      ThreadInfo info = threadMXBean.getThreadInfo(id);
+      if (info == null) {
+        continue;
+      }
+      String name = info.getThreadName();
+      Thread.State state = info.getThreadState();
+      String group = other;
+      for (String p : prefixes) {
+        if (name.startsWith(p)) {
+          group = p;
+          break;
+        }
+      }
+      Map<Thread.State, Integer> counts = threadInfo.counts.get(group);
+      if (counts == null) {
+        counts = new HashMap<>();
+        threadInfo.counts.put(group, counts);
+      }
+      Integer c = counts.get(state);
+      counts.put(state, c != null ? c + 1 : 1);
+    }
+
+    return threadInfo;
+  }
+
+  private JvmSummaryInfo getJvmSummary() {
+    OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean();
+    RuntimeMXBean runtimeBean = ManagementFactory.getRuntimeMXBean();
+
+    JvmSummaryInfo jvmSummary = new JvmSummaryInfo();
+    jvmSummary.vmVendor = runtimeBean.getVmVendor();
+    jvmSummary.vmName = runtimeBean.getVmName();
+    jvmSummary.vmVersion = runtimeBean.getVmVersion();
+    jvmSummary.osName = osBean.getName();
+    jvmSummary.osVersion = osBean.getVersion();
+    jvmSummary.osArch = osBean.getArch();
+    jvmSummary.user = System.getProperty("user.name");
+
+    try {
+      jvmSummary.host = InetAddress.getLocalHost().getHostName();
+    } catch (UnknownHostException e) {
+    }
+
+    jvmSummary.currentWorkingDirectory = path(new File(".").getAbsoluteFile().getParentFile());
+    jvmSummary.site = path(sitePath);
+    return jvmSummary;
+  }
+
+  private static Integer toInteger(int i) {
+    return i != 0 ? i : null;
+  }
+
+  private static String bytes(double value) {
+    value /= 1024;
+    String suffix = "k";
+
+    if (value > 1024) {
+      value /= 1024;
+      suffix = "m";
+    }
+    if (value > 1024) {
+      value /= 1024;
+      suffix = "g";
+    }
+    return String.format("%1$6.2f%2$s", value, suffix).trim();
+  }
+
+  private static String path(File file) {
+    try {
+      return file.getCanonicalPath();
+    } catch (IOException err) {
+      return file.getAbsolutePath();
+    }
+  }
+
+  public static class SummaryInfo {
+    public TaskSummaryInfo taskSummary;
+    public MemSummaryInfo memSummary;
+    public ThreadSummaryInfo threadSummary;
+    public JvmSummaryInfo jvmSummary;
+  }
+
+  public static class TaskSummaryInfo {
+    public Integer total;
+    public Integer running;
+    public Integer ready;
+    public Integer sleeping;
+  }
+
+  public static class MemSummaryInfo {
+    public String total;
+    public String used;
+    public String free;
+    public String buffers;
+    public String max;
+    public Integer openFiles;
+  }
+
+  public static class ThreadSummaryInfo {
+    public Integer cpus;
+    public Integer threads;
+    public Map<String, Map<Thread.State, Integer>> counts;
+  }
+
+  public static class JvmSummaryInfo {
+    public String vmVendor;
+    public String vmName;
+    public String vmVersion;
+    public String osName;
+    public String osVersion;
+    public String osArch;
+    public String user;
+    public String host;
+    public String currentWorkingDirectory;
+    public String site;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetTask.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetTask.java
new file mode 100644
index 0000000..e4b3320
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetTask.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.config.ListTasks.TaskInfo;
+import com.google.inject.Singleton;
+
+@Singleton
+public class GetTask implements RestReadView<TaskResource> {
+
+  @Override
+  public TaskInfo apply(TaskResource rsrc) {
+    return new TaskInfo(rsrc.getTask());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetVersion.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetVersion.java
index f618959c..c71cb69 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetVersion.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetVersion.java
@@ -17,7 +17,9 @@
 import com.google.gerrit.common.Version;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.inject.Singleton;
 
+@Singleton
 public class GetVersion implements RestReadView<ConfigResource> {
   @Override
   public String apply(ConfigResource resource) throws ResourceNotFoundException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ListCaches.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ListCaches.java
new file mode 100644
index 0000000..df6d86b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ListCaches.java
@@ -0,0 +1,200 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import static com.google.gerrit.server.config.CacheResource.cacheNameOf;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.base.Joiner;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheStats;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.cache.PersistentCache;
+import com.google.inject.Inject;
+
+import org.kohsuke.args4j.Option;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+@RequiresCapability(GlobalCapability.VIEW_CACHES)
+public class ListCaches implements RestReadView<ConfigResource> {
+  private final DynamicMap<Cache<?, ?>> cacheMap;
+
+  public static enum OutputFormat {
+    LIST, TEXT_LIST;
+  }
+
+  @Option(name = "--format", usage = "output format")
+  private OutputFormat format;
+
+  public ListCaches setFormat(OutputFormat format) {
+    this.format = format;
+    return this;
+  }
+
+  @Inject
+  public ListCaches(DynamicMap<Cache<?, ?>> cacheMap) {
+    this.cacheMap = cacheMap;
+  }
+
+  @Override
+  public Object apply(ConfigResource rsrc) {
+    if (format == null) {
+      Map<String, CacheInfo> cacheInfos = new TreeMap<>();
+      for (DynamicMap.Entry<Cache<?, ?>> e : cacheMap) {
+        cacheInfos.put(cacheNameOf(e.getPluginName(), e.getExportName()),
+            new CacheInfo(e.getProvider().get()));
+      }
+      return cacheInfos;
+    } else {
+      List<String> cacheNames = new ArrayList<>();
+      for (DynamicMap.Entry<Cache<?, ?>> e : cacheMap) {
+        cacheNames.add(cacheNameOf(e.getPluginName(), e.getExportName()));
+      }
+      Collections.sort(cacheNames);
+
+      if (OutputFormat.TEXT_LIST.equals(format)) {
+        return BinaryResult.create(Joiner.on('\n').join(cacheNames))
+            .base64()
+            .setContentType("text/plain")
+            .setCharacterEncoding(UTF_8.name());
+      } else {
+        return cacheNames;
+      }
+    }
+  }
+
+  public enum CacheType {
+    MEM, DISK;
+  }
+
+  public static class CacheInfo {
+    public String name;
+    public CacheType type;
+    public EntriesInfo entries;
+    public String averageGet;
+    public HitRatioInfo hitRatio;
+
+    public CacheInfo(Cache<?,?> cache) {
+      this(null, cache);
+    }
+
+    public CacheInfo(String name, Cache<?,?> cache) {
+      this.name = name;
+
+      CacheStats stat = cache.stats();
+
+      entries = new EntriesInfo();
+      entries.setMem(cache.size());
+
+      averageGet = duration(stat.averageLoadPenalty());
+
+      hitRatio = new HitRatioInfo();
+      hitRatio.setMem(stat.hitCount(), stat.requestCount());
+
+      if (cache instanceof PersistentCache) {
+        type = CacheType.DISK;
+        PersistentCache.DiskStats diskStats =
+            ((PersistentCache) cache).diskStats();
+        entries.setDisk(diskStats.size());
+        entries.setSpace(diskStats.space());
+        hitRatio.setDisk(diskStats.hitCount(), diskStats.requestCount());
+      } else {
+        type = CacheType.MEM;
+      }
+    }
+
+    private static String duration(double ns) {
+      if (ns < 0.5) {
+        return null;
+      }
+      String suffix = "ns";
+      if (ns >= 1000.0) {
+        ns /= 1000.0;
+        suffix = "us";
+      }
+      if (ns >= 1000.0) {
+        ns /= 1000.0;
+        suffix = "ms";
+      }
+      if (ns >= 1000.0) {
+        ns /= 1000.0;
+        suffix = "s";
+      }
+      return String.format("%4.1f%s", ns, suffix).trim();
+    }
+  }
+
+  public static class EntriesInfo {
+    public Long mem;
+    public Long disk;
+    public String space;
+
+    public void setMem(long mem) {
+      this.mem = mem != 0 ? mem : null;
+    }
+
+    public void setDisk(long disk) {
+      this.disk = disk != 0 ? disk : null;
+    }
+
+    public void setSpace(double value) {
+      space = bytes(value);
+    }
+
+    private static String bytes(double value) {
+      value /= 1024;
+      String suffix = "k";
+
+      if (value > 1024) {
+        value /= 1024;
+        suffix = "m";
+      }
+      if (value > 1024) {
+        value /= 1024;
+        suffix = "g";
+      }
+      return String.format("%1$6.2f%2$s", value, suffix).trim();
+    }
+  }
+
+  public static class HitRatioInfo {
+    public Integer mem;
+    public Integer disk;
+
+    public void setMem(long value, long total) {
+      mem = percent(value, total);
+    }
+
+    public void setDisk(long value, long total) {
+      disk = percent(value, total);
+    }
+
+    private static Integer percent(long value, long total) {
+      if (total <= 0) {
+        return null;
+      }
+      return (int) ((100 * value) / total);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ListCapabilities.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ListCapabilities.java
index 88616e1..b01ae2f1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ListCapabilities.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ListCapabilities.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -29,6 +30,7 @@
 import java.util.Map;
 
 /** List capabilities visible to the calling user. */
+@Singleton
 public class ListCapabilities implements RestReadView<ConfigResource> {
   private static final Logger log = LoggerFactory.getLogger(ListCapabilities.class);
   private final DynamicMap<CapabilityDefinition> pluginCapabilities;
@@ -84,7 +86,6 @@
   }
 
   public static class CapabilityInfo {
-    final String kind = "gerritcodereview#capability";
     public String id;
     public String name;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ListTasks.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ListTasks.java
new file mode 100644
index 0000000..5c651a1
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ListTasks.java
@@ -0,0 +1,135 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import com.google.common.collect.ComparisonChain;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.TaskInfoFactory;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.git.WorkQueue.ProjectTask;
+import com.google.gerrit.server.git.WorkQueue.Task;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.util.IdGenerator;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+@Singleton
+public class ListTasks implements RestReadView<ConfigResource> {
+  private final WorkQueue workQueue;
+  private final ProjectCache projectCache;
+  private final Provider<IdentifiedUser> self;
+
+  @Inject
+  public ListTasks(WorkQueue workQueue, ProjectCache projectCache,
+      Provider<IdentifiedUser> self) {
+    this.workQueue = workQueue;
+    this.projectCache = projectCache;
+    this.self = self;
+  }
+
+  @Override
+  public List<TaskInfo> apply(ConfigResource resource) throws AuthException {
+    CurrentUser user = self.get();
+    if (!user.isIdentifiedUser()) {
+      throw new AuthException("Authentication required");
+    }
+
+    List<TaskInfo> allTasks = getTasks();
+    if (user.getCapabilities().canViewQueue()) {
+      return allTasks;
+    } else {
+      Map<String, Boolean> visibilityCache = new HashMap<>();
+
+      List<TaskInfo> visibleTasks = new ArrayList<>();
+      for (TaskInfo task : allTasks) {
+        if (task.projectName != null) {
+          Boolean visible = visibilityCache.get(task.projectName);
+          if (visible == null) {
+            ProjectState e = projectCache.get(new Project.NameKey(task.projectName));
+            visible = e != null ? e.controlFor(user).isVisible() : false;
+            visibilityCache.put(task.projectName, visible);
+          }
+          if (visible) {
+            visibleTasks.add(task);
+          }
+        }
+      }
+      return visibleTasks;
+    }
+  }
+
+  private List<TaskInfo> getTasks() {
+    List<TaskInfo> taskInfos =
+        workQueue.getTaskInfos(new TaskInfoFactory<TaskInfo>() {
+          @Override
+          public TaskInfo getTaskInfo(Task<?> task) {
+            return new TaskInfo(task);
+          }
+        });
+    Collections.sort(taskInfos, new Comparator<TaskInfo>() {
+      @Override
+      public int compare(TaskInfo a, TaskInfo b) {
+        return ComparisonChain.start()
+          .compare(a.state.ordinal(), b.state.ordinal())
+          .compare(a.delay, b.delay)
+          .compare(a.command, b.command)
+          .result();
+      }
+    });
+    return taskInfos;
+  }
+
+  public static class TaskInfo {
+    public String id;
+    public Task.State state;
+    public Timestamp startTime;
+    public long delay;
+    public String command;
+    public String remoteName;
+    public String projectName;
+
+    public TaskInfo(Task<?> task) {
+      this.id = IdGenerator.format(task.getTaskId());
+      this.state = task.getState();
+      this.startTime = new Timestamp(task.getStartTime().getTime());
+      this.delay = task.getDelay(TimeUnit.MILLISECONDS);
+      this.command = task.toString();
+
+      if (task instanceof ProjectTask) {
+        ProjectTask<?> projectTask = ((ProjectTask<?>) task);
+        Project.NameKey name = projectTask.getProjectNameKey();
+        if (name != null) {
+          this.projectName = name.get();
+        }
+        this.remoteName = projectTask.getRemoteName();
+      }
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ListTopMenus.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ListTopMenus.java
index c8e9d1e..291e48b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ListTopMenus.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ListTopMenus.java
@@ -19,9 +19,11 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.webui.TopMenu;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
 import java.util.List;
 
+@Singleton
 class ListTopMenus implements RestReadView<ConfigResource> {
   private final DynamicSet<TopMenu> extensions;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/MasterNodeStartup.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/MasterNodeStartup.java
index bf96a22..a7360de 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/MasterNodeStartup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/MasterNodeStartup.java
@@ -38,12 +38,12 @@
   static class Lifecycle implements LifecycleListener {
     private static final int INITIAL_DELAY_S = 15;
 
-    private final ReloadSubmitQueueOp.Factory submit;
+    private final ReloadSubmitQueueOp submit;
     private final long delay;
     private volatile ScheduledFuture<?> handle;
 
     @Inject
-    Lifecycle(ReloadSubmitQueueOp.Factory submit,
+    Lifecycle(ReloadSubmitQueueOp submit,
         @GerritServerConfig Config config) {
       this.submit = submit;
       this.delay = ConfigUtil.getTimeUnit(config,
@@ -54,10 +54,9 @@
     @Override
     public void start() {
       if (delay > 0) {
-        handle = submit.create()
-            .startWithFixedDelay(INITIAL_DELAY_S, delay, SECONDS);
+        handle = submit.startWithFixedDelay(INITIAL_DELAY_S, delay, SECONDS);
       } else {
-        handle = submit.create().start(INITIAL_DELAY_S, SECONDS);
+        handle = submit.start(INITIAL_DELAY_S, SECONDS);
       }
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java
index 57bbbf3..64848ba 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java
@@ -16,6 +16,7 @@
 
 import static com.google.gerrit.server.config.CapabilityResource.CAPABILITY_KIND;
 import static com.google.gerrit.server.config.ConfigResource.CONFIG_KIND;
+import static com.google.gerrit.server.config.TaskResource.TASK_KIND;
 import static com.google.gerrit.server.config.TopMenuResource.TOP_MENU_KIND;
 
 import com.google.gerrit.extensions.registration.DynamicMap;
@@ -24,11 +25,17 @@
 public class Module extends RestApiModule {
   @Override
   protected void configure() {
-    DynamicMap.mapOf(binder(), CONFIG_KIND);
-    DynamicMap.mapOf(binder(), TOP_MENU_KIND);
     DynamicMap.mapOf(binder(), CAPABILITY_KIND);
+    DynamicMap.mapOf(binder(), CONFIG_KIND);
+    DynamicMap.mapOf(binder(), TASK_KIND);
+    DynamicMap.mapOf(binder(), TOP_MENU_KIND);
     child(CONFIG_KIND, "capabilities").to(CapabilitiesCollection.class);
+    child(CONFIG_KIND, "tasks").to(TasksCollection.class);
+    get(TASK_KIND).to(GetTask.class);
+    delete(TASK_KIND).to(DeleteTask.class);
     child(CONFIG_KIND, "top-menus").to(TopMenuCollection.class);
     get(CONFIG_KIND, "version").to(GetVersion.class);
+    get(CONFIG_KIND, "preferences").to(GetPreferences.class);
+    put(CONFIG_KIND, "preferences").to(SetPreferences.class);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfigFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfigFactory.java
index 11a7c90..76f5323 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfigFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfigFactory.java
@@ -41,6 +41,7 @@
 public class PluginConfigFactory implements ReloadPluginListener {
   private static final Logger log =
       LoggerFactory.getLogger(PluginConfigFactory.class);
+  private static final String EXTENSION = ".config";
 
   private final SitePaths site;
   private final GerritServerConfigProvider cfgProvider;
@@ -301,7 +302,7 @@
    */
   public Config getProjectPluginConfig(ProjectState projectState,
       String pluginName) {
-    return projectState.getConfig(pluginName).get();
+    return projectState.getConfig(pluginName + EXTENSION).get();
   }
 
   /**
@@ -359,7 +360,7 @@
    */
   public Config getProjectPluginConfigWithInheritance(ProjectState projectState,
       String pluginName) {
-    return projectState.getConfig(pluginName).getWithInheritance();
+    return projectState.getConfig(pluginName + EXTENSION).getWithInheritance();
   }
 
   private ProjectLevelConfig getPluginConfig(Project.NameKey projectName,
@@ -368,7 +369,7 @@
     if (projectState == null) {
       throw new NoSuchProjectException(projectName);
     }
-    return projectState.getConfig(pluginName);
+    return projectState.getConfig(pluginName + EXTENSION);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/PostCaches.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/PostCaches.java
new file mode 100644
index 0000000..7302ea1
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/PostCaches.java
@@ -0,0 +1,134 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import com.google.common.cache.Cache;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.server.config.PostCaches.Input;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RequiresCapability(GlobalCapability.FLUSH_CACHES)
+@Singleton
+public class PostCaches implements RestModifyView<ConfigResource, Input> {
+  public static class Input {
+    public Operation operation;
+    public List<String> caches;
+
+    public Input() {
+    }
+
+    public Input(Operation op) {
+      this(op, null);
+    }
+
+    public Input(Operation op, List<String> c) {
+      operation = op;
+      caches = c;
+    }
+  }
+
+  public static enum Operation {
+    FLUSH_ALL, FLUSH;
+  }
+
+  private final DynamicMap<Cache<?, ?>> cacheMap;
+  private final FlushCache flushCache;
+
+  @Inject
+  public PostCaches(DynamicMap<Cache<?, ?>> cacheMap,
+      FlushCache flushCache) {
+    this.cacheMap = cacheMap;
+    this.flushCache = flushCache;
+  }
+
+  @Override
+  public Object apply(ConfigResource rsrc, Input input) throws AuthException,
+      ResourceNotFoundException, BadRequestException,
+      UnprocessableEntityException {
+    if (input == null || input.operation == null) {
+      throw new BadRequestException("operation must be specified");
+    }
+
+    switch (input.operation) {
+      case FLUSH_ALL:
+        if (input.caches != null) {
+          throw new BadRequestException(
+              "specifying caches is not allowed for operation 'FLUSH_ALL'");
+        }
+        flushAll();
+        return Response.ok("");
+      case FLUSH:
+        if (input.caches == null || input.caches.isEmpty()) {
+          throw new BadRequestException(
+              "caches must be specified for operation 'FLUSH'");
+        }
+        flush(input.caches);
+        return Response.ok("");
+      default:
+        throw new BadRequestException("unsupported operation: " + input.operation);
+    }
+  }
+
+  private void flushAll() throws AuthException {
+    for (DynamicMap.Entry<Cache<?, ?>> e : cacheMap) {
+      CacheResource cacheResource =
+          new CacheResource(e.getPluginName(), e.getExportName(),
+              e.getProvider());
+      if (FlushCache.WEB_SESSIONS.equals(cacheResource.getName())) {
+        continue;
+      }
+      flushCache.apply(cacheResource, null);
+    }
+  }
+
+  private void flush(List<String> cacheNames)
+      throws UnprocessableEntityException, AuthException {
+    List<CacheResource> cacheResources = new ArrayList<>(cacheNames.size());
+
+    for (String n : cacheNames) {
+      String pluginName = "gerrit";
+      String cacheName = n;
+      int i = cacheName.lastIndexOf('-');
+      if (i != -1) {
+        pluginName = cacheName.substring(0, i);
+        cacheName = cacheName.length() > i + 1 ? cacheName.substring(i + 1) : "";
+      }
+
+      Cache<?, ?> cache = cacheMap.get(pluginName, cacheName);
+      if (cache != null) {
+        cacheResources.add(new CacheResource(pluginName, cacheName, cache));
+      } else {
+        throw new UnprocessableEntityException(String.format(
+            "cache %s not found", n));
+      }
+    }
+
+    for (CacheResource rsrc : cacheResources) {
+      flushCache.apply(rsrc, null);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/RestCacheAdminModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/RestCacheAdminModule.java
new file mode 100644
index 0000000..992c62e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/RestCacheAdminModule.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import static com.google.gerrit.server.config.CacheResource.CACHE_KIND;
+import static com.google.gerrit.server.config.ConfigResource.CONFIG_KIND;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.RestApiModule;
+
+public class RestCacheAdminModule extends RestApiModule {
+
+  @Override
+  protected void configure() {
+    DynamicMap.mapOf(binder(), CACHE_KIND);
+    child(CONFIG_KIND, "caches").to(CachesCollection.class);
+    get(CACHE_KIND).to(GetCache.class);
+    post(CACHE_KIND, "flush").to(FlushCache.class);
+    get(CONFIG_KIND, "summary").to(GetSummary.class);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ScheduleConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ScheduleConfig.java
new file mode 100644
index 0000000..66f0171
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ScheduleConfig.java
@@ -0,0 +1,142 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import org.eclipse.jgit.lib.Config;
+import org.joda.time.DateTime;
+import org.joda.time.LocalDateTime;
+import org.joda.time.LocalTime;
+import org.joda.time.MutableDateTime;
+import org.joda.time.format.DateTimeFormat;
+import org.joda.time.format.DateTimeFormatter;
+import org.joda.time.format.ISODateTimeFormat;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.text.MessageFormat;
+import java.util.concurrent.TimeUnit;
+
+public class ScheduleConfig {
+  private static final Logger log = LoggerFactory
+      .getLogger(ScheduleConfig.class);
+  public static final long MISSING_CONFIG = -1L;
+  public static final long INVALID_CONFIG = -2L;
+  private static final String KEY_INTERVAL = "interval";
+  private static final String KEY_STARTTIME = "startTime";
+
+  private final long initialDelay;
+  private final long interval;
+
+  public ScheduleConfig(Config rc, String section) {
+    this(rc, section, null);
+  }
+
+  public ScheduleConfig(Config rc, String section, String subsection) {
+    this(rc, section, subsection, DateTime.now());
+  }
+
+  public ScheduleConfig(Config rc, String section, String subsection,
+      String keyInterval, String keyStartTime) {
+    this(rc, section, subsection, keyInterval, keyStartTime, DateTime.now());
+  }
+
+  /* For testing we need to be able to pass now */
+  ScheduleConfig(Config rc, String section, String subsection, DateTime now) {
+    this(rc, section, subsection, KEY_INTERVAL, KEY_STARTTIME, now);
+  }
+
+  private ScheduleConfig(Config rc, String section, String subsection,
+      String keyInterval, String keyStartTime, DateTime now) {
+    this.interval = interval(rc, section, subsection, keyInterval);
+    if (interval > 0) {
+      this.initialDelay = initialDelay(rc, section, subsection, keyStartTime, now,
+          interval);
+    } else {
+      this.initialDelay = interval;
+    }
+  }
+
+  public long getInitialDelay() {
+    return initialDelay;
+  }
+
+  public long getInterval() {
+    return interval;
+  }
+
+  private static long interval(Config rc, String section, String subsection,
+      String keyInterval) {
+    long interval = MISSING_CONFIG;
+    try {
+      interval =
+          ConfigUtil.getTimeUnit(rc, section, subsection, keyInterval, -1,
+              TimeUnit.MILLISECONDS);
+      if (interval == MISSING_CONFIG) {
+        log.info(MessageFormat.format(
+            "{0} schedule parameter \"{0}.{1}\" is not configured", section,
+            keyInterval));
+      }
+    } catch (IllegalArgumentException e) {
+      log.error(MessageFormat.format(
+          "Invalid {0} schedule parameter \"{0}.{1}\"", section, keyInterval),
+          e);
+      interval = INVALID_CONFIG;
+    }
+    return interval;
+  }
+
+  private static long initialDelay(Config rc, String section,
+      String subsection, String keyStartTime, DateTime now, long interval) {
+    long delay = MISSING_CONFIG;
+    String start = rc.getString(section, subsection, keyStartTime);
+    try {
+      if (start != null) {
+        DateTimeFormatter formatter;
+        MutableDateTime startTime = now.toMutableDateTime();
+        try {
+          formatter = ISODateTimeFormat.hourMinute();
+          LocalTime firstStartTime = formatter.parseLocalTime(start);
+          startTime.hourOfDay().set(firstStartTime.getHourOfDay());
+          startTime.minuteOfHour().set(firstStartTime.getMinuteOfHour());
+        } catch (IllegalArgumentException e1) {
+          formatter = DateTimeFormat.forPattern("E HH:mm");
+          LocalDateTime firstStartDateTime = formatter.parseLocalDateTime(start);
+          startTime.dayOfWeek().set(firstStartDateTime.getDayOfWeek());
+          startTime.hourOfDay().set(firstStartDateTime.getHourOfDay());
+          startTime.minuteOfHour().set(firstStartDateTime.getMinuteOfHour());
+        }
+        startTime.secondOfMinute().set(0);
+        startTime.millisOfSecond().set(0);
+        long s = startTime.getMillis();
+        long n = now.getMillis();
+        delay = (s - n) % interval;
+        if (delay <= 0) {
+          delay += interval;
+        }
+      } else {
+        log.info(MessageFormat.format(
+            "{0} schedule parameter \"{0}.{1}\" is not configured", section,
+            keyStartTime));
+      }
+    } catch (IllegalArgumentException e2) {
+      log.error(
+          MessageFormat.format("Invalid {0} schedule parameter \"{0}.{1}\"",
+              section, keyStartTime), e2);
+      delay = INVALID_CONFIG;
+    }
+    return delay;
+  }
+
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/SetPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/SetPreferences.java
new file mode 100644
index 0000000..e8b2dc5
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/SetPreferences.java
@@ -0,0 +1,74 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.account.GetPreferences.PreferenceInfo;
+import com.google.gerrit.server.account.SetPreferences.Input;
+import com.google.gerrit.server.account.VersionedAccountPreferences;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+import java.io.IOException;
+
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@Singleton
+public class SetPreferences implements RestModifyView<ConfigResource, Input> {
+  private final MetaDataUpdate.User metaDataUpdateFactory;
+  private final AllUsersName allUsersName;
+
+  @Inject
+  SetPreferences(MetaDataUpdate.User metaDataUpdateFactory,
+      AllUsersName allUsersName) {
+    this.metaDataUpdateFactory = metaDataUpdateFactory;
+    this.allUsersName = allUsersName;
+  }
+
+  @Override
+  public Object apply(ConfigResource rsrc, Input i) throws BadRequestException,
+      IOException, ConfigInvalidException {
+    if (i.changesPerPage != null || i.showSiteHeader != null
+        || i.useFlashClipboard != null || i.downloadScheme != null
+        || i.downloadCommand != null || i.copySelfOnEmail != null
+        || i.dateFormat != null || i.timeFormat != null
+        || i.reversePatchSetOrder != null
+        || i.relativeDateInChangeTable != null
+        || i.sizeBarInChangeTable != null
+        || i.legacycidInChangeTable != null
+        || i.reviewCategoryStrategy != null
+        || i.commentVisibilityStrategy != null || i.diffView != null
+        || i.changeScreen != null) {
+      throw new BadRequestException("unsupported option");
+    }
+
+    VersionedAccountPreferences p;
+    MetaDataUpdate md = metaDataUpdateFactory.create(allUsersName);
+    try {
+      p = VersionedAccountPreferences.forDefault();
+      p.load(md);
+      com.google.gerrit.server.account.SetPreferences.storeMyMenus(p, i.my);
+      p.commit(md);
+      return new PreferenceInfo(null, p, md.getRepository());
+    } finally {
+      md.close();
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/TaskResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/TaskResource.java
new file mode 100644
index 0000000..7b69533
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/TaskResource.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.git.WorkQueue.Task;
+import com.google.inject.TypeLiteral;
+
+public class TaskResource extends ConfigResource {
+  public static final TypeLiteral<RestView<TaskResource>> TASK_KIND =
+      new TypeLiteral<RestView<TaskResource>>() {};
+
+  private final Task<?> task;
+
+  public TaskResource(Task<?> task) {
+    this.task = task;
+  }
+
+  public Task<?> getTask() {
+    return task;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/TasksCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/TasksCollection.java
new file mode 100644
index 0000000..b92cfc2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/TasksCollection.java
@@ -0,0 +1,91 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.git.WorkQueue.ProjectTask;
+import com.google.gerrit.server.git.WorkQueue.Task;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+@Singleton
+public class TasksCollection implements
+    ChildCollection<ConfigResource, TaskResource> {
+  private final DynamicMap<RestView<TaskResource>> views;
+  private final ListTasks list;
+  private final WorkQueue workQueue;
+  private final Provider<IdentifiedUser> self;
+  private final ProjectCache projectCache;
+
+  @Inject
+  TasksCollection(DynamicMap<RestView<TaskResource>> views, ListTasks list,
+      WorkQueue workQueue, Provider<IdentifiedUser> self,
+      ProjectCache projectCache) {
+    this.views = views;
+    this.list = list;
+    this.workQueue = workQueue;
+    this.self = self;
+    this.projectCache = projectCache;
+  }
+
+  @Override
+  public RestView<ConfigResource> list() {
+    return list;
+  }
+
+  @Override
+  public TaskResource parse(ConfigResource parent, IdString id)
+      throws ResourceNotFoundException, AuthException {
+    CurrentUser user = self.get();
+    if (!user.isIdentifiedUser()) {
+      throw new AuthException("Authentication required");
+    }
+
+    try {
+      int taskId = (int) Long.parseLong(id.get(), 16);
+      Task<?> task = workQueue.getTask(taskId);
+      if (task != null) {
+        if (self.get().getCapabilities().canViewQueue()) {
+          return new TaskResource(task);
+        } else if (task instanceof ProjectTask) {
+          ProjectTask<?> projectTask = ((ProjectTask<?>) task);
+          ProjectState e = projectCache.get(projectTask.getProjectNameKey());
+          if (e != null && e.controlFor(user).isVisible()) {
+            return new TaskResource(task);
+          }
+        }
+      }
+      throw new ResourceNotFoundException(id);
+    } catch (NumberFormatException e) {
+      throw new ResourceNotFoundException(id);
+    }
+  }
+
+  @Override
+  public DynamicMap<RestView<TaskResource>> views() {
+    return views;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/TopMenuCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/TopMenuCollection.java
index 55fd2df..32416c52 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/TopMenuCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/TopMenuCollection.java
@@ -20,23 +20,24 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
+@Singleton
 class TopMenuCollection implements
     ChildCollection<ConfigResource, TopMenuResource> {
   private final DynamicMap<RestView<TopMenuResource>> views;
-  private final Provider<ListTopMenus> list;
+  private final ListTopMenus list;
 
   @Inject
   TopMenuCollection(DynamicMap<RestView<TopMenuResource>> views,
-      Provider<ListTopMenus> list) {
+      ListTopMenus list) {
     this.views = views;
     this.list = list;
   }
 
   @Override
   public RestView<ConfigResource> list() throws ResourceNotFoundException {
-    return list.get();
+    return list;
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/TrackingFootersProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/TrackingFootersProvider.java
index c10c0eb..a76b010 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/TrackingFootersProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/TrackingFootersProvider.java
@@ -34,8 +34,7 @@
   private static String FOOTER_TAG = "footer";
   private static String SYSTEM_TAG = "system";
   private static String REGEX_TAG = "match";
-  private final List<TrackingFooter> trackingFooters =
-      new ArrayList<TrackingFooter>();
+  private final List<TrackingFooter> trackingFooters = new ArrayList<>();
   private static final Logger log =
       LoggerFactory.getLogger(TrackingFootersProvider.class);
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/contact/EncryptedContactStore.java b/gerrit-server/src/main/java/com/google/gerrit/server/contact/EncryptedContactStore.java
index 8c1fdb6..f1f374c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/contact/EncryptedContactStore.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/contact/EncryptedContactStore.java
@@ -38,6 +38,9 @@
 import org.bouncycastle.openpgp.PGPPublicKeyRing;
 import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
 import org.bouncycastle.openpgp.PGPUtil;
+import org.bouncycastle.openpgp.bc.BcPGPPublicKeyRingCollection;
+import org.bouncycastle.openpgp.operator.bc.BcPGPDataEncryptorBuilder;
+import org.bouncycastle.openpgp.operator.bc.BcPublicKeyKeyEncryptionMethodGenerator;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -49,7 +52,6 @@
 import java.io.OutputStream;
 import java.net.URL;
 import java.security.NoSuchAlgorithmException;
-import java.security.NoSuchProviderException;
 import java.security.SecureRandom;
 import java.sql.Timestamp;
 import java.text.SimpleDateFormat;
@@ -94,11 +96,7 @@
     //
     try {
       encrypt("test", new Date(0), "test".getBytes("UTF-8"));
-    } catch (NoSuchProviderException e) {
-      throw new ProvisionException("PGP encryption not available", e);
-    } catch (PGPException e) {
-      throw new ProvisionException("PGP encryption not available", e);
-    } catch (IOException e) {
+    } catch (PGPException | IOException e) {
       throw new ProvisionException("PGP encryption not available", e);
     }
   }
@@ -111,7 +109,7 @@
   private static PGPPublicKeyRingCollection readPubRing(final File pub) {
     try (InputStream fin = new FileInputStream(pub);
         InputStream in = PGPUtil.getDecoderStream(fin)) {
-        return new PGPPublicKeyRingCollection(in);
+        return new BcPGPPublicKeyRingCollection(in);
     } catch (IOException e) {
       throw new ProvisionException("Cannot read " + pub, e);
     } catch (PGPException e) {
@@ -155,29 +153,26 @@
       u.put("account_id", String.valueOf(account.getId().get()));
       u.put("data", encStr);
       connFactory.open(storeUrl).store(u.toString().getBytes("UTF-8"));
-    } catch (IOException e) {
-      log.error("Cannot store encrypted contact information", e);
-      throw new ContactInformationStoreException(e);
-    } catch (PGPException e) {
-      log.error("Cannot store encrypted contact information", e);
-      throw new ContactInformationStoreException(e);
-    } catch (NoSuchProviderException e) {
+    } catch (IOException | PGPException e) {
       log.error("Cannot store encrypted contact information", e);
       throw new ContactInformationStoreException(e);
     }
   }
 
-  @SuppressWarnings("deprecation")
-  private final PGPEncryptedDataGenerator cpk()
-      throws NoSuchProviderException, PGPException {
+  private final PGPEncryptedDataGenerator cpk() {
+    final BcPGPDataEncryptorBuilder builder =
+        new BcPGPDataEncryptorBuilder(PGPEncryptedData.CAST5)
+            .setSecureRandom(prng);
     PGPEncryptedDataGenerator cpk =
-        new PGPEncryptedDataGenerator(PGPEncryptedData.CAST5, true, prng, "BC");
-    cpk.addMethod(dest);
+        new PGPEncryptedDataGenerator(builder, true);
+    final BcPublicKeyKeyEncryptionMethodGenerator methodGenerator =
+        new BcPublicKeyKeyEncryptionMethodGenerator(dest);
+    cpk.addMethod(methodGenerator);
     return cpk;
   }
 
   private byte[] encrypt(final String name, final Date date,
-      final byte[] rawText) throws NoSuchProviderException, PGPException,
+      final byte[] rawText) throws PGPException,
       IOException {
     final byte[] zText = compress(name, date, rawText);
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/data/PatchSetAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/PatchSetAttribute.java
index 91df974..c669e26 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/data/PatchSetAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/PatchSetAttribute.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.data;
 
+import com.google.gerrit.server.change.ChangeKind;
+
 import java.util.List;
 
 public class PatchSetAttribute {
@@ -25,6 +27,7 @@
   public Long createdOn;
   public AccountAttribute author;
   public boolean isDraft;
+  public ChangeKind kind;
 
   public List<ApprovalAttribute> approvals;
   public List<PatchSetCommentAttribute> comments;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/documentation/QueryDocumentationExecutor.java b/gerrit-server/src/main/java/com/google/gerrit/server/documentation/QueryDocumentationExecutor.java
index e4fa926..b734007 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/documentation/QueryDocumentationExecutor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/documentation/QueryDocumentationExecutor.java
@@ -46,7 +46,7 @@
   private static final Logger log =
       LoggerFactory.getLogger(QueryDocumentationExecutor.class);
 
-  private static final Version LUCENE_VERSION = Version.LUCENE_46;
+  private static final Version LUCENE_VERSION = Version.LUCENE_48;
 
   private IndexSearcher searcher;
   private QueryParser parser;
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 9d48574..e72d465 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
@@ -35,6 +35,7 @@
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.change.ChangeKindCache;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.data.AccountAttribute;
 import com.google.gerrit.server.data.ApprovalAttribute;
@@ -84,6 +85,7 @@
   private final Provider<ReviewDb> db;
   private final ChangeData.Factory changeDataFactory;
   private final ApprovalsUtil approvalsUtil;
+  private final ChangeKindCache changeKindCache;
 
   @Inject
   EventFactory(AccountCache accountCache,
@@ -93,7 +95,8 @@
       @GerritPersonIdent PersonIdent myIdent,
       Provider<ReviewDb> db,
       ChangeData.Factory changeDataFactory,
-      ApprovalsUtil approvalsUtil) {
+      ApprovalsUtil approvalsUtil,
+      ChangeKindCache changeKindCache) {
     this.accountCache = accountCache;
     this.urlProvider = urlProvider;
     this.patchListCache = patchListCache;
@@ -103,6 +106,7 @@
     this.db = db;
     this.changeDataFactory = changeDataFactory;
     this.approvalsUtil = approvalsUtil;
+    this.changeKindCache = changeKindCache;
   }
 
   /**
@@ -190,7 +194,7 @@
    */
   public void addSubmitRecords(ChangeAttribute ca,
       List<SubmitRecord> submitRecords) {
-    ca.submitRecords = new ArrayList<SubmitRecordAttribute>();
+    ca.submitRecords = new ArrayList<>();
 
     for (SubmitRecord submitRecord : submitRecords) {
       SubmitRecordAttribute sa = new SubmitRecordAttribute();
@@ -209,7 +213,7 @@
   private void addSubmitRecordLabels(SubmitRecord submitRecord,
       SubmitRecordAttribute sa) {
     if (submitRecord.labels != null && !submitRecord.labels.isEmpty()) {
-      sa.labels = new ArrayList<SubmitLabelAttribute>();
+      sa.labels = new ArrayList<>();
       for (SubmitRecord.Label lbl : submitRecord.labels) {
         SubmitLabelAttribute la = new SubmitLabelAttribute();
         la.label = lbl.label;
@@ -224,8 +228,8 @@
   }
 
   public void addDependencies(ChangeAttribute ca, Change change) {
-    ca.dependsOn = new ArrayList<DependencyAttribute>();
-    ca.neededBy = new ArrayList<DependencyAttribute>();
+    ca.dependsOn = new ArrayList<>();
+    ca.neededBy = new ArrayList<>();
     try {
       final ReviewDb db = schema.open();
       try {
@@ -293,7 +297,7 @@
 
   public void addTrackingIds(ChangeAttribute a, Multimap<String, String> set) {
     if (!set.isEmpty()) {
-      a.trackingIds = new ArrayList<TrackingIdAttribute>(set.size());
+      a.trackingIds = new ArrayList<>(set.size());
       for (Map.Entry<String, Collection<String>> e : set.asMap().entrySet()) {
         for (String id : e.getValue()) {
           TrackingIdAttribute t = new TrackingIdAttribute();
@@ -324,7 +328,7 @@
       Map<PatchSet.Id, Collection<PatchSetApproval>> approvals,
       boolean includeFiles, Change change, LabelTypes labelTypes) {
     if (!ps.isEmpty()) {
-      ca.patchSets = new ArrayList<PatchSetAttribute>(ps.size());
+      ca.patchSets = new ArrayList<>(ps.size());
       for (PatchSet p : ps) {
         PatchSetAttribute psa = asPatchSetAttribute(p);
         if (approvals != null) {
@@ -344,8 +348,7 @@
       if (comment.getKey().getParentKey().getParentKey().get()
           == Integer.parseInt(patchSetAttribute.number)) {
         if (patchSetAttribute.comments == null) {
-          patchSetAttribute.comments =
-            new ArrayList<PatchSetCommentAttribute>();
+          patchSetAttribute.comments = new ArrayList<>();
         }
         patchSetAttribute.comments.add(asPatchSetLineAttribute(comment));
       }
@@ -358,7 +361,7 @@
       PatchList patchList = patchListCache.get(change, patchSet);
       for (PatchListEntry patch : patchList.getPatches()) {
         if (patchSetAttribute.files == null) {
-          patchSetAttribute.files = new ArrayList<PatchAttribute>();
+          patchSetAttribute.files = new ArrayList<>();
         }
 
         PatchAttribute p = new PatchAttribute();
@@ -376,7 +379,7 @@
   public void addComments(ChangeAttribute ca,
       Collection<ChangeMessage> messages) {
     if (!messages.isEmpty()) {
-      ca.comments = new ArrayList<MessageAttribute>();
+      ca.comments = new ArrayList<>();
       for (ChangeMessage message : messages) {
         ca.comments.add(asMessageAttribute(message));
       }
@@ -402,7 +405,7 @@
     try {
       final ReviewDb db = schema.open();
       try {
-        p.parents = new ArrayList<String>();
+        p.parents = new ArrayList<>();
         for (PatchSetAncestor a : db.patchSetAncestors().ancestorsOf(
             patchSet.getId())) {
           p.parents.add(a.getAncestorRevision().get());
@@ -427,6 +430,7 @@
             p.sizeInsertions += pe.getInsertions();
           }
         }
+        p.kind = changeKindCache.getChangeKind(db, change, patchSet);
       } finally {
         db.close();
       }
@@ -452,7 +456,7 @@
   public void addApprovals(PatchSetAttribute p,
       Collection<PatchSetApproval> list, LabelTypes labelTypes) {
     if (!list.isEmpty()) {
-      p.approvals = new ArrayList<ApprovalAttribute>(list.size());
+      p.approvals = new ArrayList<>(list.size());
       for (PatchSetApproval a : list) {
         if (a.getValue() != 0) {
           p.approvals.add(asApprovalAttribute(a, labelTypes));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/AccountsSection.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/AccountsSection.java
index 7d868bf..4ea24ef 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/AccountsSection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/AccountsSection.java
@@ -24,7 +24,7 @@
 
   public List<PermissionRule> getSameGroupVisibility() {
     if (sameGroupVisibility == null) {
-      sameGroupVisibility = new ArrayList<PermissionRule>();
+      sameGroupVisibility = new ArrayList<>();
     }
     return sameGroupVisibility;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/AsyncReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/AsyncReceiveCommits.java
index aaf0273..a9f161e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/AsyncReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/AsyncReceiveCommits.java
@@ -23,7 +23,6 @@
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.FactoryModuleBuilder;
 import com.google.inject.Inject;
-
 import com.google.inject.name.Named;
 import com.google.inject.PrivateModule;
 import com.google.inject.Provides;
@@ -164,7 +163,9 @@
           executor.submit(scopePropagator.wrap(new Worker(commands))),
           timeoutMillis, TimeUnit.MILLISECONDS);
     } catch (ExecutionException e) {
-      log.warn("Error in ReceiveCommits", e);
+      log.warn(String.format(
+          "Error in ReceiveCommits while processing changes for project %s",
+              rc.getProject().getName()), e);
       rc.addError("internal error while processing changes " + e.getMessage());
       // ReceiveCommits has tried its best to catch errors, so anything at this
       // point is very bad.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java
index 2276d20..05ee1db 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -45,18 +46,16 @@
 import java.util.List;
 import java.util.TimeZone;
 
+@Singleton
 public class BanCommit {
-  public interface Factory {
-    BanCommit create();
-  }
 
   /**
-  * Loads a list of commits to reject from {@code refs/meta/reject-commits}.
-  *
-  * @param repo repository from which the rejected commits should be loaded
-  * @return NoteMap of commits to be rejected, null if there are none.
-  * @throws IOException the map cannot be loaded.
-  */
+   * Loads a list of commits to reject from {@code refs/meta/reject-commits}.
+   *
+   * @param repo repository from which the rejected commits should be loaded
+   * @return NoteMap of commits to be rejected, null if there are none.
+   * @throws IOException the map cannot be loaded.
+   */
   public static NoteMap loadRejectCommitsMap(Repository repo)
       throws IOException {
     try {
@@ -96,8 +95,8 @@
 
   public BanCommitResult ban(final ProjectControl projectControl,
       final List<ObjectId> commitsToBan, final String reason)
-      throws PermissionDeniedException, IOException,
-      InterruptedException, MergeException, ConcurrentRefUpdateException {
+      throws PermissionDeniedException, IOException, InterruptedException,
+      MergeException, ConcurrentRefUpdateException {
     if (!projectControl.isOwner()) {
       throw new PermissionDeniedException(
           "No project owner: not permitted to ban commits");
@@ -124,8 +123,8 @@
           banCommitNotes.set(commitToBan, createNoteContent(reason, inserter));
         }
         inserter.flush();
-        NotesBranchUtil notesBranchUtil = notesBranchUtilFactory.create(project,
-            repo, inserter);
+        NotesBranchUtil notesBranchUtil =
+            notesBranchUtilFactory.create(project, repo, inserter);
         NoteMap newlyCreated =
             notesBranchUtil.commitNewNotes(banCommitNotes, REFS_REJECT_COMMITS,
                 createPersonIdent(), buildCommitMessage(commitsToBan, reason));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommitResult.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommitResult.java
index 1b48455..baae629 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommitResult.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommitResult.java
@@ -21,9 +21,9 @@
 
 public class BanCommitResult {
 
-  private final List<ObjectId> newlyBannedCommits = new LinkedList<ObjectId>();
-  private final List<ObjectId> alreadyBannedCommits = new LinkedList<ObjectId>();
-  private final List<ObjectId> ignoredObjectIds = new LinkedList<ObjectId>();
+  private final List<ObjectId> newlyBannedCommits = new LinkedList<>();
+  private final List<ObjectId> alreadyBannedCommits = new LinkedList<>();
+  private final List<ObjectId> ignoredObjectIds = new LinkedList<>();
 
   public BanCommitResult() {
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/BranchOrderSection.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BranchOrderSection.java
new file mode 100644
index 0000000..c447d31
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BranchOrderSection.java
@@ -0,0 +1,60 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import com.google.common.collect.ImmutableList;
+
+import org.eclipse.jgit.lib.Constants;
+
+import java.util.List;
+
+public class BranchOrderSection {
+
+  /**
+   * Branch names ordered from least to the most stable.
+   *
+   * Typically the order will be like: master, stable-M.N, stable-M.N-1, ...
+   */
+  private final ImmutableList<String> order;
+
+  public BranchOrderSection(String[] order) {
+    if (order.length == 0) {
+      this.order = ImmutableList.of();
+    } else {
+      ImmutableList.Builder<String> builder = ImmutableList.builder();
+      for (String b : order) {
+        builder.add(fullName(b));
+      }
+      this.order = builder.build();
+    }
+  }
+
+  private static String fullName(String branch) {
+    if (branch.startsWith(Constants.R_HEADS)) {
+      return branch;
+    } else {
+      return Constants.R_HEADS + branch;
+    }
+  }
+
+  public List<String> getMoreStable(String branch) {
+    int i = order.indexOf(fullName(branch));
+    if (0 <= i) {
+      return order.subList(i + 1, order.size());
+    } else {
+      return ImmutableList.of();
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMergeQueue.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMergeQueue.java
index bac7542..e386bc5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMergeQueue.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMergeQueue.java
@@ -55,10 +55,8 @@
   private static final Logger log =
       LoggerFactory.getLogger(ChangeMergeQueue.class);
 
-  private final Map<Branch.NameKey, MergeEntry> active =
-      new HashMap<Branch.NameKey, MergeEntry>();
-  private final Map<Branch.NameKey, RecheckJob> recheck =
-      new HashMap<Branch.NameKey, RecheckJob>();
+  private final Map<Branch.NameKey, MergeEntry> active = new HashMap<>();
+  private final Map<Branch.NameKey, RecheckJob> recheck = new HashMap<>();
 
   private final WorkQueue workQueue;
   private final Provider<MergeOp.Factory> bgFactory;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java
index 4c3e5f4..f421dcb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java
@@ -16,7 +16,7 @@
 
 public enum CommitMergeStatus {
   /** */
-  CLEAN_MERGE("Change has been successfully merged into the git repository."),
+  CLEAN_MERGE("Change has been successfully merged into the git repository"),
 
   /** */
   CLEAN_PICK("Change has been successfully cherry-picked"),
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ConfiguredMimeTypes.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ConfiguredMimeTypes.java
new file mode 100644
index 0000000..082e1a2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ConfiguredMimeTypes.java
@@ -0,0 +1,117 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import org.eclipse.jgit.errors.InvalidPatternException;
+import org.eclipse.jgit.fnmatch.FileNameMatcher;
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+public class ConfiguredMimeTypes {
+  private static final Logger log = LoggerFactory
+      .getLogger(ConfiguredMimeTypes.class);
+
+  private static final String MIMETYPE = "mimetype";
+  private static final String KEY_PATH = "path";
+
+  private final List<TypeMatcher> matchers;
+
+  ConfiguredMimeTypes(String projectName, Config rc) {
+    Set<String> types = rc.getSubsections(MIMETYPE);
+    if (types.isEmpty()) {
+      matchers = Collections.emptyList();
+    } else {
+      matchers = new ArrayList<>();
+      for (String typeName : types) {
+        for (String path : rc.getStringList(MIMETYPE, typeName, KEY_PATH)) {
+          try {
+            add(typeName, path);
+          } catch (PatternSyntaxException | InvalidPatternException e) {
+            log.warn(String.format(
+                "Ignoring invalid %s.%s.%s = %s in project %s: %s",
+                MIMETYPE, typeName, KEY_PATH,
+                path, projectName, e.getMessage()));
+          }
+        }
+      }
+    }
+  }
+
+  private void add(String typeName, String path)
+      throws PatternSyntaxException, InvalidPatternException {
+    if (path.startsWith("^")) {
+      matchers.add(new ReType(typeName, path));
+    } else {
+      matchers.add(new FnType(typeName, path));
+    }
+  }
+
+  public String getMimeType(String path) {
+    for (TypeMatcher m : matchers) {
+      if (m.matches(path)) {
+        return m.type;
+      }
+    }
+    return null;
+  }
+
+  private abstract static class TypeMatcher {
+    final String type;
+
+    TypeMatcher(String type) {
+      this.type = type;
+    }
+
+    abstract boolean matches(String path);
+  }
+
+  private static class FnType extends TypeMatcher {
+    private final FileNameMatcher matcher;
+
+    FnType(String type, String pattern) throws InvalidPatternException {
+      super(type);
+      this.matcher = new FileNameMatcher(pattern, null);
+    }
+
+    @Override
+    boolean matches(String input) {
+      FileNameMatcher m = new FileNameMatcher(matcher);
+      m.append(input);
+      return m.isMatch();
+    }
+  }
+
+  private static class ReType extends TypeMatcher {
+    private final Pattern re;
+
+    ReType(String type, String pattern) throws PatternSyntaxException {
+      super(type);
+      this.re = Pattern.compile(pattern);
+    }
+
+    @Override
+    boolean matches(String input) {
+      return re.matcher(input).matches();
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollectionRunner.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollectionRunner.java
new file mode 100644
index 0000000..5e3ca31
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollectionRunner.java
@@ -0,0 +1,103 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import static com.google.gerrit.server.config.ScheduleConfig.MISSING_CONFIG;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.config.GcConfig;
+import com.google.gerrit.server.config.ScheduleConfig;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.concurrent.TimeUnit;
+
+/** Runnable to enable scheduling gc to run periodically */
+public class GarbageCollectionRunner implements Runnable {
+  private static final Logger gcLog = LoggerFactory
+      .getLogger(GarbageCollection.LOG_NAME);
+
+  public static Module module() {
+    return new LifecycleModule() {
+
+      @Override
+      protected void configure() {
+        listener().to(Lifecycle.class);
+      }
+    };
+  }
+
+  static class Lifecycle implements LifecycleListener {
+    private final WorkQueue queue;
+    private final GarbageCollectionRunner gcRunner;
+    private final GcConfig gcConfig;
+
+    @Inject
+    Lifecycle(WorkQueue queue, GarbageCollectionRunner gcRunner,
+        GcConfig config) {
+      this.queue = queue;
+      this.gcRunner = gcRunner;
+      this.gcConfig = config;
+    }
+
+    @Override
+    public void start() {
+      ScheduleConfig scheduleConfig = gcConfig.getScheduleConfig();
+      long interval = scheduleConfig.getInterval();
+      long delay = scheduleConfig.getInitialDelay();
+      if (delay == MISSING_CONFIG && interval == MISSING_CONFIG) {
+        gcLog.info("Ignoring missing gc schedule configuration");
+      } else if (delay < 0 || interval <= 0) {
+        gcLog.warn("Ignoring invalid gc schedule configuration");
+      } else {
+        queue.getDefaultQueue().scheduleAtFixedRate(gcRunner, delay,
+            interval, TimeUnit.MILLISECONDS);
+      }
+    }
+
+    @Override
+    public void stop() {
+      // handled by WorkQueue.stop() already
+    }
+  }
+
+  private final GarbageCollection.Factory garbageCollectionFactory;
+  private final ProjectCache projectCache;
+
+  @Inject
+  GarbageCollectionRunner(GarbageCollection.Factory garbageCollectionFactory,
+      ProjectCache projectCache) {
+    this.garbageCollectionFactory = garbageCollectionFactory;
+    this.projectCache = projectCache;
+  }
+
+  @Override
+  public void run() {
+    gcLog.info("Triggering gc on all repositories");
+    garbageCollectionFactory.create().run(
+        Lists.newArrayList(projectCache.all()));
+  }
+
+  @Override
+  public String toString() {
+    return "GC runner";
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/LabelNormalizer.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/LabelNormalizer.java
index 19b5ec9..12efd56 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/LabelNormalizer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/LabelNormalizer.java
@@ -33,6 +33,7 @@
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
 import java.util.Collection;
 import java.util.List;
@@ -46,6 +47,7 @@
  * is originally made and a later point, for example when a change is submitted.
  * This class normalizes old votes against current project configuration.
  */
+@Singleton
 public class LabelNormalizer {
   public static class Result {
     private final ImmutableList<PatchSetApproval> unchanged;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
index 43e0156..196d3e9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
@@ -234,7 +234,7 @@
   private void onCreateProject(final Project.NameKey newProjectName) {
     namesUpdateLock.lock();
     try {
-      SortedSet<Project.NameKey> n = new TreeSet<Project.NameKey>(names);
+      SortedSet<Project.NameKey> n = new TreeSet<>(names);
       n.add(newProjectName);
       names = Collections.unmodifiableSortedSet(n);
     } finally {
@@ -345,7 +345,7 @@
     // scanning the filesystem. Don't rely on the cached names collection.
     namesUpdateLock.lock();
     try {
-      SortedSet<Project.NameKey> n = new TreeSet<Project.NameKey>();
+      SortedSet<Project.NameKey> n = new TreeSet<>();
       scanProjects(basePath, "", n);
       names = Collections.unmodifiableSortedSet(n);
       return n;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
index da72009..7855b63 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.Capable;
 import com.google.gerrit.common.data.SubmitTypeRecord;
+import com.google.gerrit.extensions.common.SubmitType;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
@@ -37,11 +38,11 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Project.SubmitType;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
@@ -54,6 +55,7 @@
 import com.google.gerrit.server.mail.MergeFailSender;
 import com.google.gerrit.server.mail.MergedSender;
 import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.ChangeControl;
@@ -88,7 +90,6 @@
 
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -137,9 +138,11 @@
   private final PatchSetInfoFactory patchSetInfoFactory;
   private final IdentifiedUser.GenericFactory identifiedUserFactory;
   private final ChangeControl.GenericFactory changeControlFactory;
+  private final ChangeUpdate.Factory updateFactory;
   private final MergeQueue mergeQueue;
   private final MergeValidators.Factory mergeValidatorsFactory;
   private final ApprovalsUtil approvalsUtil;
+  private final ChangeMessagesUtil cmUtil;
 
   private final Branch.NameKey destBranch;
   private ProjectState destProject;
@@ -173,6 +176,7 @@
       final MergeFailSender.Factory mfsf,
       final PatchSetInfoFactory psif, final IdentifiedUser.GenericFactory iuf,
       final ChangeControl.GenericFactory changeControlFactory,
+      final ChangeUpdate.Factory updateFactory,
       final MergeQueue mergeQueue, @Assisted final Branch.NameKey branch,
       final ChangeHooks hooks, final AccountCache accountCache,
       final TagCache tagCache,
@@ -182,7 +186,8 @@
       final RequestScopePropagator requestScopePropagator,
       final ChangeIndexer indexer,
       final MergeValidators.Factory mergeValidatorsFactory,
-      final ApprovalsUtil approvalsUtil) {
+      final ApprovalsUtil approvalsUtil,
+      final ChangeMessagesUtil cmUtil) {
     repoManager = grm;
     schemaFactory = sf;
     notesFactory = nf;
@@ -193,6 +198,7 @@
     patchSetInfoFactory = psif;
     identifiedUserFactory = iuf;
     this.changeControlFactory = changeControlFactory;
+    this.updateFactory = updateFactory;
     this.mergeQueue = mergeQueue;
     this.hooks = hooks;
     this.accountCache = accountCache;
@@ -204,10 +210,11 @@
     this.indexer = indexer;
     this.mergeValidatorsFactory = mergeValidatorsFactory;
     this.approvalsUtil = approvalsUtil;
+    this.cmUtil = cmUtil;
     destBranch = branch;
     toMerge = ArrayListMultimap.create();
-    potentiallyStillSubmittable = new ArrayList<CodeReviewCommit>();
-    commits = new HashMap<Change.Id, CodeReviewCommit>();
+    potentiallyStillSubmittable = new ArrayList<>();
+    commits = new HashMap<>();
     toUpdate = Lists.newArrayList();
   }
 
@@ -224,7 +231,7 @@
     }
   }
 
-  public void merge() throws MergeException {
+  public void merge() throws MergeException, NoSuchChangeException, IOException {
     setDestProject();
     try {
       openSchema();
@@ -238,11 +245,11 @@
       final ListMultimap<SubmitType, CodeReviewCommit> toMergeNextTurn =
           ArrayListMultimap.create();
       final List<CodeReviewCommit> potentiallyStillSubmittableOnNextRun =
-          new ArrayList<CodeReviewCommit>();
+          new ArrayList<>();
       while (!toMerge.isEmpty()) {
         toMergeNextTurn.clear();
         final Set<SubmitType> submitTypes =
-            new HashSet<Project.SubmitType>(toMerge.keySet());
+            new HashSet<>(toMerge.keySet());
         for (final SubmitType submitType : submitTypes) {
           if (reopen) {
             branchUpdate = openBranch();
@@ -395,7 +402,7 @@
     inserter = repo.newObjectInserter();
   }
 
-  private RefUpdate openBranch() throws MergeException, OrmException {
+  private RefUpdate openBranch() throws MergeException, OrmException, NoSuchChangeException {
     try {
       final RefUpdate branchUpdate = repo.updateRef(destBranch.get());
       if (branchUpdate.getOldObjectId() != null) {
@@ -418,7 +425,7 @@
 
   private Set<RevCommit> getAlreadyAccepted(final CodeReviewCommit branchTip)
       throws MergeException {
-    final Set<RevCommit> alreadyAccepted = new HashSet<RevCommit>();
+    final Set<RevCommit> alreadyAccepted = new HashSet<>();
 
     if (branchTip != null) {
       alreadyAccepted.add(branchTip);
@@ -442,7 +449,7 @@
   }
 
   private ListMultimap<SubmitType, Change> validateChangeList(
-      final List<Change> submitted) throws MergeException {
+      final List<Change> submitted) throws MergeException, NoSuchChangeException {
     final ListMultimap<SubmitType, Change> toSubmit =
         ArrayListMultimap.create();
 
@@ -453,7 +460,7 @@
       throw new MergeException(e.getMessage(), e);
     }
 
-    final Set<ObjectId> tips = new HashSet<ObjectId>();
+    final Set<ObjectId> tips = new HashSet<>();
     for (final Ref r : allRefs.values()) {
       tips.add(r.getObjectId());
     }
@@ -556,7 +563,7 @@
         }
       }
 
-      final SubmitType submitType = getSubmitType(commit.getControl(), ps);
+      SubmitType submitType = getSubmitType(commit.getControl(), ps);
       if (submitType == null) {
         commit.setStatusCode(CommitMergeStatus.NO_SUBMIT_TYPE);
         toUpdate.add(chg);
@@ -656,7 +663,15 @@
     return account;
   }
 
-  private void updateChangeStatus(final List<Change> submitted) {
+  private String getByAccountName(CodeReviewCommit codeReviewCommit) {
+    Account account = getAccount(codeReviewCommit);
+    if (account != null && account.getFullName() != null) {
+      return " by " + account.getFullName();
+    }
+    return "";
+  }
+
+  private void updateChangeStatus(final List<Change> submitted) throws NoSuchChangeException {
     for (final Change c : submitted) {
       final CodeReviewCommit commit = commits.get(c.getId());
       final CommitMergeStatus s = commit != null ? commit.getStatusCode() : null;
@@ -672,12 +687,13 @@
       try {
         switch (s) {
           case CLEAN_MERGE:
-            setMerged(c, message(c, txt));
+            setMerged(c, message(c, txt + getByAccountName(commit)));
             break;
 
           case CLEAN_REBASE:
           case CLEAN_PICK:
-            setMerged(c, message(c, txt + " as " + commit.name()));
+            setMerged(c, message(c, txt + " as " + commit.name()
+                + getByAccountName(commit)));
             break;
 
           case ALREADY_MERGED:
@@ -823,7 +839,8 @@
   }
 
   private void setMerged(Change c, ChangeMessage msg)
-      throws OrmException, IOException {
+      throws OrmException, IOException, NoSuchChangeException {
+    ChangeUpdate update = null;
     try {
       db.changes().beginTransaction(c.getId());
 
@@ -834,8 +851,15 @@
       c = setMergedPatchSet(c.getId(), merged);
       PatchSetApproval submitter =
           approvalsUtil.getSubmitter(db, commit.notes(), merged);
-      addMergedMessage(submitter, msg);
+      ChangeControl control = commit.getControl();
+      update = updateFactory.create(control, c.getLastUpdatedOn());
 
+      // TODO(yyonas): we need to be able to change the author of the message
+      // is not the person for whom the change was made. addMergedMessage
+      // did this in the past.
+      if (msg != null) {
+        cmUtil.addChangeMessage(db, update, msg);
+      }
       db.commit();
 
       sendMergedEmail(c, submitter);
@@ -852,6 +876,8 @@
     } finally {
       db.rollback();
     }
+    indexer.index(db, c);
+    update.commit();
   }
 
   private Change setMergedPatchSet(Change.Id changeId, final PatchSet.Id merged)
@@ -880,16 +906,6 @@
     });
   }
 
-  private void addMergedMessage(PatchSetApproval submitter, ChangeMessage msg)
-      throws OrmException {
-    if (msg != null) {
-      if (submitter != null && msg.getAuthor() == null) {
-        msg.setAuthor(submitter.getAccountId());
-      }
-      db.changeMessages().insert(Collections.singleton(msg));
-    }
-  }
-
   private void sendMergedEmail(final Change c, final PatchSetApproval from) {
     workQueue.getDefaultQueue()
         .submit(requestScopePropagator.wrap(new Runnable() {
@@ -932,11 +948,11 @@
         c, identifiedUserFactory.create(c.getOwner()));
   }
 
-  private void setNew(CodeReviewCommit c, ChangeMessage msg) {
+  private void setNew(CodeReviewCommit c, ChangeMessage msg) throws NoSuchChangeException, IOException {
     sendMergeFail(c.notes(), msg, true);
   }
 
-  private void setNew(Change c, ChangeMessage msg) throws OrmException {
+  private void setNew(Change c, ChangeMessage msg) throws OrmException, NoSuchChangeException, IOException {
     sendMergeFail(notesFactory.create(c), msg, true);
   }
 
@@ -946,7 +962,8 @@
 
   private RetryStatus getRetryStatus(
       @Nullable PatchSetApproval submitter,
-      ChangeMessage msg) {
+      ChangeMessage msg,
+      ChangeNotes notes) {
     if (submitter != null
         && TimeUtil.nowMs() - submitter.getGranted().getTime()
           > MAX_SUBMIT_WINDOW) {
@@ -954,8 +971,7 @@
     }
 
     try {
-      ChangeMessage last = Iterables.getLast(db.changeMessages().byChange(
-          msg.getPatchSetId().getParentKey()), null);
+      ChangeMessage last = Iterables.getLast(cmUtil.byChange(db, notes));
       if (last != null) {
         if (Objects.equal(last.getAuthor(), msg.getAuthor())
             && Objects.equal(last.getMessage(), msg.getMessage())) {
@@ -974,7 +990,7 @@
   }
 
   private void sendMergeFail(ChangeNotes notes, final ChangeMessage msg,
-      boolean makeNew) {
+      boolean makeNew) throws NoSuchChangeException, IOException {
     PatchSetApproval submitter = null;
     try {
       submitter = approvalsUtil.getSubmitter(
@@ -984,7 +1000,7 @@
     }
 
     if (!makeNew) {
-      RetryStatus retryStatus = getRetryStatus(submitter, msg);
+      RetryStatus retryStatus = getRetryStatus(submitter, msg, notes);
       if (retryStatus == RetryStatus.RETRY_NO_MESSAGE) {
         return;
       } else if (retryStatus == RetryStatus.UNSUBMIT) {
@@ -995,6 +1011,7 @@
     final boolean setStatusNew = makeNew;
     final Change c = notes.getChange();
     Change change = null;
+    ChangeUpdate update = null;
     try {
       db.changes().beginTransaction(c.getId());
       try {
@@ -1012,7 +1029,13 @@
             return c;
           }
         });
-        db.changeMessages().insert(Collections.singleton(msg));
+        ChangeControl control = changeControl(change);
+
+        //TODO(yyonas): atomic change is not propagated.
+        update = updateFactory.create(control, c.getLastUpdatedOn());
+        if (msg != null) {
+          cmUtil.addChangeMessage(db, update, msg);
+        }
         db.commit();
       } finally {
         db.rollback();
@@ -1020,6 +1043,9 @@
     } catch (OrmException err) {
       log.warn("Cannot record merge failure message", err);
     }
+    if (update != null) {
+      update.commit();
+    }
 
     CheckedFuture<?, IOException> indexFuture;
     if (change != null) {
@@ -1083,7 +1109,7 @@
     }
   }
 
-  private void abandonAllOpenChanges() {
+  private void abandonAllOpenChanges() throws NoSuchChangeException {
     Exception err = null;
     try {
       openSchema();
@@ -1105,8 +1131,13 @@
   }
 
   private void abandonOneChange(Change change) throws OrmException,
-      IOException {
+    NoSuchChangeException,  IOException {
     db.changes().beginTransaction(change.getId());
+
+    //TODO(dborowitz): support InternalUser in ChangeUpdate
+    ChangeControl control = changeControlFactory.controlFor(change,
+        identifiedUserFactory.create(change.getOwner()));
+    ChangeUpdate update = updateFactory.create(control);
     try {
       change = db.changes().atomicUpdate(
         change.getId(),
@@ -1120,6 +1151,7 @@
             return null;
           }
         });
+
       if (change != null) {
         ChangeMessage msg = new ChangeMessage(
             new ChangeMessage.Key(
@@ -1129,12 +1161,15 @@
             change.getLastUpdatedOn(),
             change.currentPatchSetId());
         msg.setMessage("Project was deleted.");
-        db.changeMessages().insert(Collections.singleton(msg));
+
+        //TODO(yyonas): atomic change is not propagated.
+        cmUtil.addChangeMessage(db, update, msg);
         db.commit();
         indexer.index(db, change);
       }
     } finally {
       db.rollback();
     }
+    update.commit();
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSorter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSorter.java
index 9b4582d..4985390 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSorter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSorter.java
@@ -40,8 +40,8 @@
 
   Collection<CodeReviewCommit> sort(final Collection<CodeReviewCommit> incoming)
       throws IOException {
-    final Set<CodeReviewCommit> heads = new HashSet<CodeReviewCommit>();
-    final Set<CodeReviewCommit> sort = new HashSet<CodeReviewCommit>(incoming);
+    final Set<CodeReviewCommit> heads = new HashSet<>();
+    final Set<CodeReviewCommit> sort = new HashSet<>(incoming);
     while (!sort.isEmpty()) {
       final CodeReviewCommit n = removeOne(sort);
 
@@ -52,7 +52,7 @@
       }
 
       RevCommit c;
-      final RevCommitList<RevCommit> contents = new RevCommitList<RevCommit>();
+      final RevCommitList<RevCommit> contents = new RevCommitList<>();
       while ((c = rw.next()) != null) {
         if (!c.has(canMergeFlag) || !incoming.contains(c)) {
           // We cannot merge n as it would bring something we
@@ -60,7 +60,7 @@
           //
           if (n.missing == null) {
             n.setStatusCode(CommitMergeStatus.MISSING_DEPENDENCY);
-            n.missing = new ArrayList<CodeReviewCommit>();
+            n.missing = new ArrayList<>();
           }
           n.missing.add((CodeReviewCommit) c);
         } else {
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 a005742..403901a 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
@@ -38,6 +38,7 @@
 import com.google.inject.assistedinject.AssistedInject;
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.LargeObjectException;
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.errors.NoMergeBaseException;
 import org.eclipse.jgit.errors.NoMergeBaseException.MergeBaseFailureReason;
@@ -51,6 +52,7 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.merge.MergeStrategy;
 import org.eclipse.jgit.merge.Merger;
+import org.eclipse.jgit.merge.ThreeWayMergeStrategy;
 import org.eclipse.jgit.merge.ThreeWayMerger;
 import org.eclipse.jgit.revwalk.FooterKey;
 import org.eclipse.jgit.revwalk.FooterLine;
@@ -78,10 +80,20 @@
 import java.util.TimeZone;
 
 public class MergeUtil {
+  public static final FooterKey CHANGE_ID = new FooterKey("Change-Id");
+  private static final FooterKey REVIEWED_ON = new FooterKey("Reviewed-on");
   private static final Logger log = LoggerFactory.getLogger(MergeUtil.class);
+  private static final String R_HEADS_MASTER =
+      Constants.R_HEADS + Constants.MASTER;
 
   public static boolean useRecursiveMerge(Config cfg) {
-    return cfg.getBoolean("core", null, "useRecursiveMerge", false);
+    return cfg.getBoolean("core", null, "useRecursiveMerge", true);
+  }
+
+  public static ThreeWayMergeStrategy getMergeStrategy(Config cfg) {
+    return useRecursiveMerge(cfg)
+        ? MergeStrategy.RECURSIVE
+        : MergeStrategy.RESOLVE;
   }
 
   public static interface Factory {
@@ -89,12 +101,6 @@
     MergeUtil create(ProjectState project, boolean useContentMerge);
   }
 
-  private static final String R_HEADS_MASTER =
-      Constants.R_HEADS + Constants.MASTER;
-
-  private static final FooterKey REVIEWED_ON = new FooterKey("Reviewed-on");
-  private static final FooterKey CHANGE_ID = new FooterKey("Change-Id");
-
   private final Provider<ReviewDb> db;
   private final IdentifiedUser.GenericFactory identifiedUserFactory;
   private final Provider<String> urlProvider;
@@ -311,9 +317,9 @@
     return "Verified".equalsIgnoreCase(id.get());
   }
 
-  private List<PatchSetApproval> safeGetApprovals(CodeReviewCommit n) {
+  private Iterable<PatchSetApproval> safeGetApprovals(CodeReviewCommit n) {
     try {
-      return approvalsUtil.byPatchSet(db.get(), n.notes(), n.getPatchsetId());
+      return approvalsUtil.byPatchSet(db.get(), n.getControl(), n.getPatchsetId());
     } catch (OrmException e) {
       log.error("Can't read approval records for " + n.getPatchsetId(), e);
       return Collections.emptyList();
@@ -360,7 +366,7 @@
     if (submitter != null) {
       IdentifiedUser who =
           identifiedUserFactory.create(submitter.getAccountId());
-      Set<String> emails = new HashSet<String>();
+      Set<String> emails = new HashSet<>();
       for (RevCommit c : codeReviewCommits) {
         try {
           rw.parseBody(c);
@@ -394,9 +400,12 @@
       return false;
     }
 
-    final ThreeWayMerger m = newThreeWayMerger(repo, createDryRunInserter(repo));
+    ThreeWayMerger m = newThreeWayMerger(repo, createDryRunInserter(repo));
     try {
       return m.merge(new AnyObjectId[] {mergeTip, toMerge});
+    } catch (LargeObjectException e) {
+      log.warn("Cannot merge due to LargeObjectException: " + toMerge.name());
+      return false;
     } catch (NoMergeBaseException e) {
       return false;
     } catch (IOException e) {
@@ -440,8 +449,7 @@
       // that on the current merge tip.
       //
       try {
-        final ThreeWayMerger m =
-            newThreeWayMerger(repo, createDryRunInserter(repo));
+        ThreeWayMerger m = newThreeWayMerger(repo, createDryRunInserter(repo));
         m.setBase(toMerge.getParent(0));
         return m.merge(mergeTip, toMerge);
       } catch (IOException e) {
@@ -544,7 +552,7 @@
       final CodeReviewCommit mergeTip, final ObjectId treeId,
       final CodeReviewCommit n) throws IOException,
       MissingObjectException, IncorrectObjectTypeException {
-    final List<CodeReviewCommit> merged = new ArrayList<CodeReviewCommit>();
+    final List<CodeReviewCommit> merged = new ArrayList<>();
     rw.resetRetain(canMergeFlag);
     rw.markStart(n);
     rw.markUninteresting(mergeTip);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java
index e8b4b6a..b48028e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java
@@ -137,7 +137,7 @@
     return projectName;
   }
 
-  Repository getRepository() {
+  public Repository getRepository() {
     return db;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java
index 45ecb63..4279b31 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java
@@ -132,7 +132,7 @@
 
   private final OutputStream out;
   private final String taskName;
-  private final List<Task> tasks = new CopyOnWriteArrayList<Task>();
+  private final List<Task> tasks = new CopyOnWriteArrayList<>();
   private int spinnerIndex;
   private char spinnerState = NO_SPINNER;
   private boolean done;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/PerThreadRequestScope.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/PerThreadRequestScope.java
index e7bb0ae..6087432 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/PerThreadRequestScope.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/PerThreadRequestScope.java
@@ -81,7 +81,7 @@
     }
   }
 
-  private static final ThreadLocal<Context> current = new ThreadLocal<Context>();
+  private static final ThreadLocal<Context> current = new ThreadLocal<>();
 
   private static Context requireContext() {
     final Context ctx = current.get();
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 55f4ff7..3513942 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
@@ -39,12 +39,13 @@
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.data.PermissionRule.Action;
 import com.google.gerrit.common.data.RefConfigSection;
+import com.google.gerrit.extensions.api.projects.ProjectState;
+import com.google.gerrit.extensions.common.InheritableBoolean;
+import com.google.gerrit.extensions.common.SubmitType;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Project.State;
-import com.google.gerrit.reviewdb.client.Project.SubmitType;
+import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.PluginConfig;
@@ -94,6 +95,9 @@
   private static final String ACCOUNTS = "accounts";
   private static final String KEY_SAME_GROUP_VISIBILITY = "sameGroupVisibility";
 
+  private static final String BRANCH_ORDER = "branchOrder";
+  private static final String BRANCH = "branch";
+
   private static final String CONTRIBUTOR_AGREEMENT = "contributor-agreement";
   private static final String KEY_ACCEPTED = "accepted";
   private static final String KEY_REQUIRE_CONTACT_INFORMATION = "requireContactInformation";
@@ -126,8 +130,8 @@
   private static final String KEY_LOCAL_DEFAULT = "local-default";
 
   private static final String LABEL = "label";
-  private static final String KEY_ABBREVIATION = "abbreviation";
   private static final String KEY_FUNCTION = "function";
+  private static final String KEY_DEFAULT_VALUE = "defaultValue";
   private static final String KEY_COPY_MIN_SCORE = "copyMinScore";
   private static final String KEY_COPY_MAX_SCORE = "copyMaxScore";
   private static final String KEY_COPY_ALL_SCORES_ON_TRIVIAL_REBASE = "copyAllScoresOnTrivialRebase";
@@ -142,17 +146,19 @@
 
   private static final SubmitType defaultSubmitAction =
       SubmitType.MERGE_IF_NECESSARY;
-  private static final State defaultStateValue =
-      State.ACTIVE;
+  private static final ProjectState defaultStateValue =
+      ProjectState.ACTIVE;
 
   private Project.NameKey projectName;
   private Project project;
   private AccountsSection accountsSection;
   private Map<AccountGroup.UUID, GroupReference> groupsByUUID;
   private Map<String, AccessSection> accessSections;
+  private BranchOrderSection branchOrderSection;
   private Map<String, ContributorAgreement> contributorAgreements;
   private Map<String, NotifyConfig> notifySections;
   private Map<String, LabelType> labelSections;
+  private ConfiguredMimeTypes mimeTypes;
   private List<CommentLinkInfo> commentLinkSections;
   private List<ValidationError> validationErrors;
   private ObjectId rulesId;
@@ -239,6 +245,10 @@
     return sort(accessSections.values());
   }
 
+  public BranchOrderSection getBranchOrderSection() {
+    return branchOrderSection;
+  }
+
   public void remove(AccessSection section) {
     if (section != null) {
       accessSections.remove(section.getName());
@@ -299,6 +309,10 @@
     return commentLinkSections;
   }
 
+  public ConfiguredMimeTypes getMimeTypes() {
+    return mimeTypes;
+  }
+
   public GroupReference resolve(AccountGroup group) {
     return resolve(GroupReference.forGroup(group));
   }
@@ -398,13 +412,13 @@
     }
     p.setParentName(rc.getString(ACCESS, null, KEY_INHERIT_FROM));
 
-    p.setUseContributorAgreements(getEnum(rc, RECEIVE, null, KEY_REQUIRE_CONTRIBUTOR_AGREEMENT, Project.InheritableBoolean.INHERIT));
-    p.setUseSignedOffBy(getEnum(rc, RECEIVE, null, KEY_REQUIRE_SIGNED_OFF_BY, Project.InheritableBoolean.INHERIT));
-    p.setRequireChangeID(getEnum(rc, RECEIVE, null, KEY_REQUIRE_CHANGE_ID, Project.InheritableBoolean.INHERIT));
+    p.setUseContributorAgreements(getEnum(rc, RECEIVE, null, KEY_REQUIRE_CONTRIBUTOR_AGREEMENT, InheritableBoolean.INHERIT));
+    p.setUseSignedOffBy(getEnum(rc, RECEIVE, null, KEY_REQUIRE_SIGNED_OFF_BY, InheritableBoolean.INHERIT));
+    p.setRequireChangeID(getEnum(rc, RECEIVE, null, KEY_REQUIRE_CHANGE_ID, InheritableBoolean.INHERIT));
     p.setMaxObjectSizeLimit(rc.getString(RECEIVE, null, KEY_MAX_OBJECT_SIZE_LIMIT));
 
     p.setSubmitType(getEnum(rc, SUBMIT, null, KEY_ACTION, defaultSubmitAction));
-    p.setUseContentMerge(getEnum(rc, SUBMIT, null, KEY_MERGE_CONTENT, Project.InheritableBoolean.INHERIT));
+    p.setUseContentMerge(getEnum(rc, SUBMIT, null, KEY_MERGE_CONTENT, InheritableBoolean.INHERIT));
     p.setState(getEnum(rc, PROJECT, null, KEY_STATE, defaultStateValue));
 
     p.setDefaultDashboard(rc.getString(DASHBOARD, null, KEY_DEFAULT));
@@ -413,9 +427,11 @@
     loadAccountsSection(rc, groupsByName);
     loadContributorAgreements(rc, groupsByName);
     loadAccessSections(rc, groupsByName);
+    loadBranchOrderSection(rc);
     loadNotifySections(rc, groupsByName);
     loadLabelSections(rc);
     loadCommentLinkSections(rc);
+    mimeTypes = new ConfiguredMimeTypes(projectName.get(), rc);
     loadPluginSections(rc);
     loadReceiveSection(rc);
   }
@@ -429,7 +445,7 @@
 
   private void loadContributorAgreements(
       Config rc, Map<String, GroupReference> groupsByName) {
-    contributorAgreements = new HashMap<String, ContributorAgreement>();
+    contributorAgreements = new HashMap<>();
     for (String name : rc.getSubsections(CONTRIBUTOR_AGREEMENT)) {
       ContributorAgreement ca = getContributorAgreement(name, true);
       ca.setDescription(rc.getString(CONTRIBUTOR_AGREEMENT, name, KEY_DESCRIPTION));
@@ -527,7 +543,7 @@
 
   private void loadAccessSections(
       Config rc, Map<String, GroupReference> groupsByName) {
-    accessSections = new HashMap<String, AccessSection>();
+    accessSections = new HashMap<>();
     for (String refName : rc.getSubsections(ACCESS)) {
       if (RefConfigSection.isValid(refName)) {
         AccessSection as = getAccessSection(refName, true);
@@ -562,6 +578,13 @@
     }
   }
 
+  private void loadBranchOrderSection(Config rc) {
+    if (rc.getSections().contains(BRANCH_ORDER)) {
+      branchOrderSection = new BranchOrderSection(
+          rc.getStringList(BRANCH_ORDER, null, BRANCH));
+    }
+  }
+
   private List<PermissionRule> loadPermissionRules(Config rc, String section,
       String subsection, String varName,
       Map<String, GroupReference> groupsByName,
@@ -649,10 +672,6 @@
             "Invalid label \"%s\"", name)));
         continue;
       }
-      String abbr = rc.getString(LABEL, name, KEY_ABBREVIATION);
-      if (abbr != null) {
-        label.setAbbreviation(abbr);
-      }
 
       String functionName = Objects.firstNonNull(
           rc.getString(LABEL, name, KEY_FUNCTION), "MaxWithBlock");
@@ -664,6 +683,17 @@
             KEY_FUNCTION, name, Joiner.on(", ").join(LABEL_FUNCTIONS))));
         label.setFunctionName(null);
       }
+
+      if (!values.isEmpty()) {
+        short dv = (short) rc.getInt(LABEL, name, KEY_DEFAULT_VALUE, 0);
+        if (isInRange(dv, values)) {
+          label.setDefaultValue(dv);
+        } else {
+          error(new ValidationError(PROJECT_CONFIG, String.format(
+              "Invalid %s \"%s\" for label \"%s\"",
+              KEY_DEFAULT_VALUE, dv, name)));
+        }
+      }
       label.setCopyMinScore(
           rc.getBoolean(LABEL, name, KEY_COPY_MIN_SCORE, false));
       label.setCopyMaxScore(
@@ -679,6 +709,15 @@
     }
   }
 
+  private boolean isInRange(short value, List<LabelValue> labelValues) {
+    for (LabelValue lv : labelValues) {
+      if (lv.getValue() == value) {
+        return true;
+      }
+    }
+    return false;
+  }
+
   private List<String> getStringListOrNull(Config rc, String section,
       String subSection, String name) {
     String[] ac = rc.getStringList(section, subSection, name);
@@ -731,9 +770,8 @@
   }
 
   private Map<String, GroupReference> readGroupList() throws IOException {
-    groupsByUUID = new HashMap<AccountGroup.UUID, GroupReference>();
-    Map<String, GroupReference> groupsByName =
-        new HashMap<String, GroupReference>();
+    groupsByUUID = new HashMap<>();
+    Map<String, GroupReference> groupsByName = new HashMap<>();
 
     BufferedReader br = new BufferedReader(new StringReader(readUTF8(GROUP_LIST)));
     String s;
@@ -775,20 +813,20 @@
     }
     set(rc, ACCESS, null, KEY_INHERIT_FROM, p.getParentName());
 
-    set(rc, RECEIVE, null, KEY_REQUIRE_CONTRIBUTOR_AGREEMENT, p.getUseContributorAgreements(), Project.InheritableBoolean.INHERIT);
-    set(rc, RECEIVE, null, KEY_REQUIRE_SIGNED_OFF_BY, p.getUseSignedOffBy(), Project.InheritableBoolean.INHERIT);
-    set(rc, RECEIVE, null, KEY_REQUIRE_CHANGE_ID, p.getRequireChangeID(), Project.InheritableBoolean.INHERIT);
+    set(rc, RECEIVE, null, KEY_REQUIRE_CONTRIBUTOR_AGREEMENT, p.getUseContributorAgreements(), InheritableBoolean.INHERIT);
+    set(rc, RECEIVE, null, KEY_REQUIRE_SIGNED_OFF_BY, p.getUseSignedOffBy(), InheritableBoolean.INHERIT);
+    set(rc, RECEIVE, null, KEY_REQUIRE_CHANGE_ID, p.getRequireChangeID(), InheritableBoolean.INHERIT);
     set(rc, RECEIVE, null, KEY_MAX_OBJECT_SIZE_LIMIT, validMaxObjectSizeLimit(p.getMaxObjectSizeLimit()));
 
     set(rc, SUBMIT, null, KEY_ACTION, p.getSubmitType(), defaultSubmitAction);
-    set(rc, SUBMIT, null, KEY_MERGE_CONTENT, p.getUseContentMerge(), Project.InheritableBoolean.INHERIT);
+    set(rc, SUBMIT, null, KEY_MERGE_CONTENT, p.getUseContentMerge(), InheritableBoolean.INHERIT);
 
     set(rc, PROJECT, null, KEY_STATE, p.getState(), defaultStateValue);
 
     set(rc, DASHBOARD, null, KEY_DEFAULT, p.getDefaultDashboard());
     set(rc, DASHBOARD, null, KEY_LOCAL_DEFAULT, p.getLocalDefaultDashboard());
 
-    Set<AccountGroup.UUID> keepGroups = new HashSet<AccountGroup.UUID>();
+    Set<AccountGroup.UUID> keepGroups = new HashSet<>();
     saveAccountsSection(rc, keepGroups);
     saveContributorAgreements(rc, keepGroups);
     saveAccessSections(rc, keepGroups);
@@ -905,7 +943,7 @@
 
   private List<String> ruleToStringList(
       List<PermissionRule> list, Set<AccountGroup.UUID> keepGroups) {
-    List<String> rules = new ArrayList<String>();
+    List<String> rules = new ArrayList<>();
     for (PermissionRule rule : sort(list)) {
       if (rule.getGroup().getUUID() != null) {
         keepGroups.add(rule.getGroup().getUUID());
@@ -919,12 +957,12 @@
       Config rc, Set<AccountGroup.UUID> keepGroups) {
     AccessSection capability = accessSections.get(AccessSection.GLOBAL_CAPABILITIES);
     if (capability != null) {
-      Set<String> have = new HashSet<String>();
+      Set<String> have = new HashSet<>();
       for (Permission permission : sort(capability.getPermissions())) {
         have.add(permission.getName().toLowerCase());
 
         boolean needRange = GlobalCapability.hasRange(permission.getName());
-        List<String> rules = new ArrayList<String>();
+        List<String> rules = new ArrayList<>();
         for (PermissionRule rule : sort(permission.getRules())) {
           GroupReference group = rule.getGroup();
           if (group.getUUID() != null) {
@@ -964,12 +1002,12 @@
         rc.unset(ACCESS, refName, KEY_GROUP_PERMISSIONS);
       }
 
-      Set<String> have = new HashSet<String>();
+      Set<String> have = new HashSet<>();
       for (Permission permission : sort(as.getPermissions())) {
         have.add(permission.getName().toLowerCase());
 
         boolean needRange = Permission.hasRange(permission.getName());
-        List<String> rules = new ArrayList<String>();
+        List<String> rules = new ArrayList<>();
         for (PermissionRule rule : sort(permission.getRules())) {
           GroupReference group = rule.getGroup();
           if (group.getUUID() != null) {
@@ -1009,14 +1047,7 @@
       LabelType label = e.getValue();
       toUnset.remove(name);
       rc.setString(LABEL, name, KEY_FUNCTION, label.getFunctionName());
-
-      if (!LabelType.defaultAbbreviation(name)
-          .equals(label.getAbbreviation())) {
-        rc.setString(
-            LABEL, name, KEY_ABBREVIATION, label.getAbbreviation());
-      } else {
-        rc.unset(LABEL, name, KEY_ABBREVIATION);
-      }
+      rc.setInt(LABEL, name, KEY_DEFAULT_VALUE, label.getDefaultValue());
       if (label.isCopyMinScore()) {
         rc.setBoolean(LABEL, name, KEY_COPY_MIN_SCORE, true);
       } else {
@@ -1111,7 +1142,7 @@
 
   private void error(ValidationError error) {
     if (validationErrors == null) {
-      validationErrors = new ArrayList<ValidationError>(4);
+      validationErrors = new ArrayList<>(4);
     }
     validationErrors.add(error);
   }
@@ -1130,7 +1161,7 @@
   }
 
   private static <T extends Comparable<? super T>> List<T> sort(Collection<T> m) {
-    ArrayList<T> r = new ArrayList<T>(m);
+    ArrayList<T> r = new ArrayList<>(m);
     Collections.sort(r);
     return r;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseSorter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseSorter.java
index b0232ca..8595582 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseSorter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseSorter.java
@@ -42,8 +42,8 @@
 
   public List<CodeReviewCommit> sort(Collection<CodeReviewCommit> incoming)
       throws IOException {
-    final List<CodeReviewCommit> sorted = new ArrayList<CodeReviewCommit>();
-    final Set<CodeReviewCommit> sort = new HashSet<CodeReviewCommit>(incoming);
+    final List<CodeReviewCommit> sorted = new ArrayList<>();
+    final Set<CodeReviewCommit> sort = new HashSet<>(incoming);
     while (!sort.isEmpty()) {
       final CodeReviewCommit n = removeOne(sort);
 
@@ -54,7 +54,7 @@
       }
 
       CodeReviewCommit c;
-      final List<CodeReviewCommit> contents = new ArrayList<CodeReviewCommit>();
+      final List<CodeReviewCommit> contents = new ArrayList<>();
       while ((c = (CodeReviewCommit) rw.next()) != null) {
         if (!c.has(canMergeFlag) || !incoming.contains(c)) {
           // We cannot merge n as it would bring something we
@@ -62,7 +62,7 @@
           //
           if (n.missing == null) {
             n.setStatusCode(CommitMergeStatus.MISSING_DEPENDENCY);
-            n.missing = new ArrayList<CodeReviewCommit>();
+            n.missing = new ArrayList<>();
           }
           n.missing.add(c);
         } else {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
index ef58c2b..cedf11a43 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
@@ -50,6 +50,7 @@
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.Capable;
+import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
@@ -69,12 +70,15 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalCopier;
 import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.change.ChangeInserter;
+import com.google.gerrit.server.change.ChangeKind;
+import com.google.gerrit.server.change.ChangeKindCache;
 import com.google.gerrit.server.change.ChangesCollection;
 import com.google.gerrit.server.change.MergeabilityChecker;
 import com.google.gerrit.server.change.RevisionResource;
@@ -97,12 +101,14 @@
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.RefControl;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.ssh.SshInfo;
+import com.google.gerrit.server.util.LabelVote;
 import com.google.gerrit.server.util.MagicBranch;
 import com.google.gerrit.server.util.RequestScopePropagator;
 import com.google.gerrit.server.util.TimeUtil;
@@ -278,6 +284,7 @@
   private final ChangeHooks hooks;
   private final ApprovalsUtil approvalsUtil;
   private final ApprovalCopier approvalCopier;
+  private final ChangeMessagesUtil cmUtil;
   private final GitRepositoryManager repoManager;
   private final ProjectCache projectCache;
   private final String canonicalWebUrl;
@@ -294,6 +301,7 @@
   private final SshInfo sshInfo;
   private final AllProjectsName allProjectsName;
   private final ReceiveConfig receiveConfig;
+  private final ChangeKindCache changeKindCache;
 
   private final ProjectControl projectControl;
   private final Project project;
@@ -305,10 +313,10 @@
 
   private List<CreateRequest> newChanges = Collections.emptyList();
   private final Map<Change.Id, ReplaceRequest> replaceByChange =
-      new HashMap<Change.Id, ReplaceRequest>();
+      new HashMap<>();
   private final Map<RevCommit, ReplaceRequest> replaceByCommit =
-      new HashMap<RevCommit, ReplaceRequest>();
-  private final Set<RevCommit> validCommits = new HashSet<RevCommit>();
+      new HashMap<>();
+  private final Set<RevCommit> validCommits = new HashSet<>();
 
   private ListMultimap<Change.Id, Ref> refsByChange;
   private SetMultimap<ObjectId, Ref> refsById;
@@ -319,7 +327,7 @@
   private final MergeQueue mergeQueue;
   private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
 
-  private final List<CommitValidationMessage> messages = new ArrayList<CommitValidationMessage>();
+  private final List<CommitValidationMessage> messages = new ArrayList<>();
   private ListMultimap<Error, String> errors = LinkedListMultimap.create();
   private Task newProgress;
   private Task replaceProgress;
@@ -343,6 +351,7 @@
       final ChangeHooks hooks,
       final ApprovalsUtil approvalsUtil,
       final ApprovalCopier approvalCopier,
+      final ChangeMessagesUtil cmUtil,
       final ProjectCache projectCache,
       final GitRepositoryManager repoManager,
       final TagCache tagCache,
@@ -366,6 +375,7 @@
       final SubmoduleOp.Factory subOpFactory,
       final Provider<Submit> submitProvider,
       final MergeQueue mergeQueue,
+      final ChangeKindCache changeKindCache,
       final DynamicMap<ProjectConfigEntry> pluginConfigEntries) throws IOException {
     this.currentUser = (IdentifiedUser) projectControl.getCurrentUser();
     this.db = db;
@@ -382,6 +392,7 @@
     this.hooks = hooks;
     this.approvalsUtil = approvalsUtil;
     this.approvalCopier = approvalCopier;
+    this.cmUtil = cmUtil;
     this.projectCache = projectCache;
     this.repoManager = repoManager;
     this.canonicalWebUrl = canonicalWebUrl;
@@ -398,6 +409,7 @@
     this.sshInfo = sshInfo;
     this.allProjectsName = allProjectsName;
     this.receiveConfig = config;
+    this.changeKindCache = changeKindCache;
 
     this.projectControl = projectControl;
     this.labelTypes = projectControl.getLabelTypes();
@@ -438,7 +450,7 @@
       rp.setCheckReferencedObjectsAreReachable(config.checkReferencedObjectsAreReachable);
       rp.setAdvertiseRefsHook(new VisibleRefFilter(tagCache, changeCache, repo, projectControl, db, false));
     }
-    List<AdvertiseRefsHook> advHooks = new ArrayList<AdvertiseRefsHook>(3);
+    List<AdvertiseRefsHook> advHooks = new ArrayList<>(3);
     advHooks.add(new AdvertiseRefsHook() {
       @Override
       public void advertiseRefs(BaseReceivePack rp)
@@ -575,65 +587,70 @@
     }
 
     for (final ReceiveCommand c : commands) {
-      if (c.getResult() == OK) {
-        switch (c.getType()) {
-          case CREATE:
-            if (isHead(c) || isConfig(c)) {
-              autoCloseChanges(c);
-            }
-            break;
+        if (c.getResult() == OK) {
+          try {
+            switch (c.getType()) {
+              case CREATE:
+                if (isHead(c) || isConfig(c)) {
+                  autoCloseChanges(c);
+                }
+                break;
 
-          case UPDATE: // otherwise known as a fast-forward
-            tagCache.updateFastForward(project.getNameKey(),
-                c.getRefName(),
-                c.getOldId(),
-                c.getNewId());
-            if (isHead(c) || isConfig(c)) {
-              autoCloseChanges(c);
-            }
-            break;
+              case UPDATE: // otherwise known as a fast-forward
+                tagCache.updateFastForward(project.getNameKey(),
+                    c.getRefName(),
+                    c.getOldId(),
+                    c.getNewId());
+                if (isHead(c) || isConfig(c)) {
+                  autoCloseChanges(c);
+                }
+                break;
 
-          case UPDATE_NONFASTFORWARD:
-            if (isHead(c) || isConfig(c)) {
-              autoCloseChanges(c);
-            }
-            break;
+              case UPDATE_NONFASTFORWARD:
+                if (isHead(c) || isConfig(c)) {
+                  autoCloseChanges(c);
+                }
+                break;
 
-          case DELETE:
-            ResultSet<SubmoduleSubscription> submoduleSubscriptions = null;
-            Branch.NameKey projRef = new Branch.NameKey(project.getNameKey(),
-                c.getRefName());
-            try {
-              submoduleSubscriptions =
-                  db.submoduleSubscriptions().bySuperProject(projRef);
-              db.submoduleSubscriptions().delete(submoduleSubscriptions);
-            } catch (OrmException e) {
-              log.error("Cannot delete submodule subscription(s) of branch "
-                  + projRef + ": " + submoduleSubscriptions, e);
+              case DELETE:
+                ResultSet<SubmoduleSubscription> submoduleSubscriptions = null;
+                Branch.NameKey projRef = new Branch.NameKey(project.getNameKey(),
+                    c.getRefName());
+                try {
+                  submoduleSubscriptions =
+                      db.submoduleSubscriptions().bySuperProject(projRef);
+                  db.submoduleSubscriptions().delete(submoduleSubscriptions);
+                } catch (OrmException e) {
+                  log.error("Cannot delete submodule subscription(s) of branch "
+                      + projRef + ": " + submoduleSubscriptions, e);
+                }
+                break;
             }
-            break;
+
+            if (isConfig(c)) {
+              projectCache.evict(project);
+              ProjectState ps = projectCache.get(project.getNameKey());
+              repoManager.setProjectDescription(project.getNameKey(), //
+                  ps.getProject().getDescription());
+            }
+
+            if (!MagicBranch.isMagicBranch(c.getRefName())) {
+              // We only fire gitRefUpdated for direct refs updates.
+              // Events for change refs are fired when they are created.
+              //
+              gitRefUpdated.fire(project.getNameKey(), c.getRefName(),
+                  c.getOldId(), c.getNewId());
+              hooks.doRefUpdatedHook(
+                  new Branch.NameKey(project.getNameKey(), c.getRefName()),
+                  c.getOldId(),
+                  c.getNewId(),
+                  currentUser.getAccount());
+            }
+          } catch (NoSuchChangeException e) {
+            c.setResult(REJECTED_OTHER_REASON,
+                "No such change: " + e.getMessage());
+          }
         }
-
-        if (isConfig(c)) {
-          projectCache.evict(project);
-          ProjectState ps = projectCache.get(project.getNameKey());
-          repoManager.setProjectDescription(project.getNameKey(), //
-              ps.getProject().getDescription());
-        }
-
-        if (!MagicBranch.isMagicBranch(c.getRefName())) {
-          // We only fire gitRefUpdated for direct refs updates.
-          // Events for change refs are fired when they are created.
-          //
-          gitRefUpdated.fire(project.getNameKey(), c.getRefName(),
-              c.getOldId(), c.getNewId());
-          hooks.doRefUpdatedHook(
-              new Branch.NameKey(project.getNameKey(), c.getRefName()),
-              c.getOldId(),
-              c.getNewId(),
-              currentUser.getAccount());
-        }
-      }
     }
     closeProgress.end();
     commandProgress.end();
@@ -677,7 +694,9 @@
     StringBuilder m = new StringBuilder()
         .append("  ")
         .append(url)
-        .append(change.getChangeId());
+        .append(change.getChangeId())
+        .append(" ")
+        .append(ChangeUtil.cropSubject(change.getSubject()));
     if (change.getStatus() == Change.Status.DRAFT) {
       m.append(" [DRAFT]");
     }
@@ -714,6 +733,8 @@
         }
       } else if (replace.inputCommand.getResult() == NOT_ATTEMPTED) {
         reject(replace.inputCommand, "internal server error");
+        log.error(String.format("Replacement for project %s was not attempted",
+            project.getName()));
       }
     }
 
@@ -1082,7 +1103,10 @@
     RefControl ctl;
     Set<Account.Id> reviewer = Sets.newLinkedHashSet();
     Set<Account.Id> cc = Sets.newLinkedHashSet();
+    Map<String, Short> labels = new HashMap<>();
     List<RevCommit> baseCommit;
+    LabelTypes labelTypes;
+    CmdLineParser clp;
 
     @Option(name = "--base", metaVar = "BASE", usage = "merge base of changes")
     List<ObjectId> base;
@@ -1111,9 +1135,23 @@
       draft = !publish;
     }
 
-    MagicBranchInput(ReceiveCommand cmd) {
+    @Option(name = "-l", metaVar = "LABEL+VALUE",
+        usage = "label(s) to assign (defaults to +1 if no value provided")
+    void addLabel(final String token) throws CmdLineException {
+      LabelVote v = LabelVote.parse(token);
+      try {
+        LabelType.checkName(v.getLabel());
+        ApprovalsUtil.checkLabel(labelTypes, v.getLabel(), v.getValue());
+      } catch (IllegalArgumentException e) {
+        throw clp.reject(e.getMessage());
+      }
+      labels.put(v.getLabel(), v.getValue());
+    }
+
+    MagicBranchInput(ReceiveCommand cmd, LabelTypes labelTypes) {
       this.cmd = cmd;
       this.draft = cmd.getRefName().startsWith(MagicBranch.NEW_DRAFT_CHANGE);
+      this.labelTypes = labelTypes;
     }
 
     boolean isDraft() {
@@ -1128,6 +1166,10 @@
       return new MailRecipients(reviewer, cc);
     }
 
+    Map<String, Short> getLabels() {
+      return labels;
+    }
+
     String parse(CmdLineParser clp, Repository repo, Set<String> refs)
         throws CmdLineException {
       String ref = MagicBranch.getDestBranchName(cmd.getRefName());
@@ -1170,6 +1212,10 @@
       }
       return ref.substring(0, split);
     }
+
+    void setCmdLineParser(CmdLineParser clp) {
+      this.clp = clp;
+    }
   }
 
   private void parseMagicBranch(final ReceiveCommand cmd) {
@@ -1179,12 +1225,13 @@
       return;
     }
 
-    magicBranch = new MagicBranchInput(cmd);
+    magicBranch = new MagicBranchInput(cmd, labelTypes);
     magicBranch.reviewer.addAll(reviewersFromCommandLine);
     magicBranch.cc.addAll(ccFromCommandLine);
 
     String ref;
     CmdLineParser clp = optionParserFactory.create(magicBranch);
+    magicBranch.setCmdLineParser(clp);
     try {
       ref = magicBranch.parse(clp, repo, rp.getAdvertisedRefs().keySet());
     } catch (CmdLineException e) {
@@ -1242,6 +1289,7 @@
     if (magicBranch.isSubmit() && !projectControl.controlForRef(
         MagicBranch.NEW_CHANGE + ref).canSubmit()) {
       reject(cmd, "submit not allowed");
+      return;
     }
 
     RevWalk walk = rp.getRevWalk();
@@ -1395,7 +1443,7 @@
       }
 
       List<ChangeLookup> pending = Lists.newArrayList();
-      final Set<Change.Key> newChangeIds = new HashSet<Change.Key>();
+      final Set<Change.Key> newChangeIds = new HashSet<>();
       for (;;) {
         final RevCommit c = walk.next();
         if (c == null) {
@@ -1583,8 +1631,10 @@
       final Account.Id me = currentUser.getAccountId();
       final List<FooterLine> footerLines = commit.getFooterLines();
       final MailRecipients recipients = new MailRecipients();
+      Map<String, Short> approvals = new HashMap<>();
       if (magicBranch != null) {
         recipients.add(magicBranch.getMailRecipients());
+        approvals = magicBranch.getLabels();
       }
       recipients.add(getRecipientsFromFooters(accountResolver, ps, footerLines));
       recipients.remove(me);
@@ -1596,6 +1646,7 @@
 
       ins
         .setReviewers(recipients.getReviewers())
+        .setApprovals(approvals)
         .setMessage(msg)
         .setSendMail(false)
         .insert();
@@ -1683,14 +1734,18 @@
         }
       }
     } catch (OrmException err) {
-      log.error("Cannot read database before replacement", err);
+      log.error(String.format(
+          "Cannot read database before replacement for project %s",
+          project.getName()), err);
       for (ReplaceRequest req : replaceByChange.values()) {
         if (req.inputCommand.getResult() == NOT_ATTEMPTED) {
           req.inputCommand.setResult(REJECTED_OTHER_REASON, "internal server error");
         }
       }
     } catch (IOException err) {
-      log.error("Cannot read repository before replacement", err);
+      log.error(String.format(
+          "Cannot read repository before replacement for project %s",
+          project.getName()), err);
       for (ReplaceRequest req : replaceByChange.values()) {
         if (req.inputCommand.getResult() == NOT_ATTEMPTED) {
           req.inputCommand.setResult(REJECTED_OTHER_REASON, "internal server error");
@@ -1887,7 +1942,7 @@
       ListenableFuture<PatchSet.Id> future = changeUpdateExector.submit(
           requestScopePropagator.wrap(new Callable<PatchSet.Id>() {
         @Override
-        public PatchSet.Id call() throws OrmException, IOException {
+        public PatchSet.Id call() throws OrmException, IOException, NoSuchChangeException {
           try {
             if (caller == Thread.currentThread()) {
               return insertPatchSet(db);
@@ -1909,12 +1964,38 @@
       return Futures.makeChecked(future, INSERT_EXCEPTION);
     }
 
+    private ChangeMessage newChangeMessage(ReviewDb db) throws OrmException {
+      msg =
+          new ChangeMessage(new ChangeMessage.Key(change.getId(), ChangeUtil
+              .messageUUID(db)), currentUser.getAccountId(), newPatchSet.getCreatedOn(),
+              newPatchSet.getId());
+      RevCommit priorCommit = revisions.inverse().get(priorPatchSet);
+      ChangeKind changeKind = changeKindCache.getChangeKind(
+          projectControl.getProjectState(), repo, priorCommit, newCommit);
+      String message = "Uploaded patch set " + newPatchSet.getPatchSetId();
+      switch (changeKind) {
+        case TRIVIAL_REBASE:
+          message += ": Patch Set " + priorPatchSet.get() + " was rebased";
+          break;
+        case NO_CODE_CHANGE:
+          message += ": Commit message was updated";
+          break;
+        case REWORK:
+        default:
+          break;
+      }
+      msg.setMessage(message + ".");
+      return msg;
+    }
+
     PatchSet.Id insertPatchSet(ReviewDb db) throws OrmException, IOException {
       final Account.Id me = currentUser.getAccountId();
       final List<FooterLine> footerLines = newCommit.getFooterLines();
       final MailRecipients recipients = new MailRecipients();
+      Map<String, Short> approvals = new HashMap<>();
       if (magicBranch != null) {
         recipients.add(magicBranch.getMailRecipients());
+        approvals = magicBranch.getLabels();
       }
       recipients.add(getRecipientsFromFooters(accountResolver, newPatchSet, footerLines));
       recipients.remove(me);
@@ -1942,13 +2023,11 @@
         approvalCopier.copy(db, changeCtl, newPatchSet);
         approvalsUtil.addReviewers(db, update, labelTypes, change, newPatchSet,
             info, recipients.getReviewers(), oldRecipients.getAll());
+        approvalsUtil.addApprovals(db, update, labelTypes, newPatchSet, info,
+            change, changeCtl, approvals);
         recipients.add(oldRecipients);
 
-        msg =
-            new ChangeMessage(new ChangeMessage.Key(change.getId(), ChangeUtil
-                .messageUUID(db)), me, newPatchSet.getCreatedOn(), newPatchSet.getId());
-        msg.setMessage("Uploaded patch set " + newPatchSet.getPatchSetId() + ".");
-        db.changeMessages().insert(Collections.singleton(msg));
+        cmUtil.addChangeMessage(db, update, newChangeMessage(db));
 
         if (mergedIntoRef == null) {
           // Change should be new, so it can go through review again.
@@ -2004,7 +2083,7 @@
       if (mergedIntoRef != null) {
         // Change was already submitted to a branch, close it.
         //
-        markChangeMergedByPush(db, this);
+        markChangeMergedByPush(db, this, changeCtl);
       }
 
       if (cmd.getResult() == NOT_ATTEMPTED) {
@@ -2212,7 +2291,7 @@
     return true;
   }
 
-  private void autoCloseChanges(final ReceiveCommand cmd) {
+  private void autoCloseChanges(final ReceiveCommand cmd) throws NoSuchChangeException {
     final RevWalk rw = rp.getRevWalk();
     try {
       rw.reset();
@@ -2224,7 +2303,7 @@
       final SetMultimap<ObjectId, Ref> byCommit = changeRefsById();
       final Map<Change.Key, Change.Id> byKey = openChangesByKey(
           new Branch.NameKey(project.getNameKey(), cmd.getRefName()));
-      final List<ReplaceRequest> toClose = new ArrayList<ReplaceRequest>();
+      final List<ReplaceRequest> toClose = new ArrayList<>();
       RevCommit c;
       while ((c = rw.next()) != null) {
         final Set<Ref> refs = byCommit.get(c.copy());
@@ -2312,7 +2391,7 @@
     result.newPatchSet = ps;
     result.info = patchSetInfoFactory.get(commit, psi);
     result.mergedIntoRef = refName;
-    markChangeMergedByPush(db, result);
+    markChangeMergedByPush(db, result, result.changeCtl);
     hooks.doChangeMergedHook(
         change, currentUser.getAccount(), result.newPatchSet, db);
     sendMergedEmail(result);
@@ -2333,18 +2412,20 @@
 
   private Map<Change.Key, Change.Id> openChangesByKey(Branch.NameKey branch)
       throws OrmException {
-    final Map<Change.Key, Change.Id> r = new HashMap<Change.Key, Change.Id>();
+    final Map<Change.Key, Change.Id> r = new HashMap<>();
     for (Change c : db.changes().byBranchOpenAll(branch)) {
       r.put(c.getKey(), c.getId());
     }
     return r;
   }
 
-  private void markChangeMergedByPush(ReviewDb db, final ReplaceRequest result)
-      throws OrmException, IOException {
+  private void markChangeMergedByPush(ReviewDb db, final ReplaceRequest result,
+      ChangeControl control) throws OrmException, IOException {
     Change.Id id = result.change.getId();
     db.changes().beginTransaction(id);
     Change change;
+
+    ChangeUpdate update;
     try {
       change = db.changes().atomicUpdate(id, new AtomicUpdate<Change>() {
           @Override
@@ -2377,12 +2458,15 @@
           result.info.getKey());
       msg.setMessage(msgBuf.toString());
 
-      db.changeMessages().insert(Collections.singleton(msg));
+      update = updateFactory.create(control, change.getLastUpdatedOn());
+
+      cmUtil.addChangeMessage(db, update, msg);
       db.commit();
     } finally {
       db.rollback();
     }
     indexer.index(db, change);
+    update.commit();
   }
 
   private void sendMergedEmail(final ReplaceRequest result) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReloadSubmitQueueOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReloadSubmitQueueOp.java
index bcc2107..dd7a85b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReloadSubmitQueueOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReloadSubmitQueueOp.java
@@ -20,17 +20,15 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.util.HashSet;
 
+@Singleton
 public class ReloadSubmitQueueOp extends DefaultQueueOp {
-  public interface Factory {
-    ReloadSubmitQueueOp create();
-  }
-
   private static final Logger log =
       LoggerFactory.getLogger(ReloadSubmitQueueOp.class);
 
@@ -46,7 +44,7 @@
   }
 
   public void run() {
-    final HashSet<Branch.NameKey> pending = new HashSet<Branch.NameKey>();
+    final HashSet<Branch.NameKey> pending = new HashSet<>();
     try {
       final ReviewDb c = schema.open();
       try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/RenameGroupOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/RenameGroupOp.java
index 7490006..2b3994c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/RenameGroupOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/RenameGroupOp.java
@@ -69,7 +69,7 @@
     this.uuid = uuid;
     this.oldName = oldName;
     this.newName = newName;
-    this.retryOn = new ArrayList<Project.NameKey>();
+    this.retryOn = new ArrayList<>();
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
index b91eadc..89d3202 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.git;
 
+import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.Nullable;
@@ -118,7 +119,7 @@
     this.account = account;
     this.changeHooks = changeHooks;
 
-    updatedSubscribers = new HashSet<Branch.NameKey>();
+    updatedSubscribers = new HashSet<>();
   }
 
   public void update() throws SubmoduleException {
@@ -159,14 +160,13 @@
                 destBranch.get());
 
         final Set<SubmoduleSubscription> oldSubscriptions =
-            new HashSet<SubmoduleSubscription>(schema.submoduleSubscriptions()
+            new HashSet<>(schema.submoduleSubscriptions()
                 .bySuperProject(destBranch).toList());
         final List<SubmoduleSubscription> newSubscriptions =
             new SubmoduleSectionParser(bbc, thisServer, target, repoManager)
                 .parseAllSections();
 
-        final Set<SubmoduleSubscription> alreadySubscribeds =
-            new HashSet<SubmoduleSubscription>();
+        final Set<SubmoduleSubscription> alreadySubscribeds = new HashSet<>();
         for (SubmoduleSubscription s : newSubscriptions) {
           if (oldSubscriptions.contains(s)) {
             alreadySubscribeds.add(s);
@@ -241,12 +241,10 @@
               log.error("Possible circular subscription involving " + s);
             } else {
 
-              Map<Branch.NameKey, ObjectId> modules =
-                  new HashMap<Branch.NameKey, ObjectId>(1);
+            Map<Branch.NameKey, ObjectId> modules = new HashMap<>(1);
               modules.put(updatedBranch, mergedCommit);
 
-              Map<Branch.NameKey, String> paths =
-                  new HashMap<Branch.NameKey, String>(1);
+            Map<Branch.NameKey, String> paths = new HashMap<>(1);
               paths.put(updatedBranch, s.getPath());
               updateGitlinks(s.getSuperProject(), myRw, modules, paths, msgbuf);
             }
@@ -298,8 +296,13 @@
         msgbuf.append(me.getKey().getParentKey().get());
         msgbuf.append("  ").append(me.getValue().getName());
         msgbuf.append("\n");
-        if (modules.size() == 1 && msg != null) {
-          msgbuf.append(msg);
+        if (modules.size() == 1) {
+          if (!Strings.isNullOrEmpty(msg)) {
+            msgbuf.append(msg);
+          } else {
+            msgbuf.append("\n");
+            msgbuf.append(c.getFullMessage());
+          }
         } else {
           msgbuf.append(c.getShortMessage());
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagMatcher.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagMatcher.java
index b63378f..e550927 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagMatcher.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagMatcher.java
@@ -27,8 +27,8 @@
 
 class TagMatcher {
   final BitSet mask = new BitSet();
-  final List<Ref> newRefs = new ArrayList<Ref>();
-  final List<LostRef> lostRefs = new ArrayList<LostRef>();
+  final List<Ref> newRefs = new ArrayList<>();
+  final List<LostRef> lostRefs = new ArrayList<>();
   final TagSetHolder holder;
   final TagCache cache;
   final Repository db;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSet.java
index 99c7bdc..6519bd9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSet.java
@@ -51,8 +51,8 @@
 
   TagSet(Project.NameKey projectName) {
     this.projectName = projectName;
-    this.refs = new HashMap<String, CachedRef>();
-    this.tags = new ObjectIdOwnerMap<Tag>();
+    this.refs = new HashMap<>();
+    this.tags = new ObjectIdOwnerMap<>();
   }
 
   Tag lookupTag(AnyObjectId id) {
@@ -238,7 +238,7 @@
     // Gerrit Code Review server, perhaps about 50% of new references.
     // Since a complete rebuild is so costly, try this approach first.
 
-    Map<ObjectId, Integer> byObj = new HashMap<ObjectId, Integer>();
+    Map<ObjectId, Integer> byObj = new HashMap<>();
     for (CachedRef r : old.refs.values()) {
       ObjectId id = r.get();
       if (!byObj.containsKey(id)) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
index e531f5f..de37215 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
@@ -222,13 +222,20 @@
           return;
         }
 
-        final ObjectId res = newTree.writeTree(inserter);
-        if (res.equals(srcTree) && !update.allowEmpty()) {
+        ObjectId res = newTree.writeTree(inserter);
+        if (res.equals(srcTree) && !update.allowEmpty()
+            && (commit.getTreeId() == null)) {
           // If there are no changes to the content, don't create the commit.
           return;
         }
 
-        commit.setTreeId(res);
+        if (commit.getTreeId() == null) {
+          commit.setTreeId(res);
+        } else {
+          // In this case, the caller populated the tree without using DirCache.
+          res = commit.getTreeId();
+        }
+
         if (src != null) {
           commit.addParentId(src);
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
index 5a2071c..a913601 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
@@ -77,8 +77,8 @@
     }
 
     final Set<Change.Id> visibleChanges = visibleChanges();
-    final Map<String, Ref> result = new HashMap<String, Ref>();
-    final List<Ref> deferredTags = new ArrayList<Ref>();
+    final Map<String, Ref> result = new HashMap<>();
+    final List<Ref> deferredTags = new ArrayList<>();
 
     for (Ref ref : refs.values()) {
       if (ref.getName().startsWith(RefNames.REFS_CACHE_AUTOMERGE)) {
@@ -149,7 +149,7 @@
 
     final Project project = projectCtl.getProject();
     try {
-      final Set<Change.Id> visibleChanges = new HashSet<Change.Id>();
+      final Set<Change.Id> visibleChanges = new HashSet<>();
       for (Change change : changeCache.get(project.getNameKey())) {
         if (projectCtl.controlFor(change).isVisible(reviewDb)) {
           visibleChanges.add(change.getId());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java
index 6e274a1..161fa9e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.git;
 
 import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.ListenableFutureTask;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.reviewdb.client.Project.NameKey;
@@ -28,6 +29,7 @@
 import org.slf4j.LoggerFactory;
 
 import java.lang.Thread.UncaughtExceptionHandler;
+import java.lang.reflect.Field;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Date;
@@ -90,10 +92,14 @@
   private final CopyOnWriteArrayList<Executor> queues;
 
   @Inject
-  WorkQueue(final IdGenerator idGenerator, @GerritServerConfig final Config cfg) {
+  WorkQueue(IdGenerator idGenerator, @GerritServerConfig Config cfg) {
+    this(idGenerator, cfg.getInt("execution", "defaultThreadPoolSize", 1));
+  }
+
+  public WorkQueue(IdGenerator idGenerator, int defaultThreadPoolSize) {
     this.idGenerator = idGenerator;
-    this.queues = new CopyOnWriteArrayList<Executor>();
-    defaultQueueSize = cfg.getInt("execution", "defaultThreadPoolSize", 1);
+    this.queues = new CopyOnWriteArrayList<>();
+    this.defaultQueueSize = defaultThreadPoolSize;
   }
 
   /** Get the default work queue, for miscellaneous tasks. */
@@ -115,7 +121,7 @@
 
   /** Get all of the tasks currently scheduled in any work queue. */
   public List<Task<?>> getTasks() {
-    final List<Task<?>> r = new ArrayList<Task<?>>();
+    final List<Task<?>> r = new ArrayList<>();
     for (final Executor e : queues) {
       e.addAllTo(r);
     }
@@ -182,7 +188,7 @@
         }
       });
 
-      all = new ConcurrentHashMap<Integer, Task<?>>( //
+      all = new ConcurrentHashMap<>( //
           corePoolSize << 1, // table size
           0.75f, // load factor
           corePoolSize + 4 // concurrency level
@@ -203,9 +209,9 @@
         Task<V> task;
 
         if (runnable instanceof ProjectRunnable) {
-          task = new ProjectTask<V>((ProjectRunnable) runnable, r, this, id);
+          task = new ProjectTask<>((ProjectRunnable) runnable, r, this, id);
         } else {
-          task = new Task<V>(runnable, r, this, id);
+          task = new Task<>(runnable, r, this, id);
         }
 
         if (all.putIfAbsent(task.getTaskId(), task) == null) {
@@ -374,6 +380,26 @@
 
     @Override
     public String toString() {
+      //This is a workaround to be able to print a proper name when the task
+      //is wrapped into a ListenableFutureTask.
+      if (runnable instanceof ListenableFutureTask<?>) {
+        String errorMessage;
+        try {
+          for (Field field : ListenableFutureTask.class.getSuperclass()
+              .getDeclaredFields()) {
+            if (field.getType().isAssignableFrom(Callable.class)) {
+              field.setAccessible(true);
+              return ((Callable<?>) field.get(runnable)).toString();
+            }
+          }
+          errorMessage = "Cannot find wrapped Callable field";
+        } catch (SecurityException | IllegalArgumentException
+            | IllegalAccessException e) {
+          errorMessage = "Cannot call toString on Callable field";
+        }
+        log.debug("Cannot get a proper name for ListenableFutureTask: {}",
+            errorMessage);
+      }
       return runnable.toString();
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
index 3b24bac..b89d91f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
@@ -56,7 +56,7 @@
 
     this.patchSetInfoFactory = patchSetInfoFactory;
     this.gitRefUpdated = gitRefUpdated;
-    this.newCommits = new HashMap<Change.Id, CodeReviewCommit>();
+    this.newCommits = new HashMap<>();
   }
 
   @Override
@@ -138,15 +138,16 @@
     final PatchSetApproval submitAudit = args.mergeUtil.getSubmitter(n);
 
     IdentifiedUser cherryPickUser;
+    PersonIdent serverNow = args.serverIdent.get();
     PersonIdent cherryPickCommitterIdent;
     if (submitAudit != null) {
       cherryPickUser =
           args.identifiedUserFactory.create(submitAudit.getAccountId());
       cherryPickCommitterIdent = cherryPickUser.newCommitterIdent(
-          submitAudit.getGranted(), args.serverIdent.get().getTimeZone());
+          serverNow.getWhen(), serverNow.getTimeZone());
     } else {
       cherryPickUser = args.identifiedUserFactory.create(n.change().getOwner());
-      cherryPickCommitterIdent = args.serverIdent.get();
+      cherryPickCommitterIdent = serverNow;
     }
 
     final String cherryPickCmtMsg = args.mergeUtil.createCherryPickCommitMessage(n);
@@ -179,7 +180,7 @@
 
       final List<PatchSetApproval> approvals = Lists.newArrayList();
       for (PatchSetApproval a
-          : args.approvalsUtil.byPatchSet(args.db, n.notes(), n.getPatchsetId())) {
+          : args.approvalsUtil.byPatchSet(args.db, n.getControl(), n.getPatchsetId())) {
         approvals.add(new PatchSetApproval(ps.getId(), a));
       }
       args.db.patchSetApprovals().insert(approvals);
@@ -212,7 +213,7 @@
   private static void insertAncestors(ReviewDb db, PatchSet.Id id, RevCommit src)
       throws OrmException {
     final int cnt = src.getParentCount();
-    List<PatchSetAncestor> toInsert = new ArrayList<PatchSetAncestor>(cnt);
+    List<PatchSetAncestor> toInsert = new ArrayList<>(cnt);
     for (int p = 0; p < cnt; p++) {
       PatchSetAncestor a;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java
index f8177f8..130d170 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java
@@ -49,7 +49,7 @@
     super(args);
     this.patchSetInfoFactory = patchSetInfoFactory;
     this.rebaseChange = rebaseChange;
-    this.newCommits = new HashMap<Change.Id, CodeReviewCommit>();
+    this.newCommits = new HashMap<>();
   }
 
   @Override
@@ -92,7 +92,7 @@
 
             List<PatchSetApproval> approvals = Lists.newArrayList();
             for (PatchSetApproval a : args.approvalsUtil.byPatchSet(
-                args.db, n.notes(), n.getPatchsetId())) {
+                args.db, n.getControl(), n.getPatchsetId())) {
               approvals.add(new PatchSetApproval(newPatchSet.getId(), a));
             }
             // rebaseChange.rebase() may already have copied some approvals,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
index a3c9e6e..a864b6c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.server.git.strategy;
 
+import com.google.gerrit.extensions.common.SubmitType;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.Project.SubmitType;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.IdentifiedUser;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java
index f260249..091523b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.git.strategy;
 
+import com.google.gerrit.extensions.common.SubmitType;
 import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Project.SubmitType;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.GerritPersonIdent;
@@ -32,6 +32,7 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.PersonIdent;
@@ -45,6 +46,7 @@
 import java.util.Set;
 
 /** Factory to create a {@link SubmitStrategy} for a {@link SubmitType}. */
+@Singleton
 public class SubmitStrategyFactory {
   private static final Logger log = LoggerFactory
       .getLogger(SubmitStrategyFactory.class);
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 32eabbc..9d0eb66 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
@@ -95,8 +95,7 @@
   public List<CommitValidationMessage> validateForReceiveCommits(
       CommitReceivedEvent receiveEvent) throws CommitValidationException {
 
-    List<CommitValidationListener> validators =
-        new LinkedList<CommitValidationListener>();
+    List<CommitValidationListener> validators = new LinkedList<>();
 
     validators.add(new UploadMergesPermissionValidator(refControl));
     validators.add(new AmendedGerritMergeCommitValidationListener(
@@ -114,8 +113,7 @@
     validators.add(new BannedCommitsValidator(repo));
     validators.add(new PluginCommitValidationListener(commitValidationListeners));
 
-    List<CommitValidationMessage> messages =
-        new LinkedList<CommitValidationMessage>();
+    List<CommitValidationMessage> messages = new LinkedList<>();
 
     try {
       for (CommitValidationListener commitValidator : validators) {
@@ -132,8 +130,7 @@
   public List<CommitValidationMessage> validateForGerritCommits(
       CommitReceivedEvent receiveEvent) throws CommitValidationException {
 
-    List<CommitValidationListener> validators =
-        new LinkedList<CommitValidationListener>();
+    List<CommitValidationListener> validators = new LinkedList<>();
 
     validators.add(new UploadMergesPermissionValidator(refControl));
     validators.add(new AmendedGerritMergeCommitValidationListener(
@@ -149,8 +146,7 @@
     validators.add(new ConfigValidator(refControl, repo));
     validators.add(new PluginCommitValidationListener(commitValidationListeners));
 
-    List<CommitValidationMessage> messages =
-        new LinkedList<CommitValidationMessage>();
+    List<CommitValidationMessage> messages = new LinkedList<>();
 
     try {
       for (CommitValidationListener commitValidator : validators) {
@@ -185,8 +181,7 @@
         CommitReceivedEvent receiveEvent) throws CommitValidationException {
       final List<String> idList = receiveEvent.commit.getFooterLines(CHANGE_ID);
 
-      List<CommitValidationMessage> messages =
-          new LinkedList<CommitValidationMessage>();
+      List<CommitValidationMessage> messages = new LinkedList<>();
 
       if (idList.isEmpty()) {
         if (projectControl.getProjectState().isRequireChangeID()) {
@@ -325,8 +320,7 @@
       IdentifiedUser currentUser = (IdentifiedUser) refControl.getCurrentUser();
 
       if (RefNames.REFS_CONFIG.equals(refControl.getRefName())) {
-        List<CommitValidationMessage> messages =
-            new LinkedList<CommitValidationMessage>();
+        List<CommitValidationMessage> messages = new LinkedList<>();
 
         try {
           ProjectConfig cfg =
@@ -385,8 +379,7 @@
     @Override
     public List<CommitValidationMessage> onCommitReceived(
         CommitReceivedEvent receiveEvent) throws CommitValidationException {
-      List<CommitValidationMessage> messages =
-          new LinkedList<CommitValidationMessage>();
+      List<CommitValidationMessage> messages = new LinkedList<>();
 
       for (CommitValidationListener validator : commitValidationListeners) {
         try {
@@ -456,8 +449,7 @@
 
       if (!currentUser.getEmailAddresses().contains(author.getEmailAddress())
           && !refControl.canForgeAuthor()) {
-        List<CommitValidationMessage> messages =
-            new LinkedList<CommitValidationMessage>();
+        List<CommitValidationMessage> messages = new LinkedList<>();
 
         messages.add(getInvalidEmailError(receiveEvent.commit, "author", author,
             currentUser, canonicalWebUrl));
@@ -487,8 +479,7 @@
       if (!currentUser.getEmailAddresses()
           .contains(committer.getEmailAddress())
           && !refControl.canForgeCommitter()) {
-        List<CommitValidationMessage> messages =
-            new LinkedList<CommitValidationMessage>();
+        List<CommitValidationMessage> messages = new LinkedList<>();
         messages.add(getInvalidEmailError(receiveEvent.commit, "committer", committer,
             currentUser, canonicalWebUrl));
         throw new CommitValidationException("invalid committer", messages);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/UploadValidationException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/UploadValidationException.java
new file mode 100644
index 0000000..159496b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/UploadValidationException.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git.validators;
+
+import org.eclipse.jgit.transport.ServiceMayNotContinueException;
+
+public class UploadValidationException extends ServiceMayNotContinueException {
+
+  private static final long serialVersionUID = 1L;
+
+  public UploadValidationException(String message, Throwable cause) {
+    super(message, cause);
+  }
+
+  public UploadValidationException(String message) {
+    super(message);
+  }
+}
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/UploadValidationListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/UploadValidationListener.java
new file mode 100644
index 0000000..fefe02a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/UploadValidationListener.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git.validators;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.validators.ValidationException;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.UploadPack;
+
+import java.util.Collection;
+
+/**
+ * Listener to provide validation for upload operations.
+ *
+ * Invoked by Gerrit before it begins to send a pack to the client.
+ *
+ * Implementors can block the upload operation by throwing a
+ * ValidationException. The exception's message text will be reported to
+ * the end-user over the client's protocol connection.
+ */
+@ExtensionPoint
+public interface UploadValidationListener {
+
+  /**
+   * Validate an upload before it begins.
+   *
+   * @param repository The repository
+   * @param project The project
+   * @param remoteHost Remote address/hostname of the user
+   * @param wants The list of wanted objects. These may be RevObject or
+   *        RevCommit if the processor parsed them. Implementors should not rely
+   *        on the values being parsed.
+   * @param haves The list of common objects. Empty on an initial clone request.
+   *        These may be RevObject or RevCommit if the processor parsed them.
+   *        Implementors should not rely on the values being parsed.
+   * @throws ValidationException to block the upload and send a message
+   *         back to the end-used over the client's protocol connection.
+   */
+  public void onPreUpload(Repository repository, Project project,
+      String remoteHost, UploadPack up, Collection<? extends ObjectId> wants,
+      Collection<? extends ObjectId> haves)
+      throws ValidationException;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/UploadValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/UploadValidators.java
new file mode 100644
index 0000000..1735d28
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/UploadValidators.java
@@ -0,0 +1,80 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git.validators;
+
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.validators.ValidationException;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.PreUploadHook;
+import org.eclipse.jgit.transport.ServiceMayNotContinueException;
+import org.eclipse.jgit.transport.UploadPack;
+
+import java.util.Collection;
+
+import javax.inject.Inject;
+
+public class UploadValidators implements PreUploadHook {
+
+  private final DynamicSet<UploadValidationListener> uploadValidationListeners;
+  private final Project project;
+  private final Repository repository;
+  private final String remoteHost;
+
+  public interface Factory {
+    UploadValidators create(Project project, Repository repository,
+        String remoteAddress);
+  }
+
+  @Inject
+  UploadValidators(
+      DynamicSet<UploadValidationListener> uploadValidationListeners,
+      @Assisted Project project, @Assisted Repository repository,
+      @Assisted String remoteHost) {
+    this.uploadValidationListeners = uploadValidationListeners;
+    this.project = project;
+    this.repository = repository;
+    this.remoteHost = remoteHost;
+  }
+
+  @Override
+  public void onSendPack(UploadPack up, Collection<? extends ObjectId> wants,
+      Collection<? extends ObjectId> haves)
+      throws ServiceMayNotContinueException {
+    for (UploadValidationListener validator : uploadValidationListeners) {
+      try {
+        validator.onPreUpload(repository, project, remoteHost, up, wants, haves);
+      } catch (ValidationException e) {
+        throw new UploadValidationException(e.getMessage());
+      }
+
+    }
+  }
+
+  @Override
+  public void onBeginNegotiateRound(UploadPack up,
+      Collection<? extends ObjectId> wants, int cntOffered)
+      throws ServiceMayNotContinueException {
+  }
+
+  @Override
+  public void onEndNegotiateRound(UploadPack up,
+      Collection<? extends ObjectId> wants, int cntCommon, int cntNotFound,
+      boolean ready) throws ServiceMayNotContinueException {
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java
index c5273ba..163b335 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java
@@ -38,10 +38,12 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import java.util.List;
 import java.util.Map;
 
+@Singleton
 public class AddIncludedGroups implements RestModifyView<GroupResource, Input> {
   public static class Input {
     @DefaultInput
@@ -69,15 +71,15 @@
     }
   }
 
-  private final Provider<GroupsCollection> groupsCollection;
+  private final GroupsCollection groupsCollection;
   private final GroupIncludeCache groupIncludeCache;
-  private final ReviewDb db;
+  private final Provider<ReviewDb> db;
   private final GroupJson json;
 
   @Inject
-  public AddIncludedGroups(Provider<GroupsCollection> groupsCollection,
+  public AddIncludedGroups(GroupsCollection groupsCollection,
       GroupIncludeCache groupIncludeCache,
-      ReviewDb db, GroupJson json) {
+      Provider<ReviewDb> db, GroupJson json) {
     this.groupsCollection = groupsCollection;
     this.groupIncludeCache = groupIncludeCache;
     this.db = db;
@@ -101,7 +103,7 @@
     Account.Id me = ((IdentifiedUser) control.getCurrentUser()).getAccountId();
 
     for (String includedGroup : input.groups) {
-      GroupDescription.Basic d = groupsCollection.get().parse(includedGroup);
+      GroupDescription.Basic d = groupsCollection.parse(includedGroup);
       if (!control.canAddGroup(d.getGroupUUID())) {
         throw new AuthException(String.format("Cannot add group: %s",
             d.getName()));
@@ -111,7 +113,7 @@
         AccountGroupById.Key agiKey =
             new AccountGroupById.Key(group.getId(),
                 d.getGroupUUID());
-        AccountGroupById agi = db.accountGroupById().get(agiKey);
+        AccountGroupById agi = db.get().accountGroupById().get(agiKey);
         if (agi == null) {
           agi = new AccountGroupById(agiKey);
           newIncludedGroups.put(d.getGroupUUID(), agi);
@@ -123,12 +125,12 @@
     }
 
     if (!newIncludedGroups.isEmpty()) {
-      db.accountGroupByIdAud().insert(newIncludedGroupsAudits);
-      db.accountGroupById().insert(newIncludedGroups.values());
+      db.get().accountGroupByIdAud().insert(newIncludedGroupsAudits);
+      db.get().accountGroupById().insert(newIncludedGroups.values());
       for (AccountGroupById agi : newIncludedGroups.values()) {
-        groupIncludeCache.evictMemberIn(agi.getIncludeUUID());
+        groupIncludeCache.evictParentGroupsOf(agi.getIncludeUUID());
       }
-      groupIncludeCache.evictMembersOf(group.getGroupUUID());
+      groupIncludeCache.evictSubgroupsOf(group.getGroupUUID());
     }
 
     return result;
@@ -138,10 +140,10 @@
     static class Input {
     }
 
-    private final Provider<AddIncludedGroups> put;
+    private final AddIncludedGroups put;
     private final String id;
 
-    PutIncludedGroup(Provider<AddIncludedGroups> put, String id) {
+    PutIncludedGroup(AddIncludedGroups put, String id) {
       this.put = put;
       this.id = id;
     }
@@ -152,7 +154,7 @@
         UnprocessableEntityException, OrmException {
       AddIncludedGroups.Input in = new AddIncludedGroups.Input();
       in.groups = ImmutableList.of(id);
-      List<GroupInfo> list = put.get().apply(resource, in);
+      List<GroupInfo> list = put.apply(resource, in);
       if (list.size() == 1) {
         return list.get(0);
       }
@@ -160,6 +162,7 @@
     }
   }
 
+  @Singleton
   static class UpdateIncludedGroup implements RestModifyView<IncludedGroupResource, PutIncludedGroup.Input> {
     static class Input {
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java
index 3da7839..df58c8f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java
@@ -43,10 +43,12 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import java.util.List;
 import java.util.Map;
 
+@Singleton
 public class AddMembers implements RestModifyView<GroupResource, Input> {
   public static class Input {
     @DefaultInput
@@ -75,20 +77,20 @@
 
   private final AccountManager accountManager;
   private final AuthType authType;
-  private final Provider<AccountsCollection> accounts;
+  private final AccountsCollection accounts;
   private final AccountResolver accountResolver;
   private final AccountCache accountCache;
   private final AccountInfo.Loader.Factory infoFactory;
-  private final ReviewDb db;
+  private final Provider<ReviewDb> db;
 
   @Inject
   AddMembers(AccountManager accountManager,
       AuthConfig authConfig,
-      Provider<AccountsCollection> accounts,
+      AccountsCollection accounts,
       AccountResolver accountResolver,
       AccountCache accountCache,
       AccountInfo.Loader.Factory infoFactory,
-      ReviewDb db) {
+      Provider<ReviewDb> db) {
     this.accountManager = accountManager;
     this.authType = authConfig.getAuthType();
     this.accounts = accounts;
@@ -129,7 +131,7 @@
       if (!newAccountGroupMembers.containsKey(a.getId())) {
         AccountGroupMember.Key key =
             new AccountGroupMember.Key(a.getId(), internalGroup.getId());
-        AccountGroupMember m = db.accountGroupMembers().get(key);
+        AccountGroupMember m = db.get().accountGroupMembers().get(key);
         if (m == null) {
           m = new AccountGroupMember(key);
           newAccountGroupMembers.put(m.getAccountId(), m);
@@ -140,8 +142,8 @@
       result.add(loader.get(a.getId()));
     }
 
-    db.accountGroupMembersAudit().insert(newAccountGroupMemberAudits);
-    db.accountGroupMembers().insert(newAccountGroupMembers.values());
+    db.get().accountGroupMembersAudit().insert(newAccountGroupMemberAudits);
+    db.get().accountGroupMembers().insert(newAccountGroupMembers.values());
     for (AccountGroupMember m : newAccountGroupMembers.values()) {
       accountCache.evict(m.getAccountId());
     }
@@ -153,7 +155,7 @@
   private Account findAccount(String nameOrEmail) throws AuthException,
       UnprocessableEntityException, OrmException {
     try {
-      return accounts.get().parse(nameOrEmail).getAccount();
+      return accounts.parse(nameOrEmail).getAccount();
     } catch (UnprocessableEntityException e) {
       // might be because the account does not exist or because the account is
       // not visible
@@ -194,10 +196,10 @@
     static class Input {
     }
 
-    private final Provider<AddMembers> put;
+    private final AddMembers put;
     private final String id;
 
-    PutMember(Provider<AddMembers> put, String id) {
+    PutMember(AddMembers put, String id) {
       this.put = put;
       this.id = id;
     }
@@ -208,7 +210,7 @@
         UnprocessableEntityException, OrmException {
       AddMembers.Input in = new AddMembers.Input();
       in._oneMember = id;
-      List<AccountInfo> list = put.get().apply(resource, in);
+      List<AccountInfo> list = put.apply(resource, in);
       if (list.size() == 1) {
         return list.get(0);
       }
@@ -216,14 +218,15 @@
     }
   }
 
+  @Singleton
   static class UpdateMember implements RestModifyView<MemberResource, PutMember.Input> {
     static class Input {
     }
 
-    private final Provider<GetMember> get;
+    private final GetMember get;
 
     @Inject
-    UpdateMember(Provider<GetMember> get) {
+    UpdateMember(GetMember get) {
       this.get = get;
     }
 
@@ -231,7 +234,7 @@
     public AccountInfo apply(MemberResource resource, PutMember.Input input)
         throws OrmException {
       // Do nothing, the user is already a member.
-      return get.get().apply(resource);
+      return get.apply(resource);
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java
index 04dcda3..8cc1a4a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java
@@ -37,19 +37,22 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import java.util.List;
 import java.util.Map;
 
+@Singleton
 public class DeleteIncludedGroups implements RestModifyView<GroupResource, Input> {
-  private final Provider<GroupsCollection> groupsCollection;
+  private final GroupsCollection groupsCollection;
   private final GroupIncludeCache groupIncludeCache;
-  private final ReviewDb db;
+  private final Provider<ReviewDb> db;
   private final Provider<CurrentUser> self;
 
   @Inject
-  DeleteIncludedGroups(Provider<GroupsCollection> groupsCollection,
-      GroupIncludeCache groupIncludeCache, ReviewDb db,
+  DeleteIncludedGroups(GroupsCollection groupsCollection,
+      GroupIncludeCache groupIncludeCache,
+      Provider<ReviewDb> db,
       Provider<CurrentUser> self) {
     this.groupsCollection = groupsCollection;
     this.groupIncludeCache = groupIncludeCache;
@@ -72,7 +75,7 @@
     final List<AccountGroupById> toRemove = Lists.newLinkedList();
 
     for (final String includedGroup : input.groups) {
-      GroupDescription.Basic d = groupsCollection.get().parse(includedGroup);
+      GroupDescription.Basic d = groupsCollection.parse(includedGroup);
       if (!control.canRemoveGroup(d.getGroupUUID())) {
         throw new AuthException(String.format("Cannot delete group: %s",
             d.getName()));
@@ -86,11 +89,11 @@
 
     if (!toRemove.isEmpty()) {
       writeAudits(toRemove);
-      db.accountGroupById().delete(toRemove);
+      db.get().accountGroupById().delete(toRemove);
       for (final AccountGroupById g : toRemove) {
-        groupIncludeCache.evictMemberIn(g.getIncludeUUID());
+        groupIncludeCache.evictParentGroupsOf(g.getIncludeUUID());
       }
-      groupIncludeCache.evictMembersOf(internalGroup.getGroupUUID());
+      groupIncludeCache.evictSubgroupsOf(internalGroup.getGroupUUID());
     }
 
     return Response.none();
@@ -100,7 +103,7 @@
       final AccountGroup.Id groupId) throws OrmException {
     final Map<AccountGroup.UUID, AccountGroupById> groups =
         Maps.newHashMap();
-    for (final AccountGroupById g : db.accountGroupById().byGroup(groupId)) {
+    for (AccountGroupById g : db.get().accountGroupById().byGroup(groupId)) {
       groups.put(g.getIncludeUUID(), g);
     }
     return groups;
@@ -112,7 +115,7 @@
     final List<AccountGroupByIdAud> auditUpdates = Lists.newLinkedList();
     for (final AccountGroupById g : toBeRemoved) {
       AccountGroupByIdAud audit = null;
-      for (AccountGroupByIdAud a : db
+      for (AccountGroupByIdAud a : db.get()
           .accountGroupByIdAud().byGroupInclude(g.getGroupId(),
               g.getIncludeUUID())) {
         if (a.isActive()) {
@@ -126,9 +129,10 @@
         auditUpdates.add(audit);
       }
     }
-    db.accountGroupByIdAud().update(auditUpdates);
+    db.get().accountGroupByIdAud().update(auditUpdates);
   }
 
+  @Singleton
   static class DeleteIncludedGroup implements
       RestModifyView<IncludedGroupResource, DeleteIncludedGroup.Input> {
     static class Input {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java
index fd1b8f4..654ad88 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java
@@ -36,19 +36,22 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import java.util.List;
 import java.util.Map;
 
+@Singleton
 public class DeleteMembers implements RestModifyView<GroupResource, Input> {
-  private final Provider<AccountsCollection> accounts;
+  private final AccountsCollection accounts;
   private final AccountCache accountCache;
-  private final ReviewDb db;
+  private final Provider<ReviewDb> db;
   private final Provider<CurrentUser> self;
 
   @Inject
-  DeleteMembers(Provider<AccountsCollection> accounts,
-      AccountCache accountCache, ReviewDb db, Provider<CurrentUser> self) {
+  DeleteMembers(AccountsCollection accounts,
+      AccountCache accountCache, Provider<ReviewDb> db,
+      Provider<CurrentUser> self) {
     this.accounts = accounts;
     this.accountCache = accountCache;
     this.db = db;
@@ -70,7 +73,7 @@
     final List<AccountGroupMember> toRemove = Lists.newLinkedList();
 
     for (final String nameOrEmail : input.members) {
-      Account a = accounts.get().parse(nameOrEmail).getAccount();
+      Account a = accounts.parse(nameOrEmail).getAccount();
 
       if (!control.canRemoveMember(a.getId())) {
         throw new AuthException("Cannot delete member: " + a.getFullName());
@@ -83,7 +86,7 @@
     }
 
     writeAudits(toRemove);
-    db.accountGroupMembers().delete(toRemove);
+    db.get().accountGroupMembers().delete(toRemove);
     for (final AccountGroupMember m : toRemove) {
       accountCache.evict(m.getAccountId());
     }
@@ -98,7 +101,7 @@
     final List<AccountGroupMemberAudit> auditInserts = Lists.newLinkedList();
     for (final AccountGroupMember m : toBeRemoved) {
       AccountGroupMemberAudit audit = null;
-      for (AccountGroupMemberAudit a : db.accountGroupMembersAudit()
+      for (AccountGroupMemberAudit a : db.get().accountGroupMembersAudit()
           .byGroupAccount(m.getAccountGroupId(), m.getAccountId())) {
         if (a.isActive()) {
           audit = a;
@@ -115,19 +118,21 @@
         auditInserts.add(audit);
       }
     }
-    db.accountGroupMembersAudit().update(auditUpdates);
-    db.accountGroupMembersAudit().insert(auditInserts);
+    db.get().accountGroupMembersAudit().update(auditUpdates);
+    db.get().accountGroupMembersAudit().insert(auditInserts);
   }
 
   private Map<Account.Id, AccountGroupMember> getMembers(
       final AccountGroup.Id groupId) throws OrmException {
     final Map<Account.Id, AccountGroupMember> members = Maps.newHashMap();
-    for (final AccountGroupMember m : db.accountGroupMembers().byGroup(groupId)) {
+    for (final AccountGroupMember m : db.get().accountGroupMembers()
+        .byGroup(groupId)) {
       members.put(m.getAccountId(), m);
     }
     return members;
   }
 
+  @Singleton
   static class DeleteMember implements RestModifyView<MemberResource, DeleteMember.Input> {
     static class Input {
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetDescription.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetDescription.java
index 4674a21..8c804dd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetDescription.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetDescription.java
@@ -18,7 +18,9 @@
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.inject.Singleton;
 
+@Singleton
 class GetDescription implements RestReadView<GroupResource> {
   @Override
   public String apply(GroupResource resource) throws MethodNotAllowedException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetDetail.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetDetail.java
index f1d2e15..936798d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetDetail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetDetail.java
@@ -19,7 +19,9 @@
 import com.google.gerrit.server.group.GroupJson.GroupInfo;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
+@Singleton
 public class GetDetail implements RestReadView<GroupResource> {
   private final GroupJson json;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetGroup.java
index 09180dd..95042a2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetGroup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetGroup.java
@@ -18,7 +18,9 @@
 import com.google.gerrit.server.group.GroupJson.GroupInfo;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
+@Singleton
 class GetGroup implements RestReadView<GroupResource> {
   private final GroupJson json;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetIncludedGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetIncludedGroup.java
index 32f20c0..5d3853e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetIncludedGroup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetIncludedGroup.java
@@ -18,7 +18,9 @@
 import com.google.gerrit.server.group.GroupJson.GroupInfo;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
+@Singleton
 public class GetIncludedGroup implements RestReadView<IncludedGroupResource>  {
   private final GroupJson json;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetMember.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetMember.java
index d98dd24..2782932e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetMember.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetMember.java
@@ -18,7 +18,9 @@
 import com.google.gerrit.server.account.AccountInfo;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
+@Singleton
 public class GetMember implements RestReadView<MemberResource> {
   private final AccountInfo.Loader.Factory infoFactory;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetName.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetName.java
index c6da7c4..ce4df2a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetName.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetName.java
@@ -15,7 +15,9 @@
 package com.google.gerrit.server.group;
 
 import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.inject.Singleton;
 
+@Singleton
 public class GetName implements RestReadView<GroupResource> {
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetOptions.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetOptions.java
index 3fbfc70..5d1ede0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetOptions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetOptions.java
@@ -15,7 +15,9 @@
 package com.google.gerrit.server.group;
 
 import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.inject.Singleton;
 
+@Singleton
 public class GetOptions implements RestReadView<GroupResource> {
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetOwner.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetOwner.java
index 0113a164..5fc62c6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetOwner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetOwner.java
@@ -23,7 +23,9 @@
 import com.google.gerrit.server.group.GroupJson.GroupInfo;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
+@Singleton
 public class GetOwner implements RestReadView<GroupResource> {
 
   private final GroupControl.Factory controlFactory;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupJson.java
index 8c9056d..9f851f4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupJson.java
@@ -127,7 +127,6 @@
   }
 
   public static class GroupInfo extends GroupBaseInfo {
-    final String kind = "gerritcodereview#group";
     public String url;
     public GroupOptionsInfo options;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupsCollection.java
index 7f9fe77..76cd137 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupsCollection.java
@@ -35,7 +35,9 @@
 import com.google.gerrit.server.account.GroupControl;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
+@Singleton
 public class GroupsCollection implements
     RestCollection<TopLevelResource, GroupResource>,
     AcceptsCreate<TopLevelResource> {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/IncludedGroupsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/IncludedGroupsCollection.java
index 2ffff20..72f17b4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/IncludedGroupsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/IncludedGroupsCollection.java
@@ -31,21 +31,24 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
+@Singleton
 public class IncludedGroupsCollection implements
     ChildCollection<GroupResource, IncludedGroupResource>,
     AcceptsCreate<GroupResource> {
   private final DynamicMap<RestView<IncludedGroupResource>> views;
-  private final Provider<ListIncludedGroups> list;
-  private final Provider<GroupsCollection> groupsCollection;
+  private final ListIncludedGroups list;
+  private final GroupsCollection groupsCollection;
   private final Provider<ReviewDb> dbProvider;
-  private final Provider<AddIncludedGroups> put;
+  private final AddIncludedGroups put;
 
   @Inject
   IncludedGroupsCollection(DynamicMap<RestView<IncludedGroupResource>> views,
-      Provider<ListIncludedGroups> list,
-      Provider<GroupsCollection> groupsCollection,
-      Provider<ReviewDb> dbProvider, Provider<AddIncludedGroups> put) {
+      ListIncludedGroups list,
+      GroupsCollection groupsCollection,
+      Provider<ReviewDb> dbProvider,
+      AddIncludedGroups put) {
     this.views = views;
     this.list = list;
     this.groupsCollection = groupsCollection;
@@ -55,7 +58,7 @@
 
   @Override
   public RestView<GroupResource> list() {
-    return list.get();
+    return list;
   }
 
   @Override
@@ -68,7 +71,7 @@
     }
 
     GroupDescription.Basic member =
-        groupsCollection.get().parse(TopLevelResource.INSTANCE, id).getGroup();
+        groupsCollection.parse(TopLevelResource.INSTANCE, id).getGroup();
     if (isMember(parent, member)
         && resource.getControl().canSeeGroup(member.getGroupUUID())) {
       return new IncludedGroupResource(resource, member);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java
index 09dfef8..0998602 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java
@@ -67,7 +67,7 @@
 
   @Option(name = "--project", aliases = {"-p"},
       usage = "projects for which the groups should be listed")
-  private final List<ProjectControl> projects = new ArrayList<ProjectControl>();
+  private final List<ProjectControl> projects = new ArrayList<>();
 
   @Option(name = "--visible-to-all", usage = "to list only groups that are visible to all registered users")
   private boolean visibleToAll;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListIncludedGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListIncludedGroups.java
index f112e34..671486c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListIncludedGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListIncludedGroups.java
@@ -27,6 +27,7 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import org.slf4j.Logger;
 
@@ -34,6 +35,7 @@
 import java.util.Comparator;
 import java.util.List;
 
+@Singleton
 public class ListIncludedGroups implements RestReadView<GroupResource> {
   private static final Logger log = org.slf4j.LoggerFactory.getLogger(ListIncludedGroups.class);
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/MembersCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/MembersCollection.java
index efed115..fc69a1f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/MembersCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/MembersCollection.java
@@ -31,22 +31,24 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
+@Singleton
 public class MembersCollection implements
     ChildCollection<GroupResource, MemberResource>,
     AcceptsCreate<GroupResource> {
   private final DynamicMap<RestView<MemberResource>> views;
   private final Provider<ListMembers> list;
-  private final Provider<AccountsCollection> accounts;
+  private final AccountsCollection accounts;
   private final Provider<ReviewDb> db;
-  private final Provider<AddMembers> put;
+  private final AddMembers put;
 
   @Inject
   MembersCollection(DynamicMap<RestView<MemberResource>> views,
       Provider<ListMembers> list,
-      Provider<AccountsCollection> accounts,
+      AccountsCollection accounts,
       Provider<ReviewDb> db,
-      Provider<AddMembers> put) {
+      AddMembers put) {
     this.views = views;
     this.list = list;
     this.accounts = accounts;
@@ -68,7 +70,7 @@
       throw new MethodNotAllowedException();
     }
 
-    IdentifiedUser user = accounts.get().parse(TopLevelResource.INSTANCE, id).getUser();
+    IdentifiedUser user = accounts.parse(TopLevelResource.INSTANCE, id).getUser();
     AccountGroupMember.Key key =
         new AccountGroupMember.Key(user.getAccountId(), parent.toAccountGroup().getId());
     if (db.get().accountGroupMembers().get(key) != null
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutDescription.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutDescription.java
index 663eb9d..c2dc23a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutDescription.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutDescription.java
@@ -27,9 +27,12 @@
 import com.google.gerrit.server.group.PutDescription.Input;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import java.util.Collections;
 
+@Singleton
 public class PutDescription implements RestModifyView<GroupResource, Input> {
   public static class Input {
     @DefaultInput
@@ -37,10 +40,10 @@
   }
 
   private final GroupCache groupCache;
-  private final ReviewDb db;
+  private final Provider<ReviewDb> db;
 
   @Inject
-  PutDescription(GroupCache groupCache, ReviewDb db) {
+  PutDescription(GroupCache groupCache, Provider<ReviewDb> db) {
     this.groupCache = groupCache;
     this.db = db;
   }
@@ -59,14 +62,14 @@
       throw new AuthException("Not group owner");
     }
 
-    AccountGroup group = db.accountGroups().get(
+    AccountGroup group = db.get().accountGroups().get(
         resource.toAccountGroup().getId());
     if (group == null) {
       throw new ResourceNotFoundException();
     }
 
     group.setDescription(Strings.emptyToNull(input.description));
-    db.accountGroups().update(Collections.singleton(group));
+    db.get().accountGroups().update(Collections.singleton(group));
     groupCache.evict(group);
 
     return Strings.isNullOrEmpty(input.description)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutGroup.java
index 4d3d6be..9768270 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutGroup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutGroup.java
@@ -18,7 +18,9 @@
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.group.CreateGroup.Input;
+import com.google.inject.Singleton;
 
+@Singleton
 public class PutGroup implements RestModifyView<GroupResource, Input> {
   @Override
   public Response<?> apply(GroupResource resource, Input input)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutName.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutName.java
index 447b666..6d980ae 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutName.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutName.java
@@ -29,7 +29,9 @@
 import com.google.gerrit.server.group.PutName.Input;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
+@Singleton
 public class PutName implements RestModifyView<GroupResource, Input> {
   public static class Input {
     @DefaultInput
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutOptions.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutOptions.java
index 5139c34..6ed6703 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutOptions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutOptions.java
@@ -25,19 +25,22 @@
 import com.google.gerrit.server.group.PutOptions.Input;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import java.util.Collections;
 
+@Singleton
 public class PutOptions implements RestModifyView<GroupResource, Input> {
   public static class Input {
     public Boolean visibleToAll;
   }
 
   private final GroupCache groupCache;
-  private final ReviewDb db;
+  private final Provider<ReviewDb> db;
 
   @Inject
-  PutOptions(GroupCache groupCache, ReviewDb db) {
+  PutOptions(GroupCache groupCache, Provider<ReviewDb> db) {
     this.groupCache = groupCache;
     this.db = db;
   }
@@ -59,14 +62,14 @@
       input.visibleToAll = false;
     }
 
-    AccountGroup group = db.accountGroups().get(
+    AccountGroup group = db.get().accountGroups().get(
         resource.toAccountGroup().getId());
     if (group == null) {
       throw new ResourceNotFoundException();
     }
 
     group.setVisibleToAll(input.visibleToAll);
-    db.accountGroups().update(Collections.singleton(group));
+    db.get().accountGroups().update(Collections.singleton(group));
     groupCache.evict(group);
 
     return new GroupOptionsInfo(group);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutOwner.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutOwner.java
index 737a74a..11d34ab 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutOwner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutOwner.java
@@ -31,23 +31,25 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import java.util.Collections;
 
+@Singleton
 public class PutOwner implements RestModifyView<GroupResource, Input> {
   public static class Input {
     @DefaultInput
     public String owner;
   }
 
-  private final Provider<GroupsCollection> groupsCollection;
+  private final GroupsCollection groupsCollection;
   private final GroupCache groupCache;
-  private final ReviewDb db;
+  private final Provider<ReviewDb> db;
   private final GroupJson json;
 
   @Inject
-  PutOwner(Provider<GroupsCollection> groupsCollection, GroupCache groupCache,
-      ReviewDb db, GroupJson json) {
+  PutOwner(GroupsCollection groupsCollection, GroupCache groupCache,
+      Provider<ReviewDb> db, GroupJson json) {
     this.groupsCollection = groupsCollection;
     this.groupCache = groupCache;
     this.db = db;
@@ -70,15 +72,15 @@
       throw new BadRequestException("owner is required");
     }
 
-    group = db.accountGroups().get(group.getId());
+    group = db.get().accountGroups().get(group.getId());
     if (group == null) {
       throw new ResourceNotFoundException();
     }
 
-    GroupDescription.Basic owner = groupsCollection.get().parse(input.owner);
+    GroupDescription.Basic owner = groupsCollection.parse(input.owner);
     if (!group.getOwnerGroupUUID().equals(owner.getGroupUUID())) {
       group.setOwnerGroupUUID(owner.getGroupUUID());
-      db.accountGroups().update(Collections.singleton(group));
+      db.get().accountGroups().update(Collections.singleton(group));
       groupCache.evict(group);
     }
     return json.format(owner);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeBatchIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeBatchIndexer.java
index aeacac5..4779674 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeBatchIndexer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeBatchIndexer.java
@@ -32,7 +32,9 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.change.MergeabilityChecker;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.git.MultiProgressMonitor;
 import com.google.gerrit.server.git.MultiProgressMonitor.Task;
 import com.google.gerrit.server.patch.PatchListLoader;
@@ -43,13 +45,14 @@
 import org.eclipse.jgit.diff.DiffEntry;
 import org.eclipse.jgit.diff.DiffFormatter;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.ProgressMonitor;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.merge.ThreeWayMergeStrategy;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevTree;
@@ -112,6 +115,7 @@
   private final ListeningExecutorService executor;
   private final ChangeIndexer.Factory indexerFactory;
   private final MergeabilityChecker mergeabilityChecker;
+  private final ThreeWayMergeStrategy mergeStrategy;
 
   @Inject
   ChangeBatchIndexer(SchemaFactory<ReviewDb> schemaFactory,
@@ -119,6 +123,7 @@
       GitRepositoryManager repoManager,
       @IndexExecutor ListeningExecutorService executor,
       ChangeIndexer.Factory indexerFactory,
+      @GerritServerConfig Config config,
       @Nullable MergeabilityChecker mergeabilityChecker) {
     this.schemaFactory = schemaFactory;
     this.changeDataFactory = changeDataFactory;
@@ -126,6 +131,7 @@
     this.executor = executor;
     this.indexerFactory = indexerFactory;
     this.mergeabilityChecker = mergeabilityChecker;
+    this.mergeStrategy = MergeUtil.getMergeStrategy(config);
   }
 
   public Result indexAll(ChangeIndex index, Iterable<Project.NameKey> projects,
@@ -150,9 +156,7 @@
     final AtomicBoolean ok = new AtomicBoolean(true);
 
     for (final Project.NameKey project : projects) {
-      if (!updateMergeable(project)) {
-        ok.set(false);
-      }
+      updateMergeable(project);
       final ListenableFuture<?> future = executor.submit(reindexProject(
           indexerFactory.create(index), project, doneTask, failedTask,
           verboseWriter));
@@ -212,7 +216,7 @@
     if (mergeabilityChecker != null) {
       try {
         mergeabilityChecker.newCheck().addProject(project).run();
-      } catch (IOException e) {
+      } catch (Exception e) {
         log.error("Error in mergeability checker", e);
         return false;
       }
@@ -239,8 +243,13 @@
               byId.put(r.getObjectId(), changeDataFactory.create(db, c));
             }
           }
-          new ProjectIndexer(indexer, byId, repo, done, failed, verboseWriter)
-              .call();
+          new ProjectIndexer(indexer,
+              mergeStrategy,
+              byId,
+              repo,
+              done,
+              failed,
+              verboseWriter).call();
         } catch (RepositoryNotFoundException rnfe) {
           log.error(rnfe.getMessage());
         } finally {
@@ -261,6 +270,7 @@
 
   public static class ProjectIndexer implements Callable<Void> {
     private final ChangeIndexer indexer;
+    private final ThreeWayMergeStrategy mergeStrategy;
     private final Multimap<ObjectId, ChangeData> byId;
     private final ProgressMonitor done;
     private final ProgressMonitor failed;
@@ -268,16 +278,15 @@
     private final Repository repo;
     private RevWalk walk;
 
-    public ProjectIndexer(ChangeIndexer indexer,
-        Multimap<ObjectId, ChangeData> changesByCommitId, Repository repo) {
-      this(indexer, changesByCommitId, repo,
-          NullProgressMonitor.INSTANCE, NullProgressMonitor.INSTANCE, null);
-    }
-
-    ProjectIndexer(ChangeIndexer indexer,
-        Multimap<ObjectId, ChangeData> changesByCommitId, Repository repo,
-        ProgressMonitor done, ProgressMonitor failed, PrintWriter verboseWriter) {
+    private ProjectIndexer(ChangeIndexer indexer,
+        ThreeWayMergeStrategy mergeStrategy,
+        Multimap<ObjectId, ChangeData> changesByCommitId,
+        Repository repo,
+        ProgressMonitor done,
+        ProgressMonitor failed,
+        PrintWriter verboseWriter) {
       this.indexer = indexer;
+      this.mergeStrategy = mergeStrategy;
       this.byId = changesByCommitId;
       this.repo = repo;
       this.done = done;
@@ -376,7 +385,7 @@
           walk.parseBody(a);
           return walk.parseTree(a.getTree());
         case 2:
-          return PatchListLoader.automerge(repo, walk, b);
+          return PatchListLoader.automerge(repo, walk, b, mergeStrategy);
         default:
           return null;
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
index afa516f..f067e27 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.index;
 
+import com.google.common.base.Objects;
 import com.google.common.base.Splitter;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
@@ -28,6 +29,7 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import com.google.gerrit.server.query.change.ChangeStatusPredicate;
+import com.google.gerrit.server.query.change.ChangeData.ChangedLines;
 import com.google.gwtorm.protobuf.CodecFactory;
 import com.google.gwtorm.protobuf.ProtobufCodec;
 import com.google.gwtorm.server.OrmException;
@@ -93,6 +95,17 @@
         }
       };
 
+  /** Project containing the change, as a prefix field. */
+  public static final FieldDef<ChangeData, String> PROJECTS =
+      new FieldDef.Single<ChangeData, String>(
+          ChangeQueryBuilder.FIELD_PROJECTS, FieldType.PREFIX, false) {
+        @Override
+        public String get(ChangeData input, FillArgs args)
+            throws OrmException {
+          return input.change().getProject().get();
+        }
+      };
+
   /** Reference (aka branch) the change will submit onto. */
   public static final FieldDef<ChangeData, String> REF =
       new FieldDef.Single<ChangeData, String>(
@@ -104,8 +117,9 @@
         }
       };
 
+  @Deprecated
   /** Topic, a short annotation on the branch. */
-  public static final FieldDef<ChangeData, String> TOPIC =
+  public static final FieldDef<ChangeData, String> LEGACY_TOPIC =
       new FieldDef.Single<ChangeData, String>(
           ChangeQueryBuilder.FIELD_TOPIC, FieldType.EXACT, false) {
         @Override
@@ -115,6 +129,17 @@
         }
       };
 
+  /** Topic, a short annotation on the branch. */
+  public static final FieldDef<ChangeData, String> TOPIC =
+      new FieldDef.Single<ChangeData, String>(
+          "topic2", FieldType.EXACT, false) {
+        @Override
+        public String get(ChangeData input, FillArgs args)
+            throws OrmException {
+          return Objects.firstNonNull(input.change().getTopic(), "");
+        }
+      };
+
   // Same value as UPDATED, but implementations truncated to minutes.
   @Deprecated
   /** Last update time since January 1, 1970. */
@@ -404,6 +429,47 @@
         }
       };
 
+  /** The number of inserted lines in this change. */
+  public static final FieldDef<ChangeData, Integer> ADDED =
+      new FieldDef.Single<ChangeData, Integer>(
+          ChangeQueryBuilder.FIELD_ADDED, FieldType.INTEGER_RANGE, true) {
+        @Override
+        public Integer get(ChangeData input, FillArgs args)
+            throws OrmException {
+
+          return input.changedLines() != null
+              ? input.changedLines().insertions
+              : null;
+        }
+      };
+
+  /** The number of deleted lines in this change. */
+  public static final FieldDef<ChangeData, Integer> DELETED =
+      new FieldDef.Single<ChangeData, Integer>(
+          ChangeQueryBuilder.FIELD_DELETED, FieldType.INTEGER_RANGE, true) {
+        @Override
+        public Integer get(ChangeData input, FillArgs args)
+            throws OrmException {
+          return input.changedLines() != null
+              ? input.changedLines().deletions
+              : null;
+        }
+      };
+
+  /** The total number of modified lines in this change. */
+  public static final FieldDef<ChangeData, Integer> DELTA =
+      new FieldDef.Single<ChangeData, Integer>(
+          ChangeQueryBuilder.FIELD_DELTA, FieldType.INTEGER_RANGE, false) {
+        @Override
+        public Integer get(ChangeData input, FillArgs args)
+            throws OrmException {
+          ChangedLines changedLines = input.changedLines();
+          return changedLines != null
+              ? changedLines.insertions + changedLines.deletions
+              : null;
+        }
+      };
+
   private static <T> List<byte[]> toProtos(ProtobufCodec<T> codec, Collection<T> objs)
       throws OrmException {
     List<byte[]> result = Lists.newArrayListWithCapacity(objs.size());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java
index a46b22d..8bb8f0b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java
@@ -37,7 +37,7 @@
         ChangeField.STATUS,
         ChangeField.PROJECT,
         ChangeField.REF,
-        ChangeField.TOPIC,
+        ChangeField.LEGACY_TOPIC,
         ChangeField.LEGACY_UPDATED,
         ChangeField.LEGACY_SORTKEY,
         ChangeField.PATH,
@@ -57,7 +57,7 @@
         ChangeField.STATUS,
         ChangeField.PROJECT,
         ChangeField.REF,
-        ChangeField.TOPIC,
+        ChangeField.LEGACY_TOPIC,
         ChangeField.LEGACY_UPDATED,
         ChangeField.LEGACY_SORTKEY,
         ChangeField.PATH,
@@ -79,7 +79,7 @@
         ChangeField.STATUS,
         ChangeField.PROJECT,
         ChangeField.REF,
-        ChangeField.TOPIC,
+        ChangeField.LEGACY_TOPIC,
         ChangeField.LEGACY_UPDATED,
         ChangeField.SORTKEY,
         ChangeField.PATH,
@@ -104,7 +104,7 @@
         ChangeField.STATUS,
         ChangeField.PROJECT,
         ChangeField.REF,
-        ChangeField.TOPIC,
+        ChangeField.LEGACY_TOPIC,
         ChangeField.LEGACY_UPDATED,
         ChangeField.SORTKEY,
         ChangeField.PATH,
@@ -130,7 +130,7 @@
         ChangeField.STATUS,
         ChangeField.PROJECT,
         ChangeField.REF,
-        ChangeField.TOPIC,
+        ChangeField.LEGACY_TOPIC,
         ChangeField.LEGACY_UPDATED,
         ChangeField.SORTKEY,
         ChangeField.FILE_PART,
@@ -147,12 +147,60 @@
         ChangeField.APPROVAL,
         ChangeField.MERGEABLE);
 
+  @SuppressWarnings("deprecation")
   static final Schema<ChangeData> V8 = release(
         ChangeField.LEGACY_ID,
         ChangeField.ID,
         ChangeField.STATUS,
         ChangeField.PROJECT,
         ChangeField.REF,
+        ChangeField.LEGACY_TOPIC,
+        ChangeField.UPDATED,
+        ChangeField.FILE_PART,
+        ChangeField.PATH,
+        ChangeField.OWNER,
+        ChangeField.REVIEWER,
+        ChangeField.COMMIT,
+        ChangeField.TR,
+        ChangeField.LABEL,
+        ChangeField.REVIEWED,
+        ChangeField.COMMIT_MESSAGE,
+        ChangeField.COMMENT,
+        ChangeField.CHANGE,
+        ChangeField.APPROVAL,
+        ChangeField.MERGEABLE);
+
+  @SuppressWarnings("deprecation")
+  static final Schema<ChangeData> V9 = release(
+        ChangeField.LEGACY_ID,
+        ChangeField.ID,
+        ChangeField.STATUS,
+        ChangeField.PROJECT,
+        ChangeField.PROJECTS,
+        ChangeField.REF,
+        ChangeField.LEGACY_TOPIC,
+        ChangeField.UPDATED,
+        ChangeField.FILE_PART,
+        ChangeField.PATH,
+        ChangeField.OWNER,
+        ChangeField.REVIEWER,
+        ChangeField.COMMIT,
+        ChangeField.TR,
+        ChangeField.LABEL,
+        ChangeField.REVIEWED,
+        ChangeField.COMMIT_MESSAGE,
+        ChangeField.COMMENT,
+        ChangeField.CHANGE,
+        ChangeField.APPROVAL,
+        ChangeField.MERGEABLE);
+
+  static final Schema<ChangeData> V10 = release(
+        ChangeField.LEGACY_ID,
+        ChangeField.ID,
+        ChangeField.STATUS,
+        ChangeField.PROJECT,
+        ChangeField.PROJECTS,
+        ChangeField.REF,
         ChangeField.TOPIC,
         ChangeField.UPDATED,
         ChangeField.FILE_PART,
@@ -169,9 +217,36 @@
         ChangeField.APPROVAL,
         ChangeField.MERGEABLE);
 
+  static final Schema<ChangeData> V11 = release(
+        ChangeField.LEGACY_ID,
+        ChangeField.ID,
+        ChangeField.STATUS,
+        ChangeField.PROJECT,
+        ChangeField.PROJECTS,
+        ChangeField.REF,
+        ChangeField.TOPIC,
+        ChangeField.UPDATED,
+        ChangeField.FILE_PART,
+        ChangeField.PATH,
+        ChangeField.OWNER,
+        ChangeField.REVIEWER,
+        ChangeField.COMMIT,
+        ChangeField.TR,
+        ChangeField.LABEL,
+        ChangeField.REVIEWED,
+        ChangeField.COMMIT_MESSAGE,
+        ChangeField.COMMENT,
+        ChangeField.CHANGE,
+        ChangeField.APPROVAL,
+        ChangeField.MERGEABLE,
+        ChangeField.ADDED,
+        ChangeField.DELETED,
+        ChangeField.DELTA);
+
+
 
   private static Schema<ChangeData> release(Collection<FieldDef<ChangeData, ?>> fields) {
-    return new Schema<ChangeData>(true, fields);
+    return new Schema<>(true, fields);
   }
 
   @SafeVarargs
@@ -182,7 +257,7 @@
   @SafeVarargs
   @SuppressWarnings("unused")
   private static Schema<ChangeData> developer(FieldDef<ChangeData, ?>... fields) {
-    return new Schema<ChangeData>(false, Arrays.asList(fields));
+    return new Schema<>(false, Arrays.asList(fields));
   }
 
   public static final ImmutableMap<Integer, Schema<ChangeData>> ALL;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldDef.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldDef.java
index d4f9966..872179d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldDef.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldDef.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.index;
 
+import com.google.common.base.Preconditions;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.TrackingFooters;
 import com.google.gwtorm.server.OrmException;
@@ -47,6 +48,8 @@
       extends FieldDef<I, Iterable<T>> {
     Repeatable(String name, FieldType<T> type, boolean stored) {
       super(name, type, stored);
+      Preconditions.checkArgument(type != FieldType.INTEGER_RANGE,
+          "Range queries against repeated fields are unsupported");
     }
 
     @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldType.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldType.java
index a3247b9..4c40769 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldType.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldType.java
@@ -23,6 +23,10 @@
   public static final FieldType<Integer> INTEGER =
       new FieldType<Integer>("INTEGER");
 
+  /** A single-integer-valued field matched using range queries. */
+  public static final FieldType<Integer> INTEGER_RANGE =
+      new FieldType<Integer>("INTEGER_RANGE");
+
   /** A single integer-valued field. */
   public static final FieldType<Long> LONG =
       new FieldType<Long>("LONG");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexCollection.java
index 2558cb4..2380c76 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexCollection.java
@@ -35,7 +35,7 @@
   @VisibleForTesting
   public IndexCollection() {
     this.writeIndexes = Lists.newCopyOnWriteArrayList();
-    this.searchIndex = new AtomicReference<ChangeIndex>();
+    this.searchIndex = new AtomicReference<>();
   }
 
   /** @return the current search index version. */
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IntegerRangePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IntegerRangePredicate.java
new file mode 100644
index 0000000..1259951
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IntegerRangePredicate.java
@@ -0,0 +1,56 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index;
+
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.util.RangeUtil;
+import com.google.gerrit.server.util.RangeUtil.Range;
+import com.google.gwtorm.server.OrmException;
+
+public abstract class IntegerRangePredicate<T> extends IndexPredicate<T> {
+  private final Range range;
+
+  protected IntegerRangePredicate(FieldDef<T, Integer> type,
+      String value) throws QueryParseException {
+    super(type, value);
+    range = RangeUtil.getRange(value, Integer.MIN_VALUE, Integer.MAX_VALUE);
+    if (range == null) {
+      throw new QueryParseException("Invalid range predicate: " + value);
+    }
+  }
+
+  protected abstract int getValueInt(T object) throws OrmException;
+
+  @Override
+  public boolean match(T object) throws OrmException {
+    int valueInt = getValueInt(object);
+    return valueInt >= range.min && valueInt <= range.max;
+  }
+
+  /** Return the minimum value of this predicate's range, inclusive. */
+  public int getMinimumValue() {
+    return range.min;
+  }
+
+  /** Return the maximum value of this predicate's range, inclusive. */
+  public int getMaximumValue() {
+    return range.max;
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/Schema.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/Schema.java
index bda2ace..0de1379 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/Schema.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/Schema.java
@@ -82,6 +82,10 @@
     return fields;
   }
 
+  public final boolean hasField(FieldDef<T, ?> field) {
+    return fields.get(field.getName()) == field;
+  }
+
   /**
    * Build all fields in the schema from an input object.
    * <p>
@@ -109,9 +113,9 @@
             if (v == null) {
               return null;
             } else if (f.isRepeatable()) {
-              return new Values<T>(f, (Iterable<?>) v);
+              return new Values<>(f, (Iterable<?>) v);
             } else {
-              return new Values<T>(f, Collections.singleton(v));
+              return new Values<>(f, Collections.singleton(v));
             }
           }
         }).filter(Predicates.notNull());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/TimestampRangePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/TimestampRangePredicate.java
index 9277f6b..f611052 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/TimestampRangePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/TimestampRangePredicate.java
@@ -25,6 +25,7 @@
 import java.sql.Timestamp;
 import java.util.Date;
 
+// TODO: Migrate this to IntegerRangePredicate
 public abstract class TimestampRangePredicate<I> extends IndexPredicate<I> {
   @SuppressWarnings({"deprecation", "unchecked"})
   protected static FieldDef<ChangeData, Timestamp> updatedField(
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 8e2dc03..b3c0d61 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
@@ -98,7 +98,7 @@
     formatChange();
     appendText(velocifyFile("ChangeFooter.vm"));
     try {
-      TreeSet<String> names = new TreeSet<String>();
+      TreeSet<String> names = new TreeSet<>();
       for (Account.Id who : changeData.reviewers().values()) {
         names.add(getNameEmailFor(who));
       }
@@ -338,7 +338,7 @@
 
   /** Find all users who are authors of any part of this change. */
   protected Set<Account.Id> getAuthors() {
-    Set<Account.Id> authors = new HashSet<Account.Id>();
+    Set<Account.Id> authors = new HashSet<>();
 
     authors.add(change.getOwner());
     if (patchSet != null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
index cb92b0f..2beb49f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
@@ -62,10 +62,11 @@
     this.notify = notify;
   }
 
-  public void setPatchLineComments(final List<PatchLineComment> plc) {
+  public void setPatchLineComments(final List<PatchLineComment> plc)
+      throws OrmException {
     inlineComments = plc;
 
-    Set<String> paths = new HashSet<String>();
+    Set<String> paths = new HashSet<>();
     for (PatchLineComment c : plc) {
       Patch.Key p = c.getKey().getParentKey();
       if (!Patch.COMMIT_MSG.equals(p.getFileName())) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
index 3c9f39c..f570ac8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
@@ -45,30 +45,32 @@
   protected void init() throws EmailException {
     super.init();
 
-    try {
-      // Try to mark interested owners with TO and CC or BCC line.
-      Watchers matching = getWatchers(NotifyType.NEW_CHANGES);
-      for (Account.Id user : Iterables.concat(
-          matching.to.accounts,
-          matching.cc.accounts,
-          matching.bcc.accounts)) {
-        if (isOwnerOfProjectOrBranch(user)) {
-          add(RecipientType.TO, user);
+    if (change.getStatus() == Change.Status.NEW) {
+      try {
+        // Try to mark interested owners with TO and CC or BCC line.
+        Watchers matching = getWatchers(NotifyType.NEW_CHANGES);
+        for (Account.Id user : Iterables.concat(
+            matching.to.accounts,
+            matching.cc.accounts,
+            matching.bcc.accounts)) {
+          if (isOwnerOfProjectOrBranch(user)) {
+            add(RecipientType.TO, user);
+          }
         }
+
+        // Add everyone else. Owners added above will not be duplicated.
+        add(RecipientType.TO, matching.to);
+        add(RecipientType.CC, matching.cc);
+        add(RecipientType.BCC, matching.bcc);
+      } catch (OrmException err) {
+        // Just don't CC everyone. Better to send a partial message to those
+        // we already have queued up then to fail deliver entirely to people
+        // who have a lower interest in the change.
+        log.warn("Cannot notify watchers for new change", err);
       }
 
-      // Add everyone else. Owners added above will not be duplicated.
-      add(RecipientType.TO, matching.to);
-      add(RecipientType.CC, matching.cc);
-      add(RecipientType.BCC, matching.bcc);
-    } catch (OrmException err) {
-      // Just don't CC everyone. Better to send a partial message to those
-      // we already have queued up then to fail deliver entirely to people
-      // who have a lower interest in the change.
-      log.warn("Cannot notify watchers for new change", err);
+      includeWatchers(NotifyType.NEW_PATCHSETS);
     }
-
-    includeWatchers(NotifyType.NEW_PATCHSETS);
   }
 
   private boolean isOwnerOfProjectOrBranch(Account.Id user) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailHeader.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailHeader.java
index 7e4f36f..46e151b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailHeader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailHeader.java
@@ -138,7 +138,7 @@
   }
 
   public static class AddressList extends EmailHeader {
-    private final List<Address> list = new ArrayList<Address>();
+    private final List<Address> list = new ArrayList<>();
 
     public AddressList() {
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java
index ac367ef..26cccb8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java
@@ -86,14 +86,14 @@
     private final Set<Account.Id> cc;
 
     public MailRecipients() {
-      this.reviewers = new HashSet<Account.Id>();
-      this.cc = new HashSet<Account.Id>();
+      this.reviewers = new HashSet<>();
+      this.cc = new HashSet<>();
     }
 
     public MailRecipients(final Set<Account.Id> reviewers,
         final Set<Account.Id> cc) {
-      this.reviewers = new HashSet<Account.Id>(reviewers);
-      this.cc = new HashSet<Account.Id>(cc);
+      this.reviewers = new HashSet<>(reviewers);
+      this.cc = new HashSet<>(cc);
     }
 
     public void add(final MailRecipients recipients) {
@@ -111,14 +111,13 @@
     }
 
     public Set<Account.Id> getCcOnly() {
-      final Set<Account.Id> cc = new HashSet<Account.Id>(this.cc);
+      final Set<Account.Id> cc = new HashSet<>(this.cc);
       cc.removeAll(reviewers);
       return Collections.unmodifiableSet(cc);
     }
 
     public Set<Account.Id> getAll() {
-      final Set<Account.Id> all =
-          new HashSet<Account.Id>(reviewers.size() + cc.size());
+      final Set<Account.Id> all = new HashSet<>(reviewers.size() + cc.size());
       all.addAll(reviewers);
       all.addAll(cc);
       return Collections.unmodifiableSet(all);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
index 07a5f9a..5cb1ba1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
@@ -62,7 +62,7 @@
       Table<Account.Id, String, PatchSetApproval> pos = HashBasedTable.create();
       Table<Account.Id, String, PatchSetApproval> neg = HashBasedTable.create();
       for (PatchSetApproval ca : args.approvalsUtil.byPatchSet(
-            args.db.get(), changeData.notes(), patchSet.getId())) {
+            args.db.get(), changeData.changeControl(), patchSet.getId())) {
         LabelType lt = labelTypes.byLabel(ca.getLabelId());
         if (lt == null) {
           continue;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java
index 9ff0dbd..0dbcbe0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java
@@ -26,8 +26,8 @@
 
 /** Sends an email alerting a user to a new change for them to review. */
 public abstract class NewChangeSender extends ChangeEmail {
-  private final Set<Account.Id> reviewers = new HashSet<Account.Id>();
-  private final Set<Account.Id> extraCC = new HashSet<Account.Id>();
+  private final Set<Account.Id> reviewers = new HashSet<>();
+  private final Set<Account.Id> extraCC = new HashSet<>();
 
   protected NewChangeSender(EmailArguments ea, Change c) {
     super(ea, c, "newchange");
@@ -61,7 +61,7 @@
     if (reviewers.isEmpty()) {
       return null;
     }
-    List<String> names = new ArrayList<String>();
+    List<String> names = new ArrayList<>();
     for (Account.Id id : reviewers) {
       names.add(getNameFor(id));
     }
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 dc09a9c..49acda8 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
@@ -52,7 +52,7 @@
   private static final String HDR_CC = "CC";
 
   protected String messageClass;
-  private final HashSet<Account.Id> rcptTo = new HashSet<Account.Id>();
+  private final HashSet<Account.Id> rcptTo = new HashSet<>();
   private final Map<String, EmailHeader> headers;
   private final Set<Address> smtpRcptTo = Sets.newHashSet();
   private Address smtpFromAddress;
@@ -66,7 +66,7 @@
   protected OutgoingEmail(EmailArguments ea, String mc) {
     args = ea;
     messageClass = mc;
-    headers = new LinkedHashMap<String, EmailHeader>();
+    headers = new LinkedHashMap<>();
   }
 
   public void setFrom(final Account.Id id) {
@@ -388,8 +388,6 @@
       StringWriter w = new StringWriter();
       template.merge(velocityContext, w);
       return w.toString();
-    } catch (EmailException e) {
-      throw e;
     } catch (Exception e) {
       throw new EmailException("Cannot format velocity template " + name, e);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/PatchSetNotificationSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/PatchSetNotificationSender.java
index 8d35165..b847a91 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/PatchSetNotificationSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/PatchSetNotificationSender.java
@@ -37,6 +37,7 @@
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
@@ -54,7 +55,7 @@
   private static final Logger log =
       LoggerFactory.getLogger(PatchSetNotificationSender.class);
 
-  private final ReviewDb db;
+  private final Provider<ReviewDb> db;
   private final GitRepositoryManager repoManager;
   private final PatchSetInfoFactory patchSetInfoFactory;
   private final ApprovalsUtil approvalsUtil;
@@ -63,7 +64,7 @@
   private final ReplacePatchSetSender.Factory replacePatchSetFactory;
 
   @Inject
-  public PatchSetNotificationSender(ReviewDb db,
+  public PatchSetNotificationSender(Provider<ReviewDb> db,
       ChangeHooks hooks,
       GitRepositoryManager repoManager,
       PatchSetInfoFactory patchSetInfoFactory,
@@ -104,7 +105,7 @@
       recipients.remove(me);
 
       if (newChange) {
-        approvalsUtil.addReviewers(db, update, labelTypes, updatedChange,
+        approvalsUtil.addReviewers(db.get(), update, labelTypes, updatedChange,
             updatedPatchSet, info, recipients.getReviewers(),
             Collections.<Account.Id> emptySet());
         try {
@@ -118,12 +119,12 @@
           log.error("Cannot send email for new change " + updatedChange.getId(), e);
         }
       } else {
-        approvalsUtil.addReviewers(db, update, labelTypes, updatedChange,
+        approvalsUtil.addReviewers(db.get(), update, labelTypes, updatedChange,
             updatedPatchSet, info, recipients.getReviewers(),
-            approvalsUtil.getReviewers(db, notes).values());
+            approvalsUtil.getReviewers(db.get(), notes).values());
         final ChangeMessage msg =
             new ChangeMessage(new ChangeMessage.Key(updatedChange.getId(),
-                ChangeUtil.messageUUID(db)), me,
+                ChangeUtil.messageUUID(db.get())), me,
                 updatedPatchSet.getCreatedOn(), updatedPatchSet.getId());
         msg.setMessage("Uploaded patch set " + updatedPatchSet.getPatchSetId() + ".");
         try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java
index 04c3f9f..872b84a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java
@@ -64,7 +64,7 @@
   /** Returns all watchers that are relevant */
   public final Watchers getWatchers(NotifyType type) throws OrmException {
     Watchers matching = new Watchers();
-    Set<Account.Id> projectWatchers = new HashSet<Account.Id>();
+    Set<Account.Id> projectWatchers = new HashSet<>();
 
     for (AccountProjectWatch w : args.db.get().accountProjectWatches()
         .byProject(project)) {
@@ -166,7 +166,7 @@
       for (AccountGroupMember m : db.accountGroupMembers().byGroup(ig.getId())) {
         matching.accounts.add(m.getAccountId());
       }
-      for (AccountGroup.UUID m : args.groupIncludes.membersOf(uuid)) {
+      for (AccountGroup.UUID m : args.groupIncludes.subgroupsOf(uuid)) {
         if (seen.add(m)) {
           q.add(m);
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java
index d80fb50..8412d22 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java
@@ -33,8 +33,8 @@
     public ReplacePatchSetSender create(Change change);
   }
 
-  private final Set<Account.Id> reviewers = new HashSet<Account.Id>();
-  private final Set<Account.Id> extraCC = new HashSet<Account.Id>();
+  private final Set<Account.Id> reviewers = new HashSet<>();
+  private final Set<Account.Id> extraCC = new HashSet<>();
 
   @Inject
   public ReplacePatchSetSender(EmailArguments ea, @Assisted Change c) {
@@ -74,7 +74,7 @@
     if (reviewers.isEmpty()) {
       return null;
     }
-    List<String> names = new ArrayList<String>();
+    List<String> names = new ArrayList<>();
     for (Account.Id id : reviewers) {
       names.add(getNameFor(id));
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java
index 4d6eb8e..92fffde 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java
@@ -21,6 +21,7 @@
 import com.google.gwtjsonrpc.server.XsrfException;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
 import org.eclipse.jgit.util.Base64;
 
@@ -29,6 +30,7 @@
 import java.util.regex.Pattern;
 
 /** Verifies the token sent by {@link RegisterNewEmailSender}. */
+@Singleton
 public class SignedTokenEmailTokenVerifier implements EmailTokenVerifier {
   private final SignedToken emailRegistrationToken;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java
index f16cb5a..b1c5955 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java
@@ -107,7 +107,7 @@
     smtpUser = cfg.getString("sendemail", null, "smtpuser");
     smtpPass = cfg.getString("sendemail", null, "smtppass");
 
-    Set<String> rcpt = new HashSet<String>();
+    Set<String> rcpt = new HashSet<>();
     for (String addr : cfg.getStringList("sendemail", null, "allowrcpt")) {
       rcpt.add(addr);
     }
@@ -152,7 +152,7 @@
     }
 
     final Map<String, EmailHeader> hdrs =
-        new LinkedHashMap<String, EmailHeader>(callerHeaders);
+        new LinkedHashMap<>(callerHeaders);
     setMissingHeader(hdrs, "MIME-Version", "1.0");
     setMissingHeader(hdrs, "Content-Type", "text/plain; charset=UTF-8");
     setMissingHeader(hdrs, "Content-Transfer-Encoding", "8bit");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/VelocityRuntimeProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/VelocityRuntimeProvider.java
index 144c74d..32098d5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/VelocityRuntimeProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/VelocityRuntimeProvider.java
@@ -45,6 +45,7 @@
     p.setProperty(RuntimeConstants.VM_PERM_INLINE_LOCAL, "true");
     p.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM_CLASS,
         Slf4jLogChute.class.getName());
+    p.setProperty(RuntimeConstants.RUNTIME_REFERENCES_STRICT, "true");
     p.setProperty("runtime.log.logsystem.log4j.category", "velocity");
 
     if (site.mail_dir.isDirectory()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
new file mode 100644
index 0000000..37094fd
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
@@ -0,0 +1,77 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.notedb;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.VersionedMetaData;
+import com.google.gwtorm.server.OrmException;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+
+/** View of contents at a single ref related to some change. **/
+public abstract class AbstractChangeNotes<T> extends VersionedMetaData {
+  private boolean loaded;
+  protected final GitRepositoryManager repoManager;
+  private final Change change;
+
+  AbstractChangeNotes(GitRepositoryManager repoManager, Change change) {
+    this.repoManager = repoManager;
+    this.change = new Change(change);
+  }
+
+  public Change.Id getChangeId() {
+    return change.getId();
+  }
+
+  public Change getChange() {
+    return change;
+  }
+
+  public T load() throws OrmException {
+    if (!loaded) {
+      Repository repo;
+      try {
+        repo = repoManager.openRepository(getProjectName());
+      } catch (IOException e) {
+        throw new OrmException(e);
+      }
+      try {
+        load(repo);
+        loaded = true;
+      } catch (ConfigInvalidException | IOException e) {
+        throw new OrmException(e);
+      } finally {
+        repo.close();
+      }
+    }
+    return self();
+  }
+
+  /**
+   * @return the NameKey for the project where the notes should be stored,
+   *    which is not necessarily the same as the change's project.
+   */
+  protected abstract Project.NameKey getProjectName();
+
+  @SuppressWarnings("unchecked")
+  protected final T self() {
+    return (T) this;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
new file mode 100644
index 0000000..0d637f5
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
@@ -0,0 +1,161 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.notedb;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.gerrit.server.notedb.ChangeNoteUtil.GERRIT_PLACEHOLDER_HOST;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.VersionedMetaData;
+import com.google.gerrit.server.project.ChangeControl;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+import java.io.IOException;
+import java.util.Date;
+
+/** A single delta related to a specific patch-set of a change. */
+public abstract class AbstractChangeUpdate extends VersionedMetaData {
+  protected final NotesMigration migration;
+  protected final GitRepositoryManager repoManager;
+  protected final MetaDataUpdate.User updateFactory;
+  protected final ChangeControl ctl;
+  protected final PersonIdent serverIdent;
+  protected final Date when;
+  protected PatchSet.Id psId;
+
+  AbstractChangeUpdate(NotesMigration migration,
+      GitRepositoryManager repoManager,
+      MetaDataUpdate.User updateFactory, ChangeControl ctl,
+      PersonIdent serverIdent, Date when) {
+    this.migration = migration;
+    this.repoManager = repoManager;
+    this.updateFactory = updateFactory;
+    this.ctl = ctl;
+    this.serverIdent = serverIdent;
+    this.when = when;
+  }
+
+  public Change getChange() {
+    return ctl.getChange();
+  }
+
+  public Date getWhen() {
+    return when;
+  }
+
+  public IdentifiedUser getUser() {
+    return (IdentifiedUser) ctl.getCurrentUser();
+  }
+
+  public void setPatchSetId(PatchSet.Id psId) {
+    checkArgument(psId == null
+        || psId.getParentKey().equals(getChange().getId()));
+    this.psId = psId;
+  }
+
+  private void load() throws IOException {
+    if (migration.write() && getRevision() == null) {
+      Repository repo = repoManager.openRepository(getProjectName());
+      try {
+        load(repo);
+      } catch (ConfigInvalidException e) {
+        throw new IOException(e);
+      } finally {
+        repo.close();
+      }
+    }
+  }
+
+  @Override
+  public BatchMetaDataUpdate openUpdate(MetaDataUpdate update) throws IOException {
+    throw new UnsupportedOperationException("use openUpdate()");
+  }
+
+  public BatchMetaDataUpdate openUpdate() throws IOException {
+    if (migration.write()) {
+      load();
+      MetaDataUpdate md =
+          updateFactory.create(getProjectName(), getUser());
+      md.setAllowEmpty(true);
+      return super.openUpdate(md);
+    }
+    return new BatchMetaDataUpdate() {
+      @Override
+      public void write(CommitBuilder commit) {
+        // Do nothing.
+      }
+
+      @Override
+      public void write(VersionedMetaData config, CommitBuilder commit) {
+        // Do nothing.
+      }
+
+      @Override
+      public RevCommit createRef(String refName) {
+        return null;
+      }
+
+      @Override
+      public RevCommit commit() {
+        return null;
+      }
+
+      @Override
+      public RevCommit commitAt(ObjectId revision) {
+        return null;
+      }
+
+      @Override
+      public void close() {
+        // Do nothing.
+      }
+    };
+  }
+
+  @Override
+  public RevCommit commit(MetaDataUpdate md) throws IOException {
+    throw new UnsupportedOperationException("use commit()");
+  }
+
+  @Override
+  protected void onLoad() throws IOException, ConfigInvalidException {
+    //Do nothing; just reads the current revision.
+  }
+
+  protected PersonIdent newIdent(Account author, Date when) {
+    return new PersonIdent(
+        author.getFullName(),
+        author.getId().get() + "@" + GERRIT_PLACEHOLDER_HOST,
+        when, serverIdent.getTimeZone());
+  }
+
+  /**
+   * @return the NameKey for the project where the update will be stored,
+   *    which is not necessarily the same as the change's project.
+   */
+  abstract protected Project.NameKey getProjectName();
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
index dbeebed..a33209c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -26,9 +26,11 @@
 import com.google.common.base.Optional;
 import com.google.common.base.Supplier;
 import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ComparisonChain;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.ImmutableSetMultimap;
+import com.google.common.collect.LinkedListMultimap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Multimap;
@@ -39,36 +41,44 @@
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
+import com.google.gerrit.reviewdb.client.PatchSet.Id;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.PatchSetApproval.LabelId;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.VersionedMetaData;
 import com.google.gerrit.server.util.LabelVote;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.notes.NoteMap;
 import org.eclipse.jgit.revwalk.FooterKey;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.util.RawParseUtils;
 
 import java.io.IOException;
+import java.nio.charset.Charset;
 import java.sql.Timestamp;
+import java.text.ParseException;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 
 /** View of a single {@link Change} based on the log of its notes branch. */
-public class ChangeNotes extends VersionedMetaData {
+public class ChangeNotes extends AbstractChangeNotes<ChangeNotes> {
   private static final Ordering<PatchSetApproval> PSA_BY_TIME =
       Ordering.natural().onResultOf(
         new Function<PatchSetApproval, Timestamp>() {
@@ -78,6 +88,49 @@
           }
         });
 
+  public static final Ordering<ChangeMessage> MESSAGE_BY_TIME =
+      Ordering.natural().onResultOf(
+        new Function<ChangeMessage, Timestamp>() {
+          @Override
+          public Timestamp apply(ChangeMessage input) {
+            return input.getWrittenOn();
+          }
+        });
+
+  public static Comparator<PatchLineComment> PatchLineCommentComparator =
+      new Comparator<PatchLineComment>() {
+    public int compare(PatchLineComment c1, PatchLineComment c2) {
+      String filename1 = c1.getKey().getParentKey().get();
+      String filename2 = c2.getKey().getParentKey().get();
+      return ComparisonChain.start()
+          .compare(filename1, filename2)
+          .compare(c1.getLine(), c2.getLine())
+          .compare(c1.getWrittenOn(), c2.getWrittenOn())
+          .result();
+    }
+  };
+
+  public static ConfigInvalidException parseException(Change.Id changeId,
+      String fmt, Object... args) {
+    return new ConfigInvalidException("Change " + changeId + ": "
+        + String.format(fmt, args));
+  }
+
+  public static Account.Id parseIdent(PersonIdent ident, Change.Id changeId)
+      throws ConfigInvalidException {
+    String email = ident.getEmailAddress();
+    int at = email.indexOf('@');
+    if (at >= 0) {
+      String host = email.substring(at + 1, email.length());
+      Integer id = Ints.tryParse(email.substring(0, at));
+      if (id != null && host.equals(GERRIT_PLACEHOLDER_HOST)) {
+        return new Account.Id(id);
+      }
+    }
+    throw parseException(changeId, "invalid identity, expected <id>@%s: %s",
+      GERRIT_PLACEHOLDER_HOST, email);
+  }
+
   @Singleton
   public static class Factory {
     private final GitRepositoryManager repoManager;
@@ -97,26 +150,38 @@
     private final Change.Id changeId;
     private final ObjectId tip;
     private final RevWalk walk;
+    private final Repository repo;
     private final Map<PatchSet.Id,
         Table<Account.Id, String, Optional<PatchSetApproval>>> approvals;
     private final Map<Account.Id, ReviewerState> reviewers;
     private final List<SubmitRecord> submitRecords;
+    private final Multimap<PatchSet.Id, ChangeMessage> changeMessages;
+    private final Multimap<Id, PatchLineComment> commentsForPs;
+    private final Multimap<PatchSet.Id, PatchLineComment> commentsForBase;
+    private NoteMap commentNoteMap;
     private Change.Status status;
 
-    private Parser(Change.Id changeId, ObjectId tip, RevWalk walk) {
-      this.changeId = changeId;
+    private Parser(Change change, ObjectId tip, RevWalk walk,
+        GitRepositoryManager repoManager) throws RepositoryNotFoundException,
+        IOException {
+      this.changeId = change.getId();
       this.tip = tip;
       this.walk = walk;
+      this.repo = repoManager.openRepository(getProjectName(change));
       approvals = Maps.newHashMap();
       reviewers = Maps.newLinkedHashMap();
       submitRecords = Lists.newArrayListWithExpectedSize(1);
+      changeMessages = LinkedListMultimap.create();
+      commentsForPs = ArrayListMultimap.create();
+      commentsForBase = ArrayListMultimap.create();
     }
 
-    private void parseAll() throws ConfigInvalidException, IOException {
+    private void parseAll() throws ConfigInvalidException, IOException, ParseException {
       walk.markStart(walk.parseCommit(tip));
       for (RevCommit commit : walk) {
         parse(commit);
       }
+      parseComments();
       pruneReviewers();
     }
 
@@ -136,12 +201,21 @@
       return ImmutableListMultimap.copyOf(result);
     }
 
-    private void parse(RevCommit commit) throws ConfigInvalidException {
+    private ImmutableListMultimap<PatchSet.Id, ChangeMessage> buildMessages() {
+      for (Collection<ChangeMessage> v : changeMessages.asMap().values()) {
+        Collections.sort((List<ChangeMessage>) v, MESSAGE_BY_TIME);
+      }
+      return ImmutableListMultimap.copyOf(changeMessages);
+    }
+
+    private void parse(RevCommit commit) throws ConfigInvalidException, IOException {
       if (status == null) {
         status = parseStatus(commit);
       }
       PatchSet.Id psId = parsePatchSetId(commit);
       Account.Id accountId = parseIdent(commit);
+      parseChangeMessage(psId, accountId, commit);
+
 
       if (submitRecords.isEmpty()) {
         // Only parse the most recent set of submit records; any older ones are
@@ -189,6 +263,70 @@
       return new PatchSet.Id(changeId, psId);
     }
 
+    private void parseChangeMessage(PatchSet.Id psId, Account.Id accountId,
+        RevCommit commit) {
+      byte[] raw = commit.getRawBuffer();
+      int size = raw.length;
+      Charset enc = RawParseUtils.parseEncoding(raw);
+
+      int subjectStart = RawParseUtils.commitMessage(raw, 0);
+      if (subjectStart < 0 || subjectStart >= size) {
+        return;
+      }
+
+      int subjectEnd = RawParseUtils.endOfParagraph(raw, subjectStart);
+      if (subjectEnd == size) {
+        return;
+      }
+
+      int changeMessageStart;
+
+      if (raw[subjectEnd] == '\n') {
+        changeMessageStart = subjectEnd + 2; //\n\n ends paragraph
+      } else if (raw[subjectEnd] == '\r') {
+        changeMessageStart = subjectEnd + 4; //\r\n\r\n ends paragraph
+      } else {
+        return;
+      }
+
+      int ptr = size - 1;
+      int changeMessageEnd = -1;
+      while(ptr > changeMessageStart) {
+        ptr = RawParseUtils.prevLF(raw, ptr, '\r');
+        if (ptr == -1) {
+          break;
+        }
+        if (raw[ptr] == '\n') {
+          changeMessageEnd = ptr - 1;
+          break;
+        } else if (raw[ptr] == '\r') {
+          changeMessageEnd = ptr - 3;
+          break;
+        }
+      }
+
+      if (ptr <= changeMessageStart) {
+        return;
+      }
+
+      String changeMsgString = RawParseUtils.decode(enc, raw,
+          changeMessageStart, changeMessageEnd + 1);
+      ChangeMessage changeMessage = new ChangeMessage(
+          new ChangeMessage.Key(psId.getParentKey(), commit.name()),
+          accountId,
+          new Timestamp(commit.getCommitterIdent().getWhen().getTime()),
+          psId);
+      changeMessage.setMessage(changeMsgString);
+      changeMessages.put(psId, changeMessage);
+    }
+
+    private void parseComments()
+        throws IOException, ConfigInvalidException, ParseException {
+      commentNoteMap = CommentsInNotesUtil.parseCommentsFromNotes(repo,
+          ChangeNoteUtil.changeRefName(changeId), walk, changeId,
+          commentsForBase, commentsForPs, Status.PUBLISHED);
+    }
+
     private void parseApproval(PatchSet.Id psId, Account.Id accountId,
         RevCommit commit, String line) throws ConfigInvalidException {
       Table<Account.Id, String, Optional<PatchSetApproval>> curr =
@@ -323,11 +461,6 @@
       }
     }
 
-    private ConfigInvalidException parseException(String fmt, Object... args) {
-      return new ConfigInvalidException("Change " + changeId + ": "
-          + String.format(fmt, args));
-    }
-
     private ConfigInvalidException expectedOneFooter(FooterKey footer,
         List<String> actual) {
       return parseException("missing or multiple %s: %s",
@@ -345,48 +478,23 @@
         throw invalidFooter(footer, actual);
       }
     }
+
+    private ConfigInvalidException parseException(String fmt, Object... args) {
+      return ChangeNotes.parseException(changeId, fmt, args);
+    }
   }
 
-  private final GitRepositoryManager repoManager;
-  private final Change change;
-  private boolean loaded;
   private ImmutableListMultimap<PatchSet.Id, PatchSetApproval> approvals;
   private ImmutableSetMultimap<ReviewerState, Account.Id> reviewers;
   private ImmutableList<SubmitRecord> submitRecords;
+  private ImmutableListMultimap<PatchSet.Id, ChangeMessage> changeMessages;
+  private ImmutableListMultimap<PatchSet.Id, PatchLineComment> commentsForBase;
+  private ImmutableListMultimap<PatchSet.Id, PatchLineComment> commentsForPS;
+  NoteMap noteMap;
 
   @VisibleForTesting
-  ChangeNotes(GitRepositoryManager repoManager, Change change) {
-    this.repoManager = repoManager;
-    this.change = new Change(change);
-  }
-
-  // TODO(dborowitz): Wrap fewer exceptions if/when we kill gwtorm.
-  public ChangeNotes load() throws OrmException {
-    if (!loaded) {
-      Repository repo;
-      try {
-        repo = repoManager.openRepository(change.getProject());
-      } catch (IOException e) {
-        throw new OrmException(e);
-      }
-      try {
-        load(repo);
-        loaded = true;
-      } catch (ConfigInvalidException | IOException e) {
-        throw new OrmException(e);
-      } finally {
-        repo.close();
-      }
-    }
-    return this;
-  }
-
-  public Change.Id getChangeId() {
-    return change.getId();
-  }
-
-  public Change getChange() {
-    return change;
+  public ChangeNotes(GitRepositoryManager repoManager, Change change) {
+    super(repoManager, change);
   }
 
   public ImmutableListMultimap<PatchSet.Id, PatchSetApproval> getApprovals() {
@@ -405,26 +513,54 @@
     return submitRecords;
   }
 
+  /** @return change messages by patch set, in chronological order. */
+  public ImmutableListMultimap<PatchSet.Id, ChangeMessage> getChangeMessages() {
+    return changeMessages;
+  }
+
+  /** @return inline comments on each patchset's base (side == 0). */
+  public ImmutableListMultimap<PatchSet.Id, PatchLineComment>
+      getBaseComments() {
+    return commentsForBase;
+  }
+
+  /** @return inline comments on each patchset (side == 1). */
+  public ImmutableListMultimap<PatchSet.Id, PatchLineComment>
+      getPatchSetComments() {
+    return commentsForPS;
+  }
+
+  /** @return the NoteMap */
+  NoteMap getNoteMap() {
+    return noteMap;
+  }
+
   @Override
   protected String getRefName() {
-    return ChangeNoteUtil.changeRefName(change.getId());
+    return ChangeNoteUtil.changeRefName(getChangeId());
   }
 
   @Override
   protected void onLoad() throws IOException, ConfigInvalidException {
     ObjectId rev = getRevision();
     if (rev == null) {
+      loadDefaults();
       return;
     }
     RevWalk walk = new RevWalk(reader);
     try {
-      Parser parser = new Parser(change.getId(), rev, walk);
+      Change change = getChange();
+      Parser parser = new Parser(change, rev, walk, repoManager);
       parser.parseAll();
 
       if (parser.status != null) {
         change.setStatus(parser.status);
       }
       approvals = parser.buildApprovals();
+      changeMessages = parser.buildMessages();
+      commentsForBase = ImmutableListMultimap.copyOf(parser.commentsForBase);
+      commentsForPS = ImmutableListMultimap.copyOf(parser.commentsForPs);
+      noteMap = parser.commentNoteMap;
 
       ImmutableSetMultimap.Builder<ReviewerState, Account.Id> reviewers =
           ImmutableSetMultimap.builder();
@@ -433,15 +569,37 @@
         reviewers.put(e.getValue(), e.getKey());
       }
       this.reviewers = reviewers.build();
+
       submitRecords = ImmutableList.copyOf(parser.submitRecords);
+    } catch (ParseException e1) {
+      // TODO(yyonas): figure out how to handle this exception
+      throw new IOException(e1);
     } finally {
       walk.close();
     }
   }
 
+  private void loadDefaults() {
+    approvals = ImmutableListMultimap.of();
+    reviewers = ImmutableSetMultimap.of();
+    submitRecords = ImmutableList.of();
+    changeMessages = ImmutableListMultimap.of();
+    commentsForBase = ImmutableListMultimap.of();
+    commentsForPS = ImmutableListMultimap.of();
+  }
+
   @Override
   protected boolean onSave(CommitBuilder commit) {
     throw new UnsupportedOperationException(
         getClass().getSimpleName() + " is read-only");
   }
+
+  private static Project.NameKey getProjectName(Change change) {
+    return change.getProject();
+  }
+
+  @Override
+  protected Project.NameKey getProjectName() {
+    return getProjectName(getChange());
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index 357d635..be3a44d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -19,15 +19,18 @@
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_STATUS;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBMITTED_WITH;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.GERRIT_PLACEHOLDER_HOST;
+import static com.google.gerrit.server.notedb.CommentsInNotesUtil.getCommentPsId;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Optional;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.GerritPersonIdent;
@@ -35,22 +38,22 @@
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.git.VersionedMetaData;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.util.LabelVote;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 
-import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.notes.NoteMap;
 import org.eclipse.jgit.revwalk.FooterKey;
 import org.eclipse.jgit.revwalk.RevCommit;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.Date;
 import java.util.List;
@@ -66,7 +69,7 @@
  * <p>
  * This class is not thread-safe.
  */
-public class ChangeUpdate extends VersionedMetaData {
+public class ChangeUpdate extends AbstractChangeUpdate {
   public interface Factory {
     ChangeUpdate create(ChangeControl ctl);
     ChangeUpdate create(ChangeControl ctl, Date when);
@@ -75,19 +78,16 @@
         Comparator<String> labelNameComparator);
   }
 
-  private final NotesMigration migration;
-  private final GitRepositoryManager repoManager;
   private final AccountCache accountCache;
-  private final MetaDataUpdate.User updateFactory;
-  private final ChangeControl ctl;
-  private final PersonIdent serverIdent;
-  private final Date when;
   private final Map<String, Optional<Short>> approvals;
   private final Map<Account.Id, ReviewerState> reviewers;
   private Change.Status status;
   private String subject;
-  private PatchSet.Id psId;
   private List<SubmitRecord> submitRecords;
+  private final CommentsInNotesUtil commentsUtil;
+  private List<PatchLineComment> commentsForBase;
+  private List<PatchLineComment> commentsForPs;
+  private String changeMessage;
 
   @AssistedInject
   private ChangeUpdate(
@@ -98,9 +98,10 @@
       MetaDataUpdate.User updateFactory,
       ProjectCache projectCache,
       IdentifiedUser user,
-      @Assisted ChangeControl ctl) {
+      @Assisted ChangeControl ctl,
+      CommentsInNotesUtil commentsUtil) {
     this(serverIdent, repoManager, migration, accountCache, updateFactory,
-        projectCache, ctl, serverIdent.getWhen());
+        projectCache, ctl, serverIdent.getWhen(), commentsUtil);
   }
 
   @AssistedInject
@@ -112,10 +113,12 @@
       MetaDataUpdate.User updateFactory,
       ProjectCache projectCache,
       @Assisted ChangeControl ctl,
-      @Assisted Date when) {
-    this(serverIdent, repoManager, migration, accountCache, updateFactory,
-        ctl, when,
-        projectCache.get(getProjectName(ctl)).getLabelTypes().nameComparator());
+      @Assisted Date when,
+      CommentsInNotesUtil commentsUtil) {
+    this(serverIdent, repoManager, migration, accountCache, updateFactory, ctl,
+        when,
+        projectCache.get(getProjectName(ctl)).getLabelTypes().nameComparator(),
+        commentsUtil);
   }
 
   private static Project.NameKey getProjectName(ChangeControl ctl) {
@@ -131,28 +134,15 @@
       MetaDataUpdate.User updateFactory,
       @Assisted ChangeControl ctl,
       @Assisted Date when,
-      @Assisted Comparator<String> labelNameComparator) {
-    this.repoManager = repoManager;
-    this.migration = migration;
+      @Assisted Comparator<String> labelNameComparator,
+      CommentsInNotesUtil commentsUtil) {
+    super(migration, repoManager, updateFactory, ctl, serverIdent, when);
     this.accountCache = accountCache;
-    this.updateFactory = updateFactory;
-    this.ctl = ctl;
-    this.when = when;
-    this.serverIdent = serverIdent;
+    this.commentsUtil = commentsUtil;
     this.approvals = Maps.newTreeMap(labelNameComparator);
     this.reviewers = Maps.newLinkedHashMap();
-  }
-
-  public Change getChange() {
-    return ctl.getChange();
-  }
-
-  public IdentifiedUser getUser() {
-    return (IdentifiedUser) ctl.getCurrentUser();
-  }
-
-  public Date getWhen() {
-    return when;
+    this.commentsForPs = Lists.newArrayList();
+    this.commentsForBase = Lists.newArrayList();
   }
 
   public void setStatus(Change.Status status) {
@@ -180,10 +170,22 @@
     this.subject = subject;
   }
 
-  public void setPatchSetId(PatchSet.Id psId) {
-    checkArgument(psId == null
-        || psId.getParentKey().equals(getChange().getId()));
-    this.psId = psId;
+  public void setChangeMessage(String changeMessage) {
+    this.changeMessage = changeMessage;
+  }
+
+  public void putComment(PatchLineComment comment) {
+    checkArgument(psId != null,
+        "setPatchSetId must be called before putComment");
+    checkArgument(getCommentPsId(comment).equals(psId),
+        "Comment on %s doesn't match previous patch set %s",
+        getCommentPsId(comment), psId);
+    checkArgument(comment.getRevId() != null);
+    if (comment.getSide() == 0) {
+      commentsForBase.add(comment);
+    } else {
+      commentsForPs.add(comment);
+    }
   }
 
   public void putReviewer(Account.Id reviewer, ReviewerState type) {
@@ -195,87 +197,61 @@
     reviewers.put(reviewer, ReviewerState.REMOVED);
   }
 
-  private void load() throws IOException {
-    if (migration.write() && getRevision() == null) {
-      Repository repo = repoManager.openRepository(getChange().getProject());
-      try {
-        load(repo);
-      } catch (ConfigInvalidException e) {
-        throw new IOException(e);
-      } finally {
-        repo.close();
-      }
+  /** @return the tree id for the updated tree */
+  private ObjectId storeCommentsInNotes() throws OrmException, IOException {
+    ChangeNotes notes = ctl.getNotes().load();
+    NoteMap noteMap = notes.getNoteMap();
+    if (noteMap == null) {
+      noteMap = NoteMap.newEmptyMap();
     }
-  }
+    if (commentsForPs.isEmpty() && commentsForBase.isEmpty()) {
+      return null;
+    }
 
-  @Override
-  public RevCommit commit(MetaDataUpdate md) throws IOException {
-    throw new UnsupportedOperationException("use commit()");
+    Multimap<PatchSet.Id, PatchLineComment> allCommentsOnBases =
+        notes.getBaseComments();
+    Multimap<PatchSet.Id, PatchLineComment> allCommentsOnPs =
+        notes.getPatchSetComments();
+
+    // This writes all comments for the base of this PS to the note map.
+    if (!commentsForBase.isEmpty()) {
+      List<PatchLineComment> baseCommentsForThisPs =
+          new ArrayList<>(allCommentsOnBases.get(psId));
+      baseCommentsForThisPs.addAll(commentsForBase);
+      commentsUtil.writeCommentsToNoteMap(noteMap, baseCommentsForThisPs,
+          inserter);
+    }
+
+    // This write all comments for this PS to the note map.
+    if (!commentsForPs.isEmpty()) {
+      List<PatchLineComment> commentsForThisPs =
+          new ArrayList<>(allCommentsOnPs.get(psId));
+      commentsForThisPs.addAll(commentsForPs);
+      commentsUtil.writeCommentsToNoteMap(noteMap, commentsForThisPs, inserter);
+    }
+    return noteMap.writeTree(inserter);
   }
 
   public RevCommit commit() throws IOException {
     BatchMetaDataUpdate batch = openUpdate();
     try {
-      batch.write(new CommitBuilder());
-      return batch.commit();
+      CommitBuilder builder = new CommitBuilder();
+      if (migration.write()) {
+        ObjectId treeId = storeCommentsInNotes();
+        if (treeId != null) {
+          builder.setTreeId(treeId);
+        }
+      }
+      batch.write(builder);
+      RevCommit c = batch.commit();
+      return c;
+    } catch (OrmException e) {
+      throw new IOException(e);
     } finally {
       batch.close();
     }
   }
 
-  private PersonIdent newIdent(Account author, Date when) {
-    return new PersonIdent(
-        author.getFullName(),
-        author.getId().get() + "@" + GERRIT_PLACEHOLDER_HOST,
-        when, serverIdent.getTimeZone());
-  }
-
-  @Override
-  public BatchMetaDataUpdate openUpdate(MetaDataUpdate update) throws IOException {
-    throw new UnsupportedOperationException("use openUpdate()");
-  }
-
-  public BatchMetaDataUpdate openUpdate() throws IOException {
-    if (migration.write()) {
-      load();
-      MetaDataUpdate md =
-          updateFactory.create(getChange().getProject(), getUser());
-      md.setAllowEmpty(true);
-      return super.openUpdate(md);
-    }
-    return new BatchMetaDataUpdate() {
-      @Override
-      public void write(CommitBuilder commit) {
-        // Do nothing.
-      }
-
-      @Override
-      public void write(VersionedMetaData config, CommitBuilder commit) {
-        // Do nothing.
-      }
-
-      @Override
-      public RevCommit createRef(String refName) {
-        return null;
-      }
-
-      @Override
-      public RevCommit commit() {
-        return null;
-      }
-
-      @Override
-      public RevCommit commitAt(ObjectId revision) {
-        return null;
-      }
-
-      @Override
-      public void close() {
-        // Do nothing.
-      }
-    };
-  }
-
   @Override
   protected String getRefName() {
     return ChangeNoteUtil.changeRefName(getChange().getId());
@@ -297,6 +273,13 @@
       msg.append("Update patch set ").append(ps);
     }
     msg.append("\n\n");
+
+    if (changeMessage != null) {
+      msg.append(changeMessage);
+      msg.append("\n\n");
+    }
+
+
     addFooter(msg, FOOTER_PATCH_SET, ps);
     if (status != null) {
       addFooter(msg, FOOTER_STATUS, status.name().toLowerCase());
@@ -348,11 +331,19 @@
     return true;
   }
 
+  @Override
+  protected Project.NameKey getProjectName() {
+    return getProjectName(ctl);
+  }
+
   private boolean isEmpty() {
     return approvals.isEmpty()
         && reviewers.isEmpty()
+        && commentsForBase.isEmpty()
+        && commentsForPs.isEmpty()
         && status == null
-        && submitRecords == null;
+        && submitRecords == null
+        && changeMessage == null;
   }
 
   private static StringBuilder addFooter(StringBuilder sb, FooterKey footer) {
@@ -371,9 +362,4 @@
   private static String sanitizeFooter(String value) {
     return value.replace('\n', ' ').replace('\0', ' ');
   }
-
-  @Override
-  protected void onLoad() throws IOException, ConfigInvalidException {
-    // Do nothing; just reads current revision.
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/CommentsInNotesUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/CommentsInNotesUtil.java
new file mode 100644
index 0000000..ede979fe
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/CommentsInNotesUtil.java
@@ -0,0 +1,535 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.notedb;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.gerrit.server.notedb.ChangeNoteUtil.GERRIT_PLACEHOLDER_HOST;
+import static com.google.gerrit.server.notedb.ChangeNotes.parseException;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Multimap;
+import com.google.common.primitives.Ints;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.CommentRange;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.notes.Note;
+import org.eclipse.jgit.notes.NoteMap;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.util.GitDateFormatter;
+import org.eclipse.jgit.util.GitDateParser;
+import org.eclipse.jgit.util.MutableInteger;
+import org.eclipse.jgit.util.QuotedString;
+import org.eclipse.jgit.util.RawParseUtils;
+import org.eclipse.jgit.util.GitDateFormatter.Format;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.nio.charset.Charset;
+import java.sql.Timestamp;
+import java.text.ParseException;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Utility functions to parse PatchLineComments out of a note byte array and
+ * store a list of PatchLineComments in the form of a note (in a byte array).
+ **/
+public class CommentsInNotesUtil {
+  private static final String AUTHOR = "Author";
+  private static final String BASE_PATCH_SET = "Base-for-patch-set";
+  private static final String COMMENT_RANGE = "Comment-range";
+  private static final String FILE = "File";
+  private static final String LENGTH = "Bytes";
+  private static final String PARENT = "Parent";
+  private static final String PATCH_SET = "Patch-set";
+  private static final String REVISION = "Revision";
+  private static final String UUID = "UUID";
+
+  public static NoteMap parseCommentsFromNotes(Repository repo, String refName,
+      RevWalk walk, Change.Id changeId,
+      Multimap<PatchSet.Id, PatchLineComment> commentsForBase,
+      Multimap<PatchSet.Id, PatchLineComment> commentsForPs,
+      Status status)
+      throws IOException, ConfigInvalidException {
+    Ref ref = repo.getRef(refName);
+    if (ref == null) {
+      return null;
+    }
+    RevCommit commit = walk.parseCommit(ref.getObjectId());
+    NoteMap noteMap = NoteMap.read(walk.getObjectReader(), commit);
+
+    for (Note note: noteMap) {
+      byte[] bytes = walk.getObjectReader().open(
+          note.getData(), Constants.OBJ_BLOB).getBytes();
+      List<PatchLineComment> result = parseNote(bytes, changeId, status);
+      if ((result == null) || (result.isEmpty())) {
+        continue;
+      }
+      PatchSet.Id psId = result.get(0).getKey().getParentKey().getParentKey();
+      short side = result.get(0).getSide();
+      if (side == 0) {
+        commentsForBase.putAll(psId, result);
+      } else {
+        commentsForPs.putAll(psId, result);
+      }
+    }
+    return noteMap;
+  }
+
+  public static List<PatchLineComment> parseNote(byte[] note,
+      Change.Id changeId, Status status) throws ConfigInvalidException {
+    List<PatchLineComment> result = Lists.newArrayList();
+    int sizeOfNote = note.length;
+    Charset enc = RawParseUtils.parseEncoding(note);
+    MutableInteger curr = new MutableInteger();
+    curr.value = 0;
+
+    boolean isForBase =
+        (RawParseUtils.match(note, curr.value, PATCH_SET.getBytes(UTF_8))) < 0;
+
+    PatchSet.Id psId = parsePsId(note, curr, changeId, enc,
+        isForBase ? BASE_PATCH_SET : PATCH_SET);
+
+    RevId revId =
+        new RevId(parseStringField(note, curr, changeId, enc, REVISION));
+
+    PatchLineComment c = null;
+    while (curr.value < sizeOfNote) {
+      String previousFileName = c == null ?
+          null : c.getKey().getParentKey().getFileName();
+      c = parseComment(note, curr, previousFileName, psId, revId,
+          isForBase, enc, status);
+      result.add(c);
+    }
+    return result;
+  }
+
+  public static String formatTime(PersonIdent ident, Timestamp t) {
+    GitDateFormatter dateFormatter = new GitDateFormatter(Format.DEFAULT);
+    // TODO(dborowitz): Use a ThreadLocal or use Joda.
+    PersonIdent newIdent = new PersonIdent(ident, t);
+    return dateFormatter.formatDate(newIdent);
+  }
+
+  public static PatchSet.Id getCommentPsId(PatchLineComment plc) {
+    return plc.getKey().getParentKey().getParentKey();
+  }
+
+  private static PatchLineComment parseComment(byte[] note, MutableInteger curr,
+      String currentFileName, PatchSet.Id psId, RevId revId, boolean isForBase,
+      Charset enc, Status status)
+          throws ConfigInvalidException {
+    Change.Id changeId = psId.getParentKey();
+
+    // Check if there is a new file.
+    boolean newFile =
+        (RawParseUtils.match(note, curr.value, FILE.getBytes(UTF_8))) != -1;
+    if (newFile) {
+      // If so, parse the new file name.
+      currentFileName = parseFilename(note, curr, changeId, enc);
+    } else if (currentFileName == null) {
+      throw parseException(changeId, "could not parse %s", FILE);
+    }
+
+    CommentRange range = parseCommentRange(note, curr, changeId);
+    if (range == null) {
+      throw parseException(changeId, "could not parse %s", COMMENT_RANGE);
+    }
+
+    Timestamp commentTime = parseTimestamp(note, curr, changeId, enc);
+    Account.Id aId = parseAuthor(note, curr, changeId, enc);
+
+    boolean hasParent =
+        (RawParseUtils.match(note, curr.value, PARENT.getBytes(enc))) != -1;
+    String parentUUID = null;
+    if (hasParent) {
+      parentUUID = parseStringField(note, curr, changeId, enc, PARENT);
+    }
+
+    String uuid = parseStringField(note, curr, changeId, enc, UUID);
+    int commentLength = parseCommentLength(note, curr, changeId, enc);
+
+    String message = RawParseUtils.decode(
+        enc, note, curr.value, curr.value + commentLength);
+    checkResult(message, "message contents", changeId);
+
+    PatchLineComment plc = new PatchLineComment(
+        new PatchLineComment.Key(new Patch.Key(psId, currentFileName), uuid),
+        range.getEndLine(), aId, parentUUID, commentTime);
+    plc.setMessage(message);
+    plc.setSide((short) (isForBase ? 0 : 1));
+    if (range.getStartCharacter() != -1) {
+      plc.setRange(range);
+    }
+    plc.setRevId(revId);
+    plc.setStatus(status);
+
+    curr.value = RawParseUtils.nextLF(note, curr.value + commentLength);
+    curr.value = RawParseUtils.nextLF(note, curr.value);
+    return plc;
+  }
+
+  private static String parseStringField(byte[] note, MutableInteger curr,
+      Change.Id changeId, Charset enc, String fieldName)
+      throws ConfigInvalidException {
+    int endOfLine = RawParseUtils.nextLF(note, curr.value);
+    checkHeaderLineFormat(note, curr, fieldName, enc, changeId);
+    int startOfField = RawParseUtils.endOfFooterLineKey(note, curr.value) + 2;
+    curr.value = endOfLine;
+    return RawParseUtils.decode(enc, note, startOfField, endOfLine - 1);
+  }
+
+  /**
+   * @return a comment range. If the comment range line in the note only has
+   *    one number, we return a CommentRange with that one number as the end
+   *    line and the other fields as -1. If the comment range line in the note
+   *    contains a whole comment range, then we return a CommentRange with all
+   *    fields set. If the line is not correctly formatted, return null.
+   */
+  private static CommentRange parseCommentRange(byte[] note, MutableInteger ptr,
+      Change.Id changeId) throws ConfigInvalidException {
+    CommentRange range = new CommentRange(-1, -1, -1, -1);
+
+    int startLine = RawParseUtils.parseBase10(note, ptr.value, ptr);
+    if (startLine == 0) {
+      return null;
+    }
+
+    if (note[ptr.value] == '\n') {
+      range.setEndLine(startLine);
+      return range;
+    } else if (note[ptr.value] == ':') {
+      range.setStartLine(startLine);
+      ptr.value += 1;
+    } else {
+      return null;
+    }
+
+    int startChar = RawParseUtils.parseBase10(note, ptr.value, ptr);
+    if (startChar == 0) {
+      return null;
+    }
+    if (note[ptr.value] == '-') {
+      range.setStartCharacter(startChar);
+      ptr.value += 1;
+    } else {
+      return null;
+    }
+
+    int endLine = RawParseUtils.parseBase10(note, ptr.value, ptr);
+    if (endLine == 0) {
+      return null;
+    }
+    if (note[ptr.value] == ':') {
+      range.setEndLine(endLine);
+      ptr.value += 1;
+    } else {
+      return null;
+    }
+
+    int endChar = RawParseUtils.parseBase10(note, ptr.value, ptr);
+    if (endChar == 0) {
+      return null;
+    }
+    if (note[ptr.value] == '\n') {
+      range.setEndCharacter(endChar);
+      ptr.value += 1;
+    } else {
+      return null;
+    }
+    return range;
+  }
+
+  private static PatchSet.Id parsePsId(byte[] note, MutableInteger curr,
+      Change.Id changeId, Charset enc, String fieldName)
+      throws ConfigInvalidException {
+    checkHeaderLineFormat(note, curr, fieldName, enc, changeId);
+    int startOfPsId =
+        RawParseUtils.endOfFooterLineKey(note, curr.value) + 1;
+    MutableInteger i = new MutableInteger();
+    int patchSetId =
+        RawParseUtils.parseBase10(note, startOfPsId, i);
+    int endOfLine = RawParseUtils.nextLF(note, curr.value);
+    if (i.value != endOfLine - 1) {
+      throw parseException(changeId, "could not parse %s", fieldName);
+    }
+    checkResult(patchSetId, "patchset id", changeId);
+    curr.value = endOfLine;
+    return new PatchSet.Id(changeId, patchSetId);
+  }
+
+  private static String parseFilename(byte[] note, MutableInteger curr,
+      Change.Id changeId, Charset enc) throws ConfigInvalidException {
+    checkHeaderLineFormat(note, curr, FILE, enc, changeId);
+    int startOfFileName =
+        RawParseUtils.endOfFooterLineKey(note, curr.value) + 2;
+    int endOfLine = RawParseUtils.nextLF(note, curr.value);
+    curr.value = endOfLine;
+    curr.value = RawParseUtils.nextLF(note, curr.value);
+    return QuotedString.GIT_PATH.dequote(
+        RawParseUtils.decode(enc, note, startOfFileName, endOfLine - 1));
+  }
+
+  private static Timestamp parseTimestamp(byte[] note, MutableInteger curr,
+      Change.Id changeId, Charset enc)
+      throws ConfigInvalidException {
+    int endOfLine = RawParseUtils.nextLF(note, curr.value);
+    Timestamp commentTime;
+    String dateString =
+        RawParseUtils.decode(enc, note, curr.value, endOfLine - 1);
+    try {
+      commentTime =
+          new Timestamp(GitDateParser.parse(dateString, null).getTime());
+    } catch (ParseException e) {
+      throw new ConfigInvalidException("could not parse comment timestamp", e);
+    }
+    curr.value = endOfLine;
+    return checkResult(commentTime, "comment timestamp", changeId);
+  }
+
+  private static Account.Id parseAuthor(byte[] note, MutableInteger curr,
+      Change.Id changeId, Charset enc) throws ConfigInvalidException {
+    checkHeaderLineFormat(note, curr, AUTHOR, enc, changeId);
+    int startOfAccountId =
+        RawParseUtils.endOfFooterLineKey(note, curr.value) + 2;
+    PersonIdent ident =
+        RawParseUtils.parsePersonIdent(note, startOfAccountId);
+    Account.Id aId = parseIdent(ident, changeId);
+    curr.value = RawParseUtils.nextLF(note, curr.value);
+    return checkResult(aId, "comment author", changeId);
+  }
+
+  private static int parseCommentLength(byte[] note, MutableInteger curr,
+      Change.Id changeId, Charset enc) throws ConfigInvalidException {
+    checkHeaderLineFormat(note, curr, LENGTH, enc, changeId);
+    int startOfLength =
+        RawParseUtils.endOfFooterLineKey(note, curr.value) + 1;
+    MutableInteger i = new MutableInteger();
+    int commentLength =
+        RawParseUtils.parseBase10(note, startOfLength, i);
+    int endOfLine = RawParseUtils.nextLF(note, curr.value);
+    if (i.value != endOfLine-1) {
+      throw parseException(changeId, "could not parse %s", PATCH_SET);
+    }
+    curr.value = endOfLine;
+    return checkResult(commentLength, "comment length", changeId);
+  }
+
+  private static <T> T checkResult(T o, String fieldName,
+      Change.Id changeId) throws ConfigInvalidException {
+    if (o == null) {
+      throw parseException(changeId, "could not parse %s", fieldName);
+    }
+    return o;
+  }
+
+  private static int checkResult(int i, String fieldName, Change.Id changeId)
+      throws ConfigInvalidException {
+    if (i <= 0) {
+      throw parseException(changeId, "could not parse %s", fieldName);
+    }
+    return i;
+  }
+
+  private PersonIdent newIdent(Account author, Date when) {
+    return new PersonIdent(
+        author.getFullName(),
+        author.getId().get() + "@" + GERRIT_PLACEHOLDER_HOST,
+        when, serverIdent.getTimeZone());
+  }
+
+  private static Account.Id parseIdent(PersonIdent ident, Change.Id changeId)
+      throws ConfigInvalidException {
+    String email = ident.getEmailAddress();
+    int at = email.indexOf('@');
+    if (at >= 0) {
+      String host = email.substring(at + 1, email.length());
+      Integer id = Ints.tryParse(email.substring(0, at));
+      if (id != null && host.equals(GERRIT_PLACEHOLDER_HOST)) {
+        return new Account.Id(id);
+      }
+    }
+    throw parseException(changeId, "invalid identity, expected <id>@%s: %s",
+      GERRIT_PLACEHOLDER_HOST, email);
+  }
+
+  private void appendHeaderField(PrintWriter writer,
+      String field, String value) throws IOException {
+    writer.print(field);
+    writer.print(": ");
+    writer.print(value);
+    writer.print('\n');
+  }
+
+  private static void checkHeaderLineFormat(byte[] note, MutableInteger curr,
+      String fieldName, Charset enc, Change.Id changeId)
+      throws ConfigInvalidException {
+    boolean correct =
+        RawParseUtils.match(note, curr.value, fieldName.getBytes(enc)) != -1;
+    correct &= (note[curr.value + fieldName.length()] == ':');
+    correct &= (note[curr.value + fieldName.length() + 1] == ' ');
+    if (!correct) {
+      throw parseException(changeId, "could not parse %s", fieldName);
+    }
+  }
+
+  private final AccountCache accountCache;
+  private final PersonIdent serverIdent;
+
+  @VisibleForTesting
+  @Inject
+  public CommentsInNotesUtil(AccountCache accountCache,
+      @GerritPersonIdent PersonIdent serverIdent) {
+    this.accountCache = accountCache;
+    this.serverIdent = serverIdent;
+  }
+
+  /**
+   * Build a note that contains the metadata for and the contents of all of the
+   * comments in the given list of comments.
+   *
+   * @param comments
+   *            A list of the comments to be written to the returned note
+   *            byte array.
+   *            All of the comments in this list must have the same side and
+   *            must share the same PatchSet.Id.
+   *            This list must not be empty because we cannot build a note
+   *            for no comments.
+   * @return the note. Null if there are no comments in the list.
+   */
+  public byte[] buildNote(List<PatchLineComment> comments)
+      throws OrmException, IOException {
+    ByteArrayOutputStream buf = new ByteArrayOutputStream();
+    OutputStreamWriter streamWriter = new OutputStreamWriter(buf, UTF_8);
+    PrintWriter writer = new PrintWriter(streamWriter);
+    PatchLineComment first = comments.get(0);
+
+    short side = first.getSide();
+    PatchSet.Id psId = getCommentPsId(first);
+    appendHeaderField(writer, side == 0
+        ? BASE_PATCH_SET
+        : PATCH_SET,
+        Integer.toString(psId.get()));
+    appendHeaderField(writer, REVISION, first.getRevId().get());
+
+    String currentFilename = null;
+
+    for (PatchLineComment c : comments) {
+      PatchSet.Id currentPsId = getCommentPsId(c);
+      checkArgument(psId.equals(currentPsId),
+          "All comments being added must all have the same PatchSet.Id. The"
+          + "comment below does not have the same PatchSet.Id as the others "
+          + "(%d).\n%s", psId.toString(), c.toString());
+      checkArgument(side == c.getSide(),
+          "All comments being added must all have the same side. The"
+          + "comment below does not have the same side as the others "
+          + "(%d).\n%s", side, c.toString());
+      String commentFilename =
+          QuotedString.GIT_PATH.quote(c.getKey().getParentKey().getFileName());
+
+      if (!commentFilename.equals(currentFilename)) {
+        currentFilename = commentFilename;
+        writer.print("File: ");
+        writer.print(commentFilename);
+        writer.print("\n\n");
+      }
+
+      // The CommentRange field for a comment is allowed to be null.
+      // If it is indeed null, then in the first line, we simply use the line
+      // number field for a comment instead. If it isn't null, we write the
+      // comment range itself.
+      CommentRange range = c.getRange();
+      if (range != null) {
+        writer.print(range.getStartLine());
+        writer.print(':');
+        writer.print(range.getStartCharacter());
+        writer.print('-');
+        writer.print(range.getEndLine());
+        writer.print(':');
+        writer.print(range.getEndCharacter());
+      } else {
+        writer.print(c.getLine());
+      }
+      writer.print("\n");
+
+      writer.print(formatTime(serverIdent, c.getWrittenOn()));
+      writer.print("\n");
+
+      PersonIdent ident =
+          newIdent(accountCache.get(c.getAuthor()).getAccount(),
+              c.getWrittenOn());
+      String nameString = ident.getName() + " <" + ident.getEmailAddress()
+          + ">";
+      appendHeaderField(writer, AUTHOR, nameString);
+
+      String parent = c.getParentUuid();
+      if (parent != null) {
+        appendHeaderField(writer, PARENT, parent);
+      }
+
+      appendHeaderField(writer, UUID, c.getKey().get());
+
+      byte[] messageBytes = c.getMessage().getBytes(UTF_8);
+      appendHeaderField(writer, LENGTH,
+          Integer.toString(messageBytes.length));
+
+      writer.print(c.getMessage());
+      writer.print("\n\n");
+    }
+    writer.close();
+    return buf.toByteArray();
+  }
+
+  public void writeCommentsToNoteMap(NoteMap noteMap,
+      List<PatchLineComment> allComments, ObjectInserter inserter)
+        throws OrmException, IOException {
+    checkArgument(!allComments.isEmpty(),
+        "No comments to write; to delete, use removeNoteFromNoteMap().");
+    ObjectId commitOID =
+        ObjectId.fromString(allComments.get(0).getRevId().get());
+    Collections.sort(allComments, ChangeNotes.PatchLineCommentComparator);
+    byte[] note = buildNote(allComments);
+    ObjectId noteId = inserter.insert(Constants.OBJ_BLOB, note, 0, note.length);
+    noteMap.set(commitOID, noteId);
+  }
+
+  public void removeNote(NoteMap noteMap, RevId commitId)
+      throws IOException {
+    noteMap.remove(ObjectId.fromString(commitId.get()));
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java
index c9ac8de..36f685d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.server.notedb;
 
-import static com.google.common.base.Preconditions.checkArgument;
-
 import com.google.common.annotations.VisibleForTesting;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.Inject;
@@ -36,20 +34,26 @@
   static NotesMigration allEnabled() {
     Config cfg = new Config();
     cfg.setBoolean("notedb", null, "write", true);
-    //cfg.setBoolean("notedb", "patchSetApprovals", "read", true);
+    cfg.setBoolean("notedb", "patchSetApprovals", "read", true);
+    cfg.setBoolean("notedb", "changeMessages", "read", true);
+    cfg.setBoolean("notedb", "publishedComments", "read", true);
     return new NotesMigration(cfg);
   }
 
   private final boolean write;
   private final boolean readPatchSetApprovals;
+  private final boolean readChangeMessages;
+  private final boolean readPublishedComments;
 
   @Inject
   NotesMigration(@GerritServerConfig Config cfg) {
     write = cfg.getBoolean("notedb", null, "write", false);
     readPatchSetApprovals =
         cfg.getBoolean("notedb", "patchSetApprovals", "read", false);
-    checkArgument(!readPatchSetApprovals,
-        "notedb.readPatchSetApprovals not yet supported");
+    readChangeMessages =
+        cfg.getBoolean("notedb", "changeMessages", "read", false);
+    readPublishedComments =
+        cfg.getBoolean("notedb", "publishedComments", "read", false);
   }
 
   public boolean write() {
@@ -59,4 +63,12 @@
   public boolean readPatchSetApprovals() {
     return readPatchSetApprovals;
   }
+
+  public boolean readChangeMessages() {
+    return readChangeMessages;
+  }
+
+  public boolean readPublishedComments() {
+    return readPublishedComments;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/DiffExecutor.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/DiffExecutor.java
new file mode 100644
index 0000000..564ca58
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/DiffExecutor.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.patch;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.Retention;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * Marker on {@link ExecutorService} used by
+ * {@link IntraLineLoader} and {@link PatchListLoader}.
+ */
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface DiffExecutor {
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/DiffExecutorModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/DiffExecutorModule.java
new file mode 100644
index 0000000..9eaea3a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/DiffExecutorModule.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.patch;
+
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.google.inject.AbstractModule;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/** Module providing the {@link DiffExecutor}. */
+public class DiffExecutorModule extends AbstractModule {
+
+  @Override
+  protected void configure() {
+  }
+
+  @Provides
+  @Singleton
+  @DiffExecutor
+  public ExecutorService createDiffExecutor() {
+    return Executors.newCachedThreadPool(new ThreadFactoryBuilder()
+        .setNameFormat("Diff-%d").setDaemon(true).build());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
index 52cba09..009b492 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
@@ -15,6 +15,7 @@
 
 package com.google.gerrit.server.patch;
 
+import com.google.common.base.Throwables;
 import com.google.common.cache.CacheLoader;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -29,7 +30,12 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 import java.util.regex.Pattern;
 
 class IntraLineLoader extends CacheLoader<IntraLineDiffKey, IntraLineDiff> {
@@ -41,12 +47,13 @@
   private static final Pattern CONTROL_BLOCK_START_RE = Pattern
       .compile("[{:][ \\t]*$");
 
-  private final IntraLineWorkerPool workerPool;
+  private final ExecutorService diffExecutor;
   private final long timeoutMillis;
 
   @Inject
-  IntraLineLoader(IntraLineWorkerPool pool, @GerritServerConfig Config cfg) {
-    workerPool = pool;
+  IntraLineLoader(@DiffExecutor ExecutorService diffExecutor,
+      @GerritServerConfig Config cfg) {
+    this.diffExecutor = diffExecutor;
     timeoutMillis =
         ConfigUtil.getTimeUnit(cfg, "cache", PatchListCacheImpl.INTRA_NAME,
             "timeout", TimeUnit.MILLISECONDS.convert(5, TimeUnit.SECONDS),
@@ -54,31 +61,34 @@
   }
 
   @Override
-  public IntraLineDiff load(IntraLineDiffKey key) throws Exception {
-    IntraLineWorkerPool.Worker w = workerPool.acquire();
-    IntraLineWorkerPool.Worker.Result r = w.computeWithTimeout(key, timeoutMillis);
-
-    if (r == IntraLineWorkerPool.Worker.Result.TIMEOUT) {
-      // Don't keep this thread. We have to murder it unsafely, which
-      // means its unable to be reused in the future. Return back a
-      // null result, indicating the cache cannot load this key.
-      //
+  public IntraLineDiff load(final IntraLineDiffKey key) throws Exception {
+    Future<IntraLineDiff> result = diffExecutor.submit(new Callable<IntraLineDiff>() {
+      @Override
+      public IntraLineDiff call() throws Exception {
+        return IntraLineLoader.compute(key);
+      }
+    });
+    try {
+      return result.get(timeoutMillis, TimeUnit.MILLISECONDS);
+    } catch (InterruptedException | TimeoutException e) {
+      log.warn(timeoutMillis + " ms timeout reached for IntraLineDiff"
+          + " in project " + key.getProject().get()
+          + " on commit " + key.getCommit().name()
+          + " for path " + key.getPath()
+          + " comparing " + key.getBlobA().name()
+          + ".." + key.getBlobB().name());
+      result.cancel(true);
       return new IntraLineDiff(IntraLineDiff.Status.TIMEOUT);
-    }
-    workerPool.release(w);
-
-    if (r.error != null) {
+    } catch (ExecutionException e) {
       // If there was an error computing the result, carry it
       // up to the caller so the cache knows this key is invalid.
-      //
-      throw r.error;
+      Throwables.propagateIfInstanceOf(e.getCause(), Exception.class);
+      throw new Exception(e.getMessage(), e.getCause());
     }
-
-    return r.diff;
   }
 
   static IntraLineDiff compute(IntraLineDiffKey key) throws Exception {
-    List<Edit> edits = new ArrayList<Edit>(key.getEdits());
+    List<Edit> edits = new ArrayList<>(key.getEdits());
     Text aContent = key.getTextA();
     Text bContent = key.getTextB();
     combineLineEdits(edits, aContent, bContent);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineWorkerPool.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineWorkerPool.java
deleted file mode 100644
index 5c6338fe..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineWorkerPool.java
+++ /dev/null
@@ -1,182 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-
-package com.google.gerrit.server.patch;
-
-import static com.google.gerrit.server.patch.IntraLineLoader.log;
-
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.inject.AbstractModule;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-
-import org.eclipse.jgit.lib.Config;
-
-import java.util.concurrent.ArrayBlockingQueue;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
-
-@Singleton
-public class IntraLineWorkerPool {
-  public static class Module extends AbstractModule {
-    @Override
-    protected void configure() {
-      bind(IntraLineWorkerPool.class);
-    }
-  }
-
-  private final BlockingQueue<Worker> workerPool;
-
-  @Inject
-  public IntraLineWorkerPool(@GerritServerConfig Config cfg) {
-    int workers = cfg.getInt(
-        "cache", PatchListCacheImpl.INTRA_NAME, "maxIdleWorkers",
-        Runtime.getRuntime().availableProcessors() * 3 / 2);
-    workerPool = new ArrayBlockingQueue<Worker>(workers, true /* fair */);
-  }
-
-  Worker acquire() {
-    Worker w = workerPool.poll();
-    if (w == null) {
-      // If no worker is immediately available, start a new one.
-      // Maximum parallelism is controlled by the web server.
-      w = new Worker();
-      w.start();
-    }
-    return w;
-  }
-
-  void release(Worker w) {
-    if (!workerPool.offer(w)) {
-      // If the idle worker pool is full, terminate the worker.
-      w.shutdownGracefully();
-    }
-  }
-
-  static class Worker extends Thread {
-    private static final AtomicInteger count = new AtomicInteger(1);
-
-    private final ArrayBlockingQueue<Input> input;
-    private final ArrayBlockingQueue<Result> result;
-
-    Worker() {
-      input = new ArrayBlockingQueue<Input>(1);
-      result = new ArrayBlockingQueue<Result>(1);
-
-      setName("IntraLineDiff-" + count.getAndIncrement());
-      setDaemon(true);
-    }
-
-    Result computeWithTimeout(IntraLineDiffKey key, long timeoutMillis)
-        throws Exception {
-      if (!input.offer(new Input(key))) {
-        log.error("Cannot enqueue task to thread " + getName());
-        return Result.TIMEOUT;
-      }
-
-      Result r = result.poll(timeoutMillis, TimeUnit.MILLISECONDS);
-      if (r != null) {
-        return r;
-      } else {
-        log.warn(timeoutMillis + " ms timeout reached for IntraLineDiff"
-            + " in project " + key.getProject().get()
-            + " on commit " + key.getCommit().name()
-            + " for path " + key.getPath()
-            + " comparing " + key.getBlobA().name()
-            + ".." + key.getBlobB().name()
-            + ".  Killing " + getName());
-        forcefullyKillThreadInAnUglyWay();
-        return Result.TIMEOUT;
-      }
-    }
-
-    @SuppressWarnings("deprecation")
-    private void forcefullyKillThreadInAnUglyWay() {
-      try {
-        stop();
-      } catch (Throwable error) {
-        // Ignore any reason the thread won't stop.
-        log.error("Cannot stop runaway thread " + getName(), error);
-      }
-    }
-
-    private void shutdownGracefully() {
-      if (!input.offer(Input.END_THREAD)) {
-        log.error("Cannot gracefully stop thread " + getName());
-      }
-    }
-
-    @Override
-    public void run() {
-      try {
-        for (;;) {
-          Input in;
-          try {
-            in = input.take();
-          } catch (InterruptedException e) {
-            log.error("Unexpected interrupt on " + getName());
-            continue;
-          }
-
-          if (in == Input.END_THREAD) {
-            return;
-          }
-
-          Result r;
-          try {
-            r = new Result(IntraLineLoader.compute(in.key));
-          } catch (Exception error) {
-            r = new Result(error);
-          }
-
-          if (!result.offer(r)) {
-            log.error("Cannot return result from " + getName());
-          }
-        }
-      } catch (ThreadDeath iHaveBeenShot) {
-        // Handle thread death by gracefully returning to the caller,
-        // allowing the thread to be destroyed.
-      }
-    }
-
-    private static class Input {
-      static final Input END_THREAD = new Input(null);
-
-      final IntraLineDiffKey key;
-
-      Input(IntraLineDiffKey key) {
-        this.key = key;
-      }
-    }
-
-    static class Result {
-      static final Result TIMEOUT = new Result((IntraLineDiff) null);
-
-      final IntraLineDiff diff;
-      final Exception error;
-
-      Result(IntraLineDiff diff) {
-        this.diff = diff;
-        this.error = null;
-      }
-
-      Result(Exception error) {
-        this.diff = null;
-        this.error = error;
-      }
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java
index 59e3050..7d44912 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java
@@ -127,7 +127,7 @@
    *        how the cache is keyed versus how the database is keyed.
    */
   public List<Patch> toPatchList(final PatchSet.Id setId) {
-    final ArrayList<Patch> r = new ArrayList<Patch>(patches.length);
+    final ArrayList<Patch> r = new ArrayList<>(patches.length);
     for (final PatchListEntry e : patches) {
       r.add(e.toPatch(setId));
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
index 7b7c731..6c769f7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
@@ -35,7 +35,7 @@
 /** Provides a cached list of {@link PatchListEntry}. */
 @Singleton
 public class PatchListCacheImpl implements PatchListCache {
-  private static final String FILE_NAME = "diff";
+  static final String FILE_NAME = "diff";
   static final String INTRA_NAME = "diff_intraline";
 
   public static Module module() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java
index 852165c..31f5e96 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java
@@ -168,7 +168,7 @@
 
   public List<String> getHeaderLines() {
     final IntList m = RawParseUtils.lineMap(header, 0, header.length);
-    final List<String> headerLines = new ArrayList<String>(m.size() - 1);
+    final List<String> headerLines = new ArrayList<>(m.size() - 1);
     for (int i = 1; i < m.size() - 1; i++) {
       final int b = m.get(i);
       int e = m.get(i + 1);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
index ea8f191..95172b0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
@@ -16,12 +16,16 @@
 package com.google.gerrit.server.patch;
 
 import com.google.common.base.Function;
+import com.google.common.base.Throwables;
 import com.google.common.cache.CacheLoader;
 import com.google.common.collect.FluentIterable;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MergeUtil;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.diff.DiffEntry;
@@ -35,6 +39,7 @@
 import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.dircache.DirCacheBuilder;
 import org.eclipse.jgit.dircache.DirCacheEntry;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.ObjectId;
@@ -45,8 +50,8 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.merge.MergeFormatter;
 import org.eclipse.jgit.merge.MergeResult;
-import org.eclipse.jgit.merge.MergeStrategy;
 import org.eclipse.jgit.merge.ResolveMerger;
+import org.eclipse.jgit.merge.ThreeWayMergeStrategy;
 import org.eclipse.jgit.patch.FileHeader;
 import org.eclipse.jgit.patch.FileHeader.PatchType;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -66,17 +71,38 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 
 public class PatchListLoader extends CacheLoader<PatchListKey, PatchList> {
   static final Logger log = LoggerFactory.getLogger(PatchListLoader.class);
 
   private final GitRepositoryManager repoManager;
   private final PatchListCache patchListCache;
+  private final ThreeWayMergeStrategy mergeStrategy;
+  private final ExecutorService diffExecutor;
+  private final long timeoutMillis;
+  private final Object lock;
+
 
   @Inject
-  PatchListLoader(GitRepositoryManager mgr, PatchListCache plc) {
+  PatchListLoader(GitRepositoryManager mgr,
+      PatchListCache plc,
+      @GerritServerConfig Config cfg,
+      @DiffExecutor ExecutorService de) {
     repoManager = mgr;
     patchListCache = plc;
+    mergeStrategy = MergeUtil.getMergeStrategy(cfg);
+    diffExecutor = de;
+    lock = new Object();
+    timeoutMillis =
+        ConfigUtil.getTimeUnit(cfg, "cache", PatchListCacheImpl.FILE_NAME,
+            "timeout", TimeUnit.MILLISECONDS.convert(5, TimeUnit.SECONDS),
+            TimeUnit.MILLISECONDS);
   }
 
   @Override
@@ -158,7 +184,7 @@
         DiffEntry diffEntry = diffEntries.get(i);
         if (paths == null || paths.contains(diffEntry.getNewPath())
             || paths.contains(diffEntry.getOldPath())) {
-          FileHeader fh = df.toFileHeader(diffEntry);
+          FileHeader fh = toFileHeader(key, df, diffEntry);
           entries.add(newEntry(aTree, fh));
         }
       }
@@ -169,6 +195,48 @@
     }
   }
 
+  private FileHeader toFileHeader(PatchListKey key,
+      final DiffFormatter diffFormatter, final DiffEntry diffEntry)
+      throws IOException {
+
+    Future<FileHeader> result = diffExecutor.submit(new Callable<FileHeader>() {
+      @Override
+      public FileHeader call() throws IOException {
+        synchronized (lock) {
+          return diffFormatter.toFileHeader(diffEntry);
+        }
+      }
+    });
+
+    try {
+      return result.get(timeoutMillis, TimeUnit.MILLISECONDS);
+    } catch (InterruptedException | TimeoutException e) {
+      log.warn(timeoutMillis + " ms timeout reached for Diff loader"
+                      + " in project " + key.projectKey.get()
+                      + " on commit " + key.getNewId().name()
+                      + " on path " + diffEntry.getNewPath()
+                      + " comparing " + diffEntry.getOldId().name()
+                      + ".." + diffEntry.getNewId().name());
+      result.cancel(true);
+      synchronized (lock) {
+        return toFileHeaderWithoutMyersDiff(diffFormatter, diffEntry);
+      }
+    } catch (ExecutionException e) {
+      // If there was an error computing the result, carry it
+      // up to the caller so the cache knows this key is invalid.
+      Throwables.propagateIfInstanceOf(e.getCause(), IOException.class);
+      throw new IOException(e.getMessage(), e.getCause());
+    }
+  }
+
+  private FileHeader toFileHeaderWithoutMyersDiff(DiffFormatter diffFormatter,
+      DiffEntry diffEntry) throws IOException {
+    HistogramDiff histogramDiff = new HistogramDiff();
+    histogramDiff.setFallbackAlgorithm(null);
+    diffFormatter.setDiffAlgorithm(histogramDiff);
+    return diffFormatter.toFileHeader(diffEntry);
+  }
+
   private PatchListEntry newCommitMessage(final RawTextComparator cmp,
       final Repository db, final ObjectReader reader,
       final RevCommit aCommit, final RevCommit bCommit) throws IOException {
@@ -224,7 +292,7 @@
     }
   }
 
-  private static RevObject aFor(final PatchListKey key,
+  private RevObject aFor(final PatchListKey key,
       final Repository repo, final RevWalk rw, final RevCommit b)
       throws IOException {
     if (key.getOldId() != null) {
@@ -240,20 +308,20 @@
         return r;
       }
       case 2:
-        return automerge(repo, rw, b);
+        return automerge(repo, rw, b, mergeStrategy);
       default:
         // TODO(sop) handle an octopus merge.
         return null;
     }
   }
 
-  public static RevTree automerge(Repository repo, RevWalk rw, RevCommit b)
-      throws IOException {
-    return automerge(repo, rw, b, true);
+  public static RevTree automerge(Repository repo, RevWalk rw, RevCommit b,
+      ThreeWayMergeStrategy mergeStrategy) throws IOException {
+    return automerge(repo, rw, b, mergeStrategy, true);
   }
 
   public static RevTree automerge(Repository repo, RevWalk rw, RevCommit b,
-      boolean save) throws IOException {
+      ThreeWayMergeStrategy mergeStrategy, boolean save) throws IOException {
     String hash = b.name();
     String refName = RefNames.REFS_CACHE_AUTOMERGE
         + hash.substring(0, 2)
@@ -264,8 +332,7 @@
       return rw.parseTree(ref.getObjectId());
     }
 
-    ObjectId treeId;
-    ResolveMerger m = (ResolveMerger) MergeStrategy.RESOLVE.newMerger(repo, true);
+    ResolveMerger m = (ResolveMerger) mergeStrategy.newMerger(repo, true);
     final ObjectInserter ins = repo.newObjectInserter();
     try {
       DirCache dc = DirCache.newInCore();
@@ -293,9 +360,11 @@
         // an exception most likely means that the merge tree was not created
         // and m.getMergeResults() is empty. This would mean that all paths are
         // unmerged and Gerrit UI would show all paths in the patch list.
+        log.warn("Error attempting automerge " + refName, e);
         return null;
       }
 
+      ObjectId treeId;
       if (couldMerge) {
         treeId = m.getResultTreeId();
 
@@ -316,10 +385,11 @@
 
         MergeFormatter fmt = new MergeFormatter();
         Map<String, MergeResult<? extends Sequence>> r = m.getMergeResults();
-        Map<String, ObjectId> resolved = new HashMap<String, ObjectId>();
+        Map<String, ObjectId> resolved = new HashMap<>();
         for (Map.Entry<String, MergeResult<? extends Sequence>> entry : r.entrySet()) {
           MergeResult<? extends Sequence> p = entry.getValue();
-          TemporaryBuffer buf = new TemporaryBuffer.LocalFile(null, 10 * 1024 * 1024);
+          TemporaryBuffer buf =
+              new TemporaryBuffer.LocalFile(null, 10 * 1024 * 1024);
           try {
             fmt.formatMerge(buf, p, "BASE", oursName, theirsName, "UTF-8");
             buf.close();
@@ -380,17 +450,18 @@
         treeId = dc.writeTree(ins);
       }
       ins.flush();
+
+      if (save) {
+        RefUpdate update = repo.updateRef(refName);
+        update.setNewObjectId(treeId);
+        update.disableRefLog();
+        update.forceUpdate();
+      }
+
+      return rw.lookupTree(treeId);
     } finally {
       ins.close();
     }
-
-    if (save) {
-      RefUpdate update = repo.updateRef(refName);
-      update.setNewObjectId(treeId);
-      update.disableRefLog();
-      update.forceUpdate();
-    }
-    return rw.parseTree(treeId);
   }
 
   private static ObjectId emptyTree(final Repository repo) throws IOException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
index d0fb7b1..3f87aef 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
@@ -136,7 +136,7 @@
     a.resolve(null, aId);
     b.resolve(a, bId);
 
-    edits = new ArrayList<Edit>(content.getEdits());
+    edits = new ArrayList<>(content.getEdits());
 
     if (!isModify(content)) {
       intralineDifferenceIsPossible = false;
@@ -148,7 +148,7 @@
       if (d != null) {
         switch (d.getStatus()) {
           case EDIT_LIST:
-            edits = new ArrayList<Edit>(d.getEdits());
+            edits = new ArrayList<>(d.getEdits());
             break;
 
           case DISABLED:
@@ -171,7 +171,9 @@
       }
     }
 
-    ensureCommentsVisible(comments);
+    if (comments != null) {
+      ensureCommentsVisible(comments);
+    }
 
     boolean hugeFile = false;
     if (a.mode == FileMode.GITLINK || b.mode == FileMode.GITLINK) {
@@ -185,7 +187,7 @@
       for (int i = 0; i < a.size(); i++) {
         a.addLine(i);
       }
-      edits = new ArrayList<Edit>(1);
+      edits = new ArrayList<>(1);
       edits.add(new Edit(a.size(), a.size()));
 
     } else {
@@ -269,7 +271,7 @@
     // correct hunks from this, but because the Edit is empty they will not
     // style it specially.
     //
-    final List<Edit> empty = new ArrayList<Edit>();
+    final List<Edit> empty = new ArrayList<>();
     int lastLine;
 
     lastLine = -1;
@@ -450,7 +452,9 @@
 
           id = tw != null ? tw.getObjectId(0) : ObjectId.zeroId();
           mode = tw != null ? tw.getFileMode(0) : FileMode.MISSING;
-          reuse = other != null && other.id.equals(id) && other.mode == mode;
+          reuse = other != null
+              && other.id.equals(id)
+              && (other.mode == mode || isBothFile(other.mode, mode));
 
           if (reuse) {
             srcContent = other.srcContent;
@@ -514,4 +518,9 @@
       return TreeWalk.forPath(reader, path, tree);
     }
   }
+
+  private static boolean isBothFile(FileMode a, FileMode b) {
+    return (a.getBits() & FileMode.TYPE_FILE) == FileMode.TYPE_FILE
+        && (b.getBits() & FileMode.TYPE_FILE) == FileMode.TYPE_FILE;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java
index ed4470a..b4337cf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java
@@ -29,9 +29,11 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.PatchLineCommentsUtil;
 import com.google.gerrit.server.account.AccountInfoCacheFactory;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.LargeObjectException;
+import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gwtorm.server.OrmException;
@@ -71,6 +73,7 @@
   private final PatchListCache patchListCache;
   private final ReviewDb db;
   private final AccountInfoCacheFactory.Factory aicFactory;
+  private final PatchLineCommentsUtil plcUtil;
 
   private final String fileName;
   @Nullable
@@ -79,6 +82,8 @@
   private final AccountDiffPreference diffPrefs;
 
   private final Change.Id changeId;
+  private boolean loadHistory = true;
+  private boolean loadComments = true;
 
   private Change change;
   private Project.NameKey projectKey;
@@ -93,6 +98,7 @@
       Provider<PatchScriptBuilder> builderFactory,
       final PatchListCache patchListCache, final ReviewDb db,
       final AccountInfoCacheFactory.Factory aicFactory,
+      PatchLineCommentsUtil plcUtil,
       @Assisted ChangeControl control,
       @Assisted final String fileName,
       @Assisted("patchSetA") @Nullable final PatchSet.Id patchSetA,
@@ -104,6 +110,7 @@
     this.db = db;
     this.control = control;
     this.aicFactory = aicFactory;
+    this.plcUtil = plcUtil;
 
     this.fileName = fileName;
     this.psa = patchSetA;
@@ -113,6 +120,14 @@
     changeId = patchSetB.getParentKey();
   }
 
+  public void setLoadHistory(boolean load) {
+    loadHistory = load;
+  }
+
+  public void setLoadComments(boolean load) {
+    loadComments = load;
+  }
+
   @Override
   public PatchScript call() throws OrmException, NoSuchChangeException,
       LargeObjectException {
@@ -149,7 +164,7 @@
           content.getOldName(), //
           content.getNewName());
 
-        return b.toPatchScript(content, comments, history);
+      return b.toPatchScript(content, comments, history);
     } catch (PatchListNotAvailableException e) {
       throw new NoSuchChangeException(changeId, e);
     } catch (IOException e) {
@@ -212,98 +227,102 @@
 
   private void loadCommentsAndHistory(final ChangeType changeType,
       final String oldName, final String newName) throws OrmException {
-    history = new ArrayList<Patch>();
-    comments = new CommentDetail(psa, psb);
+    final Map<Patch.Key, Patch> byKey = new HashMap<>();
 
-    final Map<Patch.Key, Patch> byKey = new HashMap<Patch.Key, Patch>();
-    final AccountInfoCacheFactory aic = aicFactory.create();
-
-    // This seems like a cheap trick. It doesn't properly account for a
-    // file that gets renamed between patch set 1 and patch set 2. We
-    // will wind up packing the wrong Patch object because we didn't do
-    // proper rename detection between the patch sets.
-    //
-    for (final PatchSet ps : db.patchSets().byChange(changeId)) {
-      if (!control.isPatchVisible(ps, db)) {
-        continue;
-      }
-      String name = fileName;
-      if (psa != null) {
-        switch (changeType) {
-          case COPIED:
-          case RENAMED:
-            if (ps.getId().equals(psa)) {
-              name = oldName;
-            }
-            break;
-
-          case MODIFIED:
-          case DELETED:
-          case ADDED:
-          case REWRITE:
-            break;
+    if (loadHistory) {
+      // This seems like a cheap trick. It doesn't properly account for a
+      // file that gets renamed between patch set 1 and patch set 2. We
+      // will wind up packing the wrong Patch object because we didn't do
+      // proper rename detection between the patch sets.
+      //
+      history = new ArrayList<>();
+      for (final PatchSet ps : db.patchSets().byChange(changeId)) {
+        if (!control.isPatchVisible(ps, db)) {
+          continue;
         }
-      }
-
-      final Patch p = new Patch(new Patch.Key(ps.getId(), name));
-      history.add(p);
-      byKey.put(p.getKey(), p);
-    }
-
-    switch (changeType) {
-      case ADDED:
-      case MODIFIED:
-        loadPublished(byKey, aic, newName);
-        break;
-
-      case DELETED:
-        loadPublished(byKey, aic, newName);
-        break;
-
-      case COPIED:
-      case RENAMED:
+        String name = fileName;
         if (psa != null) {
-          loadPublished(byKey, aic, oldName);
-        }
-        loadPublished(byKey, aic, newName);
-        break;
+          switch (changeType) {
+            case COPIED:
+            case RENAMED:
+              if (ps.getId().equals(psa)) {
+                name = oldName;
+              }
+              break;
 
-      case REWRITE:
-        break;
+            case MODIFIED:
+            case DELETED:
+            case ADDED:
+            case REWRITE:
+              break;
+          }
+        }
+
+        final Patch p = new Patch(new Patch.Key(ps.getId(), name));
+        history.add(p);
+        byKey.put(p.getKey(), p);
+      }
     }
 
-    final CurrentUser user = control.getCurrentUser();
-    if (user.isIdentifiedUser()) {
-      final Account.Id me = ((IdentifiedUser) user).getAccountId();
+    if (loadComments) {
+      final AccountInfoCacheFactory aic = aicFactory.create();
+      comments = new CommentDetail(psa, psb);
       switch (changeType) {
         case ADDED:
         case MODIFIED:
-          loadDrafts(byKey, aic, me, newName);
+          loadPublished(byKey, aic, newName);
           break;
 
         case DELETED:
-          loadDrafts(byKey, aic, me, newName);
+          loadPublished(byKey, aic, newName);
           break;
 
         case COPIED:
         case RENAMED:
           if (psa != null) {
-            loadDrafts(byKey, aic, me, oldName);
+            loadPublished(byKey, aic, oldName);
           }
-          loadDrafts(byKey, aic, me, newName);
+          loadPublished(byKey, aic, newName);
           break;
 
         case REWRITE:
           break;
       }
-    }
 
-    comments.setAccountInfoCache(aic.create());
+      final CurrentUser user = control.getCurrentUser();
+      if (user.isIdentifiedUser()) {
+        final Account.Id me = ((IdentifiedUser) user).getAccountId();
+        switch (changeType) {
+          case ADDED:
+          case MODIFIED:
+            loadDrafts(byKey, aic, me, newName);
+            break;
+
+          case DELETED:
+            loadDrafts(byKey, aic, me, newName);
+            break;
+
+          case COPIED:
+          case RENAMED:
+            if (psa != null) {
+              loadDrafts(byKey, aic, me, oldName);
+            }
+            loadDrafts(byKey, aic, me, newName);
+            break;
+
+          case REWRITE:
+            break;
+        }
+      }
+
+      comments.setAccountInfoCache(aic.create());
+    }
   }
 
   private void loadPublished(final Map<Patch.Key, Patch> byKey,
       final AccountInfoCacheFactory aic, final String file) throws OrmException {
-    for (PatchLineComment c : db.patchComments().publishedByChangeFile(changeId, file)) {
+    ChangeNotes notes = control.getNotes();
+    for (PatchLineComment c : plcUtil.publishedByChangeFile(db, notes, changeId, file)) {
       if (comments.include(c)) {
         aic.want(c.getAuthor());
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
index 5180854..0571b58 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
@@ -123,8 +123,7 @@
 
   private List<PatchSetInfo.ParentInfo> toParentInfos(final RevCommit[] parents,
       final RevWalk walk) throws IOException, MissingObjectException {
-    List<PatchSetInfo.ParentInfo> pInfos =
-      new ArrayList<PatchSetInfo.ParentInfo>(parents.length);
+    List<PatchSetInfo.ParentInfo> pInfos = new ArrayList<>(parents.length);
     for (RevCommit parent : parents) {
       walk.parseBody(parent);
       RevId rev = new RevId(parent.getId().name());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AbstractPreloadedPluginScanner.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AbstractPreloadedPluginScanner.java
new file mode 100644
index 0000000..0b128dd
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AbstractPreloadedPluginScanner.java
@@ -0,0 +1,157 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.plugins;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Sets;
+import com.google.gerrit.extensions.annotations.Export;
+import com.google.gerrit.server.plugins.Plugin.ApiType;
+import com.google.inject.Module;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Modifier;
+import java.util.Map;
+import java.util.Set;
+import java.util.jar.Manifest;
+
+import static com.google.common.base.Preconditions.checkState;
+
+/**
+ * Base plugin scanner for a set of pre-loaded classes.
+ *
+ * Utility base class for simplifying the development of Server plugin scanner
+ * based on a set of externally pre-loaded classes.
+ *
+ * Extending this class you can implement very easily a PluginContentScanner
+ * from a set of pre-loaded Java Classes and an API Type.
+ * The convention used by this class is:
+ * - there is at most one Guice module per Gerrit module type (SysModule, HttpModule, SshModule)
+ * - plugin is set to be restartable in Gerrit Plugin MANIFEST
+ * - only Export and Listen annotated classes can be self-discovered
+ */
+public abstract class AbstractPreloadedPluginScanner implements
+    PluginContentScanner {
+  protected final String pluginName;
+  protected final String pluginVersion;
+  protected final Set<Class<?>> preloadedClasses;
+  protected final ApiType apiType;
+
+  private Class<?> sshModuleClass;
+  private Class<?> httpModuleClass;
+  private Class<?> sysModuleClass;
+
+  public AbstractPreloadedPluginScanner(String pluginName, String pluginVersion,
+      Set<Class<?>> preloadedClasses, Plugin.ApiType apiType) {
+    this.pluginName = pluginName;
+    this.pluginVersion = pluginVersion;
+    this.preloadedClasses = preloadedClasses;
+    this.apiType = apiType;
+  }
+
+  @Override
+  public Manifest getManifest() throws IOException {
+    scanGuiceModules(preloadedClasses);
+    StringBuilder manifestString =
+        new StringBuilder("PluginName: " + pluginName + "\n"
+            + "Implementation-Version: " + pluginVersion + "\n"
+            + "Gerrit-ReloadMode: restart\n"
+            + "Gerrit-ApiType: " + apiType + "\n");
+    appendIfNotNull(manifestString, "Gerrit-SshModule: ", sshModuleClass);
+    appendIfNotNull(manifestString, "Gerrit-HttpModule: ", httpModuleClass);
+    appendIfNotNull(manifestString, "Gerrit-Module: ", sysModuleClass);
+    return new Manifest(new ByteArrayInputStream(manifestString.toString()
+        .getBytes()));
+  }
+
+  @Override
+  public Map<Class<? extends Annotation>, Iterable<ExtensionMetaData>> scan(
+      String pluginName, Iterable<Class<? extends Annotation>> annotations)
+      throws InvalidPluginException {
+    ImmutableMap.Builder<Class<? extends Annotation>, Iterable<ExtensionMetaData>> result =
+        ImmutableMap.builder();
+
+    for (Class<? extends Annotation> annotation : annotations) {
+      Set<ExtensionMetaData> classMetaDataSet = Sets.newHashSet();
+      result.put(annotation, classMetaDataSet);
+
+      for (Class<?> clazz : preloadedClasses) {
+        if (!Modifier.isAbstract(clazz.getModifiers())
+            && clazz.getAnnotation(annotation) != null) {
+          classMetaDataSet.add(new ExtensionMetaData(clazz.getName(),
+              getExportAnnotationValue(clazz, annotation)));
+        }
+      }
+    }
+    return result.build();
+  }
+
+  private void appendIfNotNull(StringBuilder string, String header,
+      Class<?> guiceModuleClass) {
+    if (guiceModuleClass != null) {
+      string.append(header);
+      string.append(guiceModuleClass.getName());
+      string.append("\n");
+    }
+  }
+
+  private void scanGuiceModules(Set<Class<?>> classes) throws IOException {
+    try {
+      Class<?> sysModuleBaseClass = Module.class;
+      Class<?> httpModuleBaseClass =
+          Class.forName("com.google.inject.servlet.ServletModule");
+      Class<?> sshModuleBaseClass =
+          Class.forName("com.google.gerrit.sshd.CommandModule");
+      sshModuleClass = null;
+      httpModuleClass = null;
+      sysModuleClass = null;
+
+      for (Class<?> clazz : classes) {
+        if (clazz.isLocalClass()) {
+          continue;
+        }
+
+        if (sshModuleBaseClass.isAssignableFrom(clazz)) {
+          sshModuleClass =
+              getUniqueGuiceModule(sshModuleBaseClass, sshModuleClass, clazz);
+        } else if (httpModuleBaseClass.isAssignableFrom(clazz)) {
+          httpModuleClass =
+              getUniqueGuiceModule(httpModuleBaseClass, httpModuleClass, clazz);
+        } else if (sysModuleBaseClass.isAssignableFrom(clazz)) {
+          sysModuleClass =
+              getUniqueGuiceModule(sysModuleBaseClass, sysModuleClass, clazz);
+        }
+      }
+    } catch (ClassNotFoundException e) {
+      throw new IOException(
+          "Cannot find base Gerrit classes for Guice Plugin Modules", e);
+    }
+  }
+
+  private Class<?> getUniqueGuiceModule(Class<?> guiceModuleBaseClass,
+      Class<?> existingGuiceModuleName, Class<?> newGuiceModuleClass) {
+    checkState(existingGuiceModuleName == null,
+        "Multiple %s implementations: %s, %s", guiceModuleBaseClass,
+        existingGuiceModuleName, newGuiceModuleClass);
+    return newGuiceModuleClass;
+  }
+
+  private String getExportAnnotationValue(Class<?> scriptClass,
+      Class<? extends Annotation> annotation) {
+    return annotation == Export.class ? scriptClass.getAnnotation(Export.class)
+        .value() : "";
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java
index bd8a524..6f1204b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java
@@ -23,7 +23,7 @@
 import com.google.gerrit.extensions.annotations.Export;
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
 import com.google.gerrit.extensions.annotations.Listen;
-import com.google.gerrit.server.plugins.JarScanner.ExtensionMetaData;
+import com.google.gerrit.server.plugins.PluginContentScanner.ExtensionMetaData;
 import com.google.inject.AbstractModule;
 import com.google.inject.Module;
 import com.google.inject.Scopes;
@@ -34,12 +34,11 @@
 import java.util.Arrays;
 import java.util.Map;
 import java.util.Set;
-import java.util.jar.JarFile;
 
 class AutoRegisterModules {
   private final String pluginName;
   private final PluginGuiceEnvironment env;
-  private final JarFile jarFile;
+  private final PluginContentScanner scanner;
   private final ClassLoader classLoader;
   private final ModuleGenerator sshGen;
   private final ModuleGenerator httpGen;
@@ -53,11 +52,11 @@
 
   AutoRegisterModules(String pluginName,
       PluginGuiceEnvironment env,
-      JarFile jarFile,
+      PluginContentScanner scanner,
       ClassLoader classLoader) {
     this.pluginName = pluginName;
     this.env = env;
-    this.jarFile = jarFile;
+    this.scanner = scanner;
     this.classLoader = classLoader;
     this.sshGen = env.hasSshModule() ? env.newSshModuleGenerator() : null;
     this.httpGen = env.hasHttpModule() ? env.newHttpModuleGenerator() : null;
@@ -111,7 +110,7 @@
 
   private void scan() throws InvalidPluginException {
     Map<Class<? extends Annotation>, Iterable<ExtensionMetaData>> extensions =
-        JarScanner.scan(jarFile, pluginName, Arrays.asList(Export.class, Listen.class));
+        scanner.scan(pluginName, Arrays.asList(Export.class, Listen.class));
     for (ExtensionMetaData export : extensions.get(Export.class)) {
       export(export);
     }
@@ -123,18 +122,18 @@
   private void export(ExtensionMetaData def) throws InvalidPluginException {
     Class<?> clazz;
     try {
-      clazz = Class.forName(def.getClassName(), false, classLoader);
+      clazz = Class.forName(def.className, false, classLoader);
     } catch (ClassNotFoundException err) {
       throw new InvalidPluginException(String.format(
           "Cannot load %s with @Export(\"%s\")",
-          def.getClassName(), def.getAnnotationValue()), err);
+          def.className, def.annotationValue), err);
     }
 
     Export export = clazz.getAnnotation(Export.class);
     if (export == null) {
       PluginLoader.log.warn(String.format(
           "In plugin %s asm incorrectly parsed %s with @Export(\"%s\")",
-          pluginName, clazz.getName(), def.getAnnotationValue()));
+          pluginName, clazz.getName(), def.annotationValue));
       return;
     }
 
@@ -162,11 +161,11 @@
   private void listen(ExtensionMetaData def) throws InvalidPluginException {
     Class<?> clazz;
     try {
-      clazz = Class.forName(def.getClassName(), false, classLoader);
+      clazz = Class.forName(def.className, false, classLoader);
     } catch (ClassNotFoundException err) {
       throw new InvalidPluginException(String.format(
           "Cannot load %s with @Listen",
-          def.getClassName()), err);
+          def.className), err);
     }
 
     Listen listen = clazz.getAnnotation(Listen.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/DisablePlugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/DisablePlugin.java
index 3a1915a..d92dce0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/DisablePlugin.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/DisablePlugin.java
@@ -22,8 +22,10 @@
 import com.google.gerrit.server.plugins.DisablePlugin.Input;
 import com.google.gerrit.server.plugins.ListPlugins.PluginInfo;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@Singleton
 class DisablePlugin implements RestModifyView<PluginResource, Input> {
   static class Input {
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/EnablePlugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/EnablePlugin.java
index f7745cc..b6f8260 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/EnablePlugin.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/EnablePlugin.java
@@ -23,11 +23,13 @@
 import com.google.gerrit.server.plugins.EnablePlugin.Input;
 import com.google.gerrit.server.plugins.ListPlugins.PluginInfo;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
 import java.io.PrintWriter;
 import java.io.StringWriter;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@Singleton
 class EnablePlugin implements RestModifyView<PluginResource, Input> {
   static class Input {
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/GetStatus.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/GetStatus.java
index 7651506..47f7985 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/GetStatus.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/GetStatus.java
@@ -16,7 +16,9 @@
 
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.plugins.ListPlugins.PluginInfo;
+import com.google.inject.Singleton;
 
+@Singleton
 class GetStatus implements RestReadView<PluginResource> {
   @Override
   public PluginInfo apply(PluginResource resource) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarPluginProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarPluginProvider.java
new file mode 100644
index 0000000..df9ccf4
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarPluginProvider.java
@@ -0,0 +1,159 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.plugins;
+
+import static com.google.gerrit.server.plugins.PluginLoader.asTemp;
+
+import com.google.common.base.Objects;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.internal.storage.file.FileSnapshot;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+
+public class JarPluginProvider implements ServerPluginProvider {
+  static final String PLUGIN_TMP_PREFIX = "plugin_";
+  static final String JAR_EXTENSION = ".jar";
+  static final Logger log = LoggerFactory.getLogger(JarPluginProvider.class);
+
+  private final File tmpDir;
+
+  @Inject
+  JarPluginProvider(SitePaths sitePaths) {
+    tmpDir = sitePaths.tmp_dir;
+  }
+
+  @Override
+  public boolean handles(File srcFile) {
+    String fileName = srcFile.getName();
+    return fileName.endsWith(JAR_EXTENSION)
+        || fileName.endsWith(JAR_EXTENSION + ".disabled");
+  }
+
+  @Override
+  public String getPluginName(File srcFile) {
+    try {
+      return Objects.firstNonNull(getJarPluginName(srcFile),
+          PluginLoader.nameOf(srcFile));
+    } catch (IOException e) {
+      throw new IllegalArgumentException("Invalid plugin file " + srcFile
+          + ": cannot get plugin name", e);
+    }
+  }
+
+  public static String getJarPluginName(File srcFile) throws IOException {
+    try (JarFile jarFile = new JarFile(srcFile)) {
+      return jarFile.getManifest().getMainAttributes()
+          .getValue("Gerrit-PluginName");
+    }
+  }
+
+  @Override
+  public ServerPlugin get(File srcFile, FileSnapshot snapshot,
+      PluginDescription description) throws InvalidPluginException {
+    try {
+      String name = getPluginName(srcFile);
+      String extension = getExtension(srcFile);
+      try (FileInputStream in = new FileInputStream(srcFile)) {
+        File tmp = asTemp(in, tempNameFor(name), extension, tmpDir);
+        return loadJarPlugin(name, srcFile, snapshot, tmp, description);
+      }
+    } catch (IOException | ClassNotFoundException e) {
+      throw new InvalidPluginException("Cannot load Jar plugin " + srcFile, e);
+    }
+  }
+
+  @Override
+  public String getProviderPluginName() {
+    return "gerrit";
+  }
+
+  private static String getExtension(File file) {
+    return getExtension(file.getName());
+  }
+
+  private static String getExtension(String name) {
+    int ext = name.lastIndexOf('.');
+    return 0 < ext ? name.substring(ext) : "";
+  }
+
+  private static String tempNameFor(String name) {
+    SimpleDateFormat fmt = new SimpleDateFormat("yyMMdd_HHmm");
+    return PLUGIN_TMP_PREFIX + name + "_" + fmt.format(new Date()) + "_";
+  }
+
+  public static File storeInTemp(String pluginName, InputStream in,
+      SitePaths sitePaths) throws IOException {
+    if (!sitePaths.tmp_dir.exists()) {
+      sitePaths.tmp_dir.mkdirs();
+    }
+    return asTemp(in, tempNameFor(pluginName), ".jar", sitePaths.tmp_dir);
+  }
+
+  private ServerPlugin loadJarPlugin(String name, File srcJar,
+      FileSnapshot snapshot, File tmp, PluginDescription description)
+      throws IOException, InvalidPluginException, MalformedURLException,
+      ClassNotFoundException {
+    JarFile jarFile = new JarFile(tmp);
+    boolean keep = false;
+    try {
+      Manifest manifest = jarFile.getManifest();
+      Plugin.ApiType type = Plugin.getApiType(manifest);
+
+      List<URL> urls = new ArrayList<>(2);
+      String overlay = System.getProperty("gerrit.plugin-classes");
+      if (overlay != null) {
+        File classes = new File(new File(new File(overlay), name), "main");
+        if (classes.isDirectory()) {
+          log.info(String.format("plugin %s: including %s", name,
+              classes.getPath()));
+          urls.add(classes.toURI().toURL());
+        }
+      }
+      urls.add(tmp.toURI().toURL());
+
+      ClassLoader pluginLoader =
+          new URLClassLoader(urls.toArray(new URL[urls.size()]),
+              PluginLoader.parentFor(type));
+
+      ServerPlugin plugin =
+          new ServerPlugin(name, description.canonicalUrl, description.user,
+              srcJar, snapshot, new JarScanner(srcJar), description.dataDir,
+              pluginLoader);
+      plugin.setCleanupHandle(new CleanupHandle(tmp, jarFile));
+      keep = true;
+      return plugin;
+    } finally {
+      if (!keep) {
+        jarFile.close();
+      }
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarScanner.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarScanner.java
index 2f25ed0..31a7c98 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarScanner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarScanner.java
@@ -24,6 +24,7 @@
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Multimap;
 import com.google.common.collect.Sets;
@@ -38,6 +39,7 @@
 import org.objectweb.asm.Opcodes;
 import org.objectweb.asm.Type;
 
+import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.lang.annotation.Annotation;
@@ -46,10 +48,12 @@
 import java.util.Enumeration;
 import java.util.Map;
 import java.util.Set;
+import java.util.jar.Attributes;
 import java.util.jar.JarEntry;
 import java.util.jar.JarFile;
+import java.util.jar.Manifest;
 
-public class JarScanner {
+public class JarScanner implements PluginContentScanner {
   private static final int SKIP_ALL = ClassReader.SKIP_CODE
       | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
   private static final Function<ClassData, ExtensionMetaData> CLASS_DATA_TO_EXTENSION_META_DATA =
@@ -57,44 +61,23 @@
         @Override
         public ExtensionMetaData apply(ClassData classData) {
           return new ExtensionMetaData(classData.className,
-              classData.annotationValue, classData.interfaces);
-        }
-      };
-  private static final Function<String, String> TO_JAVA_QUALIFIED_CLASS_NAME =
-      new Function<String, String>() {
-        @Override
-        public String apply(String in) {
-          return in.replace("/", ".");
+              classData.annotationValue);
         }
       };
 
-  public static class ExtensionMetaData {
-    private final String className;
-    private final String annotationValue;
-    private final Iterable<String> interfaces;
+  private final JarFile jarFile;
 
-    private ExtensionMetaData(String className, String annotationValue, Iterable<String> interfaces) {
-      this.className = className;
-      this.annotationValue = annotationValue;
-      this.interfaces = interfaces;
-    }
-
-    public String getAnnotationValue() {
-      return annotationValue;
-    }
-
-    public String getClassName() {
-      return className;
-    }
-
-    public Iterable<String> getInterfaces() {
-      return interfaces;
+  public JarScanner(File srcFile) throws InvalidPluginException {
+    try {
+      this.jarFile = new JarFile(srcFile);
+    } catch (IOException e) {
+      throw new InvalidPluginException("Cannot scan plugin file " + srcFile, e);
     }
   }
 
-  public static Map<Class<? extends Annotation>, Iterable<ExtensionMetaData>> scan(
-      JarFile jarFile, String pluginName,
-      Iterable<Class<? extends Annotation>> annotations)
+  @Override
+  public Map<Class<? extends Annotation>, Iterable<ExtensionMetaData>> scan(
+      String pluginName, Iterable<Class<? extends Annotation>> annotations)
       throws InvalidPluginException {
     Set<String> descriptors = Sets.newHashSet();
     Multimap<String, JarScanner.ClassData> rawMap = ArrayListMultimap.create();
@@ -183,7 +166,6 @@
     String className;
     String annotationName;
     String annotationValue;
-    Iterable<String> interfaces;
     Iterable<String> exports;
 
     private ClassData(Iterable<String> exports) {
@@ -199,9 +181,6 @@
     @Override
     public void visit(int version, int access, String name, String signature,
         String superName, String[] interfaces) {
-      this.interfaces =
-          Iterables.transform(Sets.newHashSet(interfaces),
-              TO_JAVA_QUALIFIED_CLASS_NAME);
       this.className = Type.getObjectType(name).getClassName();
       this.access = access;
     }
@@ -279,4 +258,62 @@
     public void visitEnd() {
     }
   }
+
+  @Override
+  public Optional<PluginEntry> getEntry(String resourcePath) throws IOException {
+    JarEntry jarEntry = jarFile.getJarEntry(resourcePath);
+    if (jarEntry == null || jarEntry.getSize() == 0) {
+      return Optional.absent();
+    }
+
+    return Optional.of(resourceOf(jarEntry));
+  }
+
+  @Override
+  public Enumeration<PluginEntry> entries() {
+    return Collections.enumeration(Lists.transform(
+        Collections.list(jarFile.entries()),
+        new Function<JarEntry, PluginEntry>() {
+          public PluginEntry apply(JarEntry jarEntry) {
+            try {
+              return resourceOf(jarEntry);
+            } catch (IOException e) {
+              throw new IllegalArgumentException("Cannot convert jar entry "
+                  + jarEntry + " to a resource", e);
+            }
+          }
+        }));
+  }
+
+  @Override
+  public InputStream getInputStream(PluginEntry entry)
+      throws IOException {
+    return jarFile.getInputStream(jarFile
+        .getEntry(entry.getName()));
+  }
+
+  @Override
+  public Manifest getManifest() throws IOException {
+    return jarFile.getManifest();
+  }
+
+  private PluginEntry resourceOf(JarEntry jarEntry) throws IOException {
+    return new PluginEntry(jarEntry.getName(), jarEntry.getTime(),
+        Optional.of(jarEntry.getSize()), attributesOf(jarEntry));
+  }
+
+  private Map<Object, String> attributesOf(JarEntry jarEntry)
+      throws IOException {
+    Attributes attributes = jarEntry.getAttributes();
+    if (attributes == null) {
+      return Collections.emptyMap();
+    }
+    return Maps.transformEntries(attributes,
+        new Maps.EntryTransformer<Object, Object, String>() {
+          @Override
+          public String transformEntry(Object key, Object value) {
+            return (String) value;
+          }
+        });
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JsPlugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JsPlugin.java
index ff72576..63f69b5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JsPlugin.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JsPlugin.java
@@ -28,7 +28,6 @@
 import org.eclipse.jgit.internal.storage.file.FileSnapshot;
 
 import java.io.File;
-import java.util.jar.JarFile;
 
 class JsPlugin extends Plugin {
   private Injector httpInjector;
@@ -67,11 +66,6 @@
   }
 
   @Override
-  public JarFile getJarFile() {
-    return null;
-  }
-
-  @Override
   public Injector getSysInjector() {
     return null;
   }
@@ -109,4 +103,9 @@
           new JavaScriptPlugin(fileName));
     }
   }
+
+  @Override
+  public PluginContentScanner getContentScanner() {
+    return PluginContentScanner.EMPTY;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java
index c3f4338..c5611dd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java
@@ -127,7 +127,6 @@
   }
 
   static class PluginInfo {
-    final String kind = "gerritcodereview#plugin";
     String id;
     String version;
     String indexUrl;
@@ -138,7 +137,7 @@
       version = p.getVersion();
       disabled = p.isDisabled() ? true : null;
 
-      if (p.getJarFile() != null) {
+      if (p.getSrcFile() != null) {
         indexUrl = String.format("plugins/%s/", p.getName());
       }
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/MultipleProvidersForPluginException.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/MultipleProvidersForPluginException.java
new file mode 100644
index 0000000..82a6ad9
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/MultipleProvidersForPluginException.java
@@ -0,0 +1,45 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.plugins;
+
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.collect.Iterables;
+
+import java.io.File;
+
+class MultipleProvidersForPluginException extends IllegalArgumentException {
+  private static final long serialVersionUID = 1L;
+
+  MultipleProvidersForPluginException(File pluginSrcFile,
+      Iterable<ServerPluginProvider> providersHandlers) {
+    super(pluginSrcFile.getAbsolutePath()
+        + " is claimed to be handled by more than one plugin provider: "
+        + providersListToString(providersHandlers));
+  }
+
+  private static String providersListToString(
+      Iterable<ServerPluginProvider> providersHandlers) {
+    Iterable<String> providerNames =
+        Iterables.transform(providersHandlers,
+            new Function<ServerPluginProvider, String>() {
+              @Override
+              public String apply(ServerPluginProvider provider) {
+                return provider.getProviderPluginName();
+              }
+            });
+    return Joiner.on(", ").join(providerNames);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
index 988669a..b227909 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
@@ -29,7 +29,6 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.jar.Attributes;
-import java.util.jar.JarFile;
 import java.util.jar.Manifest;
 
 public abstract class Plugin {
@@ -74,6 +73,7 @@
   private final CacheKey cacheKey;
   private final PluginUser pluginUser;
   private final FileSnapshot snapshot;
+  private CleanupHandle cleanupHandle;
 
   protected LifecycleManager manager;
 
@@ -93,6 +93,14 @@
     this.disabled = srcFile.getName().endsWith(".disabled");
   }
 
+  public CleanupHandle getCleanupHandle() {
+    return cleanupHandle;
+  }
+
+  public void setCleanupHandle(CleanupHandle cleanupHandle) {
+    this.cleanupHandle = cleanupHandle;
+  }
+
   PluginUser getPluginUser() {
     return pluginUser;
   }
@@ -124,7 +132,7 @@
 
   abstract void stop(PluginGuiceEnvironment env);
 
-  public abstract JarFile getJarFile();
+  public abstract PluginContentScanner getContentScanner();
 
   public abstract Injector getSysInjector();
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginContentScanner.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginContentScanner.java
new file mode 100644
index 0000000..0228509
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginContentScanner.java
@@ -0,0 +1,130 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.gerrit.server.plugins;
+
+import com.google.common.base.Optional;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.annotation.Annotation;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Map;
+import java.util.jar.Manifest;
+
+/**
+ * Scans the plugin returning classes and resources.
+ *
+ * Gerrit uses the scanner to automatically discover the classes
+ * and resources exported by the plugin for auto discovery
+ * of exported SSH commands, Servlets and listeners.
+ */
+public interface PluginContentScanner {
+
+  /**
+   * Scanner without resources.
+   */
+  PluginContentScanner EMPTY = new PluginContentScanner() {
+    @Override
+    public Manifest getManifest() throws IOException {
+      return new Manifest();
+    }
+
+    @Override
+    public Map<Class<? extends Annotation>, Iterable<ExtensionMetaData>> scan(
+        String pluginName, Iterable<Class<? extends Annotation>> annotations)
+        throws InvalidPluginException {
+     return Collections.emptyMap();
+    }
+
+    @Override
+    public Optional<PluginEntry> getEntry(String resourcePath)
+        throws IOException {
+      return Optional.absent();
+    }
+
+    @Override
+    public InputStream getInputStream(PluginEntry entry) throws IOException {
+      throw new FileNotFoundException("Empty plugin");
+    }
+
+    @Override
+    public Enumeration<PluginEntry> entries() {
+      return Collections.emptyEnumeration();
+    }
+  };
+
+  /**
+   * Plugin class extension meta-data
+   *
+   * Class name and annotation value of the class
+   * provided by a plugin to extend an existing
+   * extension point in Gerrit.
+   */
+  public static class ExtensionMetaData {
+    public final String className;
+    public final String annotationValue;
+
+    public ExtensionMetaData(String className, String annotationValue) {
+      this.className = className;
+      this.annotationValue = annotationValue;
+    }
+  }
+
+  /**
+   * Return the plugin meta-data manifest
+   *
+   * @return Manifest of the plugin or null if plugin has no meta-data
+   * @throws IOException if an I/O problem occurred whilst accessing the Manifest
+   */
+  Manifest getManifest() throws IOException;
+
+  /**
+   * Scans the plugin for declared public annotated classes
+   *
+   * @param pluginName the plugin name
+   * @param annotations annotations declared by the plugin classes
+   * @return map of annotations and associated plugin classes found
+   * @throws InvalidPluginException if the plugin is not valid or corrupted
+   */
+  Map<Class<? extends Annotation>, Iterable<ExtensionMetaData>> scan(
+      String pluginName, Iterable<Class<? extends Annotation>> annotations)
+      throws InvalidPluginException;
+
+  /**
+   * Return the plugin resource associated to a path
+   *
+   * @param resourcePath full path of the resource inside the plugin package
+   * @return the resource object or Optional.absent() if the resource was not found
+   * @throws IOException if there was a problem retrieving the resource
+   */
+  Optional<PluginEntry> getEntry(String resourcePath) throws IOException;
+
+  /**
+   * Return the InputStream of the resource entry
+   *
+   * @param entry resource entry inside the plugin package
+   * @return the resource input stream
+   * @throws IOException if there was an I/O problem accessing the resource
+   */
+  InputStream getInputStream(PluginEntry entry) throws IOException;
+
+  /**
+   * Return all the resources inside a plugin
+   *
+   * @return the enumeration of all resources found
+   */
+  Enumeration<PluginEntry> entries();
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginEntry.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginEntry.java
new file mode 100644
index 0000000..7242e98
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginEntry.java
@@ -0,0 +1,79 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.gerrit.server.plugins;
+
+import com.google.common.base.Optional;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Map;
+
+/**
+ * Plugin static resource entry
+ *
+ * Bean representing a static resource inside a plugin.
+ * All static resources are available at <plugin web url>/static
+ * and served by the HttpPluginServlet.
+ */
+public class PluginEntry {
+  public static final String ATTR_CHARACTER_ENCODING = "Character-Encoding";
+  public static final String ATTR_CONTENT_TYPE = "Content-Type";
+  public static final Comparator<PluginEntry> COMPARATOR_BY_NAME =
+      new Comparator<PluginEntry>() {
+        @Override
+        public int compare(PluginEntry a, PluginEntry b) {
+          return a.getName().compareTo(b.getName());
+        }
+      };
+
+  private static final Map<Object, String> EMPTY_ATTRS = Collections.emptyMap();
+  private static final Optional<Long> NO_SIZE = Optional.absent();
+
+  private final String name;
+  private final long time;
+  private final Optional<Long> size;
+  private final Map<Object, String> attrs;
+
+  public PluginEntry(String name, long time, Optional<Long> size,
+      Map<Object, String> attrs) {
+    this.name = name;
+    this.time = time;
+    this.size = size;
+    this.attrs = attrs;
+  }
+
+  public PluginEntry(String name, long time, Optional<Long> size) {
+    this(name, time, size, EMPTY_ATTRS);
+  }
+
+  public PluginEntry(String name, long time) {
+    this(name, time, NO_SIZE, EMPTY_ATTRS);
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public long getTime() {
+    return time;
+  }
+
+  public Optional<Long> getSize() {
+    return size;
+  }
+
+  public Map<Object, String> getAttrs() {
+    return attrs;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
index ae90d5b..5b63a215 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
@@ -24,6 +24,7 @@
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.annotations.RootRelative;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.registration.DynamicMap;
@@ -56,6 +57,8 @@
 import java.util.concurrent.CopyOnWriteArrayList;
 
 import javax.inject.Inject;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
 
 /**
  * Tracks Guice bindings that should be exposed to loaded plugins.
@@ -72,6 +75,7 @@
   private final CopyConfigModule copyConfigModule;
   private final Set<Key<?>> copyConfigKeys;
   private final List<StartPluginListener> onStart;
+  private final List<StopPluginListener> onStop;
   private final List<ReloadPluginListener> onReload;
 
   private Module sysModule;
@@ -82,6 +86,8 @@
   private Provider<ModuleGenerator> httpGen;
 
   private Map<TypeLiteral<?>, DynamicItem<?>> sysItems;
+  private Map<TypeLiteral<?>, DynamicItem<?>> sshItems;
+  private Map<TypeLiteral<?>, DynamicItem<?>> httpItems;
 
   private Map<TypeLiteral<?>, DynamicSet<?>> sysSets;
   private Map<TypeLiteral<?>, DynamicSet<?>> sshSets;
@@ -103,10 +109,13 @@
     this.copyConfigModule = ccm;
     this.copyConfigKeys = Guice.createInjector(ccm).getAllBindings().keySet();
 
-    onStart = new CopyOnWriteArrayList<StartPluginListener>();
+    onStart = new CopyOnWriteArrayList<>();
     onStart.addAll(listeners(sysInjector, StartPluginListener.class));
 
-    onReload = new CopyOnWriteArrayList<ReloadPluginListener>();
+    onStop = new CopyOnWriteArrayList<>();
+    onStop.addAll(listeners(sysInjector, StopPluginListener.class));
+
+    onReload = new CopyOnWriteArrayList<>();
     onReload.addAll(listeners(sysInjector, ReloadPluginListener.class));
 
     sysItems = dynamicItemsOf(sysInjector);
@@ -119,7 +128,9 @@
   }
 
   boolean hasDynamicItem(TypeLiteral<?> type) {
-    return sysItems.containsKey(type);
+    return sysItems.containsKey(type)
+        || (sshItems != null && sshItems.containsKey(type))
+        || (httpItems != null && httpItems.containsKey(type));
   }
 
   boolean hasDynamicSet(TypeLiteral<?> type) {
@@ -154,9 +165,11 @@
   public void setSshInjector(Injector injector) {
     sshModule = copy(injector);
     sshGen = injector.getProvider(ModuleGenerator.class);
+    sshItems = dynamicItemsOf(injector);
     sshSets = dynamicSetsOf(injector);
     sshMaps = dynamicMapsOf(injector);
     onStart.addAll(listeners(injector, StartPluginListener.class));
+    onStop.addAll(listeners(injector, StopPluginListener.class));
     onReload.addAll(listeners(injector, ReloadPluginListener.class));
   }
 
@@ -175,9 +188,11 @@
   public void setHttpInjector(Injector injector) {
     httpModule = copy(injector);
     httpGen = injector.getProvider(ModuleGenerator.class);
+    httpItems = dynamicItemsOf(injector);
     httpSets = dynamicSetsOf(injector);
     httpMaps = dynamicMapsOf(injector);
     onStart.addAll(listeners(injector, StartPluginListener.class));
+    onStop.addAll(listeners(injector, StopPluginListener.class));
     onReload.addAll(listeners(injector, ReloadPluginListener.class));
   }
 
@@ -202,13 +217,11 @@
   }
 
   void onStartPlugin(Plugin plugin) {
-    for (StartPluginListener l : onStart) {
-      l.onStartPlugin(plugin);
-    }
-
     RequestContext oldContext = enter(plugin);
     try {
       attachItem(sysItems, plugin.getSysInjector(), plugin);
+      attachItem(sshItems, plugin.getSshInjector(), plugin);
+      attachItem(httpItems, plugin.getHttpInjector(), plugin);
 
       attachSet(sysSets, plugin.getSysInjector(), plugin);
       attachSet(sshSets, plugin.getSshInjector(), plugin);
@@ -220,6 +233,16 @@
     } finally {
       exit(oldContext);
     }
+
+    for (StartPluginListener l : onStart) {
+      l.onStartPlugin(plugin);
+    }
+  }
+
+  void onStopPlugin(Plugin plugin) {
+    for (StopPluginListener l : onStop) {
+      l.onStopPlugin(plugin);
+    }
   }
 
   private void attachItem(Map<TypeLiteral<?>, DynamicItem<?>> items,
@@ -250,10 +273,6 @@
   }
 
   void onReloadPlugin(Plugin oldPlugin, Plugin newPlugin) {
-    for (ReloadPluginListener l : onReload) {
-      l.onReloadPlugin(oldPlugin, newPlugin);
-    }
-
     // Index all old registrations by the raw type. These may be replaced
     // during the reattach calls below. Any that are not replaced will be
     // removed when the old plugin does its stop routine.
@@ -274,9 +293,15 @@
       reattachSet(old, httpSets, newPlugin.getHttpInjector(), newPlugin);
 
       reattachItem(old, sysItems, newPlugin.getSysInjector(), newPlugin);
+      reattachItem(old, sshItems, newPlugin.getSshInjector(), newPlugin);
+      reattachItem(old, httpItems, newPlugin.getHttpInjector(), newPlugin);
     } finally {
       exit(oldContext);
     }
+
+    for (ReloadPluginListener l : onReload) {
+      l.onReloadPlugin(oldPlugin, newPlugin);
+    }
   }
 
   private void reattachMap(
@@ -459,9 +484,13 @@
 
   private Module copy(Injector src) {
     Set<TypeLiteral<?>> dynamicTypes = Sets.newHashSet();
+    Set<TypeLiteral<?>> dynamicItemTypes = Sets.newHashSet();
     for (Map.Entry<Key<?>, Binding<?>> e : src.getBindings().entrySet()) {
       TypeLiteral<?> type = e.getKey().getTypeLiteral();
-      if (type.getRawType() == DynamicSet.class
+      if (type.getRawType() == DynamicItem.class) {
+        ParameterizedType t = (ParameterizedType) type.getType();
+        dynamicItemTypes.add(TypeLiteral.get(t.getActualTypeArguments()[0]));
+      } else if (type.getRawType() == DynamicSet.class
           || type.getRawType() == DynamicMap.class) {
         ParameterizedType t = (ParameterizedType) type.getType();
         dynamicTypes.add(TypeLiteral.get(t.getActualTypeArguments()[0]));
@@ -478,6 +507,8 @@
         // using DynamicSet<F> or DynamicMap<F> internally. That should be
         // exported to plugins.
         continue;
+      } else if (dynamicItemTypes.contains(e.getKey().getTypeLiteral())) {
+        continue;
       } else if (shouldCopy(e.getKey())) {
         bindings.put(e.getKey(), e.getValue());
       }
@@ -485,6 +516,12 @@
     bindings.remove(Key.get(Injector.class));
     bindings.remove(Key.get(java.util.logging.Logger.class));
 
+    final @Nullable Binding<HttpServletRequest> requestBinding =
+        src.getExistingBinding(Key.get(HttpServletRequest.class));
+
+    final @Nullable Binding<HttpServletResponse> responseBinding =
+        src.getExistingBinding(Key.get(HttpServletResponse.class));
+
     return new AbstractModule() {
       @SuppressWarnings("unchecked")
       @Override
@@ -494,6 +531,17 @@
           Binding<Object> b = (Binding<Object>) e.getValue();
           bind(k).toProvider(b.getProvider());
         }
+
+        if (requestBinding != null) {
+          bind(HttpServletRequest.class)
+              .annotatedWith(RootRelative.class)
+              .toProvider(requestBinding.getProvider());
+        }
+        if (responseBinding != null) {
+          bind(HttpServletResponse.class)
+              .annotatedWith(RootRelative.class)
+              .toProvider(responseBinding.getProvider());
+        }
       }
     };
   }
@@ -509,6 +557,9 @@
     if (StartPluginListener.class.isAssignableFrom(type)) {
       return false;
     }
+    if (StopPluginListener.class.isAssignableFrom(type)) {
+      return false;
+    }
 
     if (type.getName().startsWith("com.google.inject.")) {
       return false;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
index 732da4b..003da6c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
@@ -18,7 +18,6 @@
 import com.google.common.base.Joiner;
 import com.google.common.base.Objects;
 import com.google.common.base.Predicate;
-import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.LinkedHashMultimap;
 import com.google.common.collect.Lists;
@@ -31,12 +30,13 @@
 import com.google.gerrit.extensions.systemstatus.ServerInformation;
 import com.google.gerrit.extensions.webui.JavaScriptPlugin;
 import com.google.gerrit.server.PluginUser;
+import com.google.gerrit.server.cache.PersistentCacheFactory;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.plugins.ServerPluginProvider.PluginDescription;
 import com.google.inject.Inject;
-import com.google.inject.Module;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
@@ -47,38 +47,36 @@
 
 import java.io.File;
 import java.io.FileFilter;
-import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.net.URLClassLoader;
-import java.text.SimpleDateFormat;
+import java.util.AbstractMap;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Date;
+import java.util.Comparator;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Queue;
 import java.util.Set;
+import java.util.TreeSet;
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.TimeUnit;
-import java.util.jar.Attributes;
-import java.util.jar.JarFile;
-import java.util.jar.Manifest;
 
 @Singleton
 public class PluginLoader implements LifecycleListener {
   static final String PLUGIN_TMP_PREFIX = "plugin_";
   static final Logger log = LoggerFactory.getLogger(PluginLoader.class);
 
+  public String getPluginName(File srcFile) throws IOException {
+    return Objects.firstNonNull(getGerritPluginName(srcFile), nameOf(srcFile));
+  }
+
   private final File pluginsDir;
   private final File dataDir;
-  private final File tmpDir;
   private final PluginGuiceEnvironment env;
   private final ServerInformationImpl srvInfoImpl;
   private final PluginUser.Factory pluginUserFactory;
@@ -90,7 +88,9 @@
   private final Provider<PluginCleanerTask> cleaner;
   private final PluginScannerThread scanner;
   private final Provider<String> urlProvider;
+  private final PersistentCacheFactory persistentCacheFactory;
   private final boolean remoteAdmin;
+  private final UniversalServerPluginProvider serverPluginFactory;
 
   @Inject
   public PluginLoader(SitePaths sitePaths,
@@ -99,10 +99,11 @@
       PluginUser.Factory puf,
       Provider<PluginCleanerTask> pct,
       @GerritServerConfig Config cfg,
-      @CanonicalWebUrl Provider<String> provider) {
+      @CanonicalWebUrl Provider<String> provider,
+      PersistentCacheFactory cacheFactory,
+      UniversalServerPluginProvider pluginFactory) {
     pluginsDir = sitePaths.plugins_dir;
     dataDir = sitePaths.data_dir;
-    tmpDir = sitePaths.tmp_dir;
     env = pe;
     srvInfoImpl = sii;
     pluginUserFactory = puf;
@@ -113,6 +114,8 @@
     cleanupHandles = Maps.newConcurrentMap();
     cleaner = pct;
     urlProvider = provider;
+    persistentCacheFactory = cacheFactory;
+    serverPluginFactory = pluginFactory;
 
     remoteAdmin =
         cfg.getBoolean("plugins", null, "allowRemoteAdmin", false);
@@ -143,7 +146,7 @@
     if (!all) {
       return running.values();
     } else {
-      ArrayList<Plugin> plugins = new ArrayList<Plugin>(running.values());
+      List<Plugin> plugins = new ArrayList<>(running.values());
       plugins.addAll(disabled.values());
       return plugins;
     }
@@ -154,9 +157,6 @@
     checkRemoteInstall();
 
     String fileName = originalName;
-    if (!(fileName.endsWith(".jar") || fileName.endsWith(".js"))) {
-      fileName += ".jar";
-    }
     File tmp = asTemp(in, ".next_" + fileName + "_", ".tmp", pluginsDir);
     String name = Objects.firstNonNull(getGerritPluginName(tmp),
         nameOf(fileName));
@@ -194,32 +194,18 @@
     }
   }
 
-  public static File storeInTemp(String pluginName, InputStream in,
-      SitePaths sitePaths) throws IOException {
-    if (!sitePaths.tmp_dir.exists()) {
-      sitePaths.tmp_dir.mkdirs();
-    }
-    return asTemp(in, tempNameFor(pluginName), ".jar", sitePaths.tmp_dir);
-  }
-
-  private static File asTemp(InputStream in,
-      String prefix, String suffix,
-      File dir) throws IOException {
+  static File asTemp(InputStream in, String prefix, String suffix, File dir)
+      throws IOException {
     File tmp = File.createTempFile(prefix, suffix, dir);
     boolean keep = false;
-    try {
-      FileOutputStream out = new FileOutputStream(tmp);
-      try {
-        byte[] data = new byte[8192];
-        int n;
-        while ((n = in.read(data)) > 0) {
-          out.write(data, 0, n);
-        }
-        keep = true;
-        return tmp;
-      } finally {
-        out.close();
+    try (FileOutputStream out = new FileOutputStream(tmp)) {
+      byte[] data = new byte[8192];
+      int n;
+      while ((n = in.read(data)) > 0) {
+        out.write(data, 0, n);
       }
+      keep = true;
+      return tmp;
     } finally {
       if (!keep) {
         tmp.delete();
@@ -228,9 +214,11 @@
   }
 
   synchronized private void unloadPlugin(Plugin plugin) {
+    persistentCacheFactory.onStop(plugin);
     String name = plugin.getName();
     log.info(String.format("Unloading plugin %s", name));
     plugin.stop(env);
+    env.onStopPlugin(plugin);
     running.remove(name);
     disabled.remove(name);
     toCleanup.add(plugin);
@@ -365,24 +353,30 @@
   }
 
   public synchronized void rescan() {
-    Multimap<String, File> jars = prunePlugins(pluginsDir);
-    if (jars.isEmpty()) {
+    Multimap<String, File> pluginsFiles = prunePlugins(pluginsDir);
+    if (pluginsFiles.isEmpty()) {
       return;
     }
 
-    syncDisabledPlugins(jars);
+    syncDisabledPlugins(pluginsFiles);
 
-    Map<String, File> activePlugins = filterDisabled(jars);
-    for (Map.Entry<String, File> entry : activePlugins.entrySet()) {
+    Map<String, File> activePlugins = filterDisabled(pluginsFiles);
+    for (Map.Entry<String, File> entry : jarsFirstSortedPluginsSet(activePlugins)) {
       String name = entry.getKey();
-      File jar = entry.getValue();
+      File file = entry.getValue();
+      String fileName = file.getName();
+      if (!isJsPlugin(fileName) && !serverPluginFactory.handles(file)) {
+        log.warn("No Plugin provider was found that handles this file format: {}", fileName);
+        continue;
+      }
+
       FileSnapshot brokenTime = broken.get(name);
-      if (brokenTime != null && !brokenTime.isModified(jar)) {
+      if (brokenTime != null && !brokenTime.isModified(file)) {
         continue;
       }
 
       Plugin active = running.get(name);
-      if (active != null && !active.isModified(jar)) {
+      if (active != null && !active.isModified(file)) {
         continue;
       }
 
@@ -392,7 +386,7 @@
       }
 
       try {
-        Plugin loadedPlugin = runPlugin(name, jar, active);
+        Plugin loadedPlugin = runPlugin(name, file, active);
         if (active == null && !loadedPlugin.isDisabled()) {
           log.info(String.format("Loaded plugin %s, version %s",
               loadedPlugin.getName(), loadedPlugin.getVersion()));
@@ -405,6 +399,38 @@
     cleanInBackground();
   }
 
+  private void addAllEntries(Map<String, File> from,
+      TreeSet<Entry<String, File>> to) {
+    Iterator<Entry<String, File>> it = from.entrySet().iterator();
+    while (it.hasNext()) {
+      Entry<String,File> entry = it.next();
+      to.add(new AbstractMap.SimpleImmutableEntry<String, File>(
+          entry.getKey(), entry.getValue()));
+    }
+  }
+
+  private TreeSet<Entry<String, File>> jarsFirstSortedPluginsSet(
+      Map<String, File> activePlugins) {
+    TreeSet<Entry<String, File>> sortedPlugins =
+        Sets.newTreeSet(new Comparator<Entry<String, File>>() {
+          @Override
+          public int compare(Entry<String, File> entry1,
+              Entry<String, File> entry2) {
+            String file1 = entry1.getValue().getName();
+            String file2 = entry2.getValue().getName();
+            int cmp = file1.compareTo(file2);
+            if (file1.endsWith(".jar")) {
+              return (file2.endsWith(".jar") ? cmp : -1);
+            } else {
+              return (file2.endsWith(".jar") ? +1 : cmp);
+            }
+          }
+        });
+
+    addAllEntries(activePlugins, sortedPlugins);
+    return sortedPlugins;
+  }
+
   private void syncDisabledPlugins(Multimap<String, File> jars) {
     stopRemovedPlugins(jars);
     dropRemovedDisabledPlugins(jars);
@@ -415,6 +441,15 @@
     FileSnapshot snapshot = FileSnapshot.save(plugin);
     try {
       Plugin newPlugin = loadPlugin(name, plugin, snapshot);
+      if (newPlugin.getCleanupHandle() != null) {
+        cleanupHandles.put(newPlugin, newPlugin.getCleanupHandle());
+      }
+      /*
+       * Pluggable plugin provider may have assigned a plugin name that could be
+       * actually different from the initial one assigned during scan. It is
+       * safer then to reassign it.
+       */
+      name = newPlugin.getName();
       boolean reload = oldPlugin != null
           && oldPlugin.canReload()
           && newPlugin.canReload();
@@ -504,10 +539,6 @@
     return 0 < ext ? name.substring(0, ext) : name;
   }
 
-  private static String getExtension(File file) {
-    return getExtension(file.getName());
-  }
-
   private static String getExtension(String name) {
     int ext = name.lastIndexOf('.');
     return 0 < ext ? name.substring(ext) : "";
@@ -516,88 +547,40 @@
   private Plugin loadPlugin(String name, File srcPlugin, FileSnapshot snapshot)
       throws IOException, ClassNotFoundException, InvalidPluginException {
     String pluginName = srcPlugin.getName();
-    if (isJarPlugin(pluginName)) {
-      File tmp;
-      FileInputStream in = new FileInputStream(srcPlugin);
-      String extension = getExtension(srcPlugin);
-      try {
-        tmp = asTemp(in, tempNameFor(name), extension, tmpDir);
-      } finally {
-        in.close();
-      }
-      return loadJarPlugin(name, srcPlugin, snapshot, tmp);
-    } else if (isJsPlugin(pluginName)) {
+    if (isJsPlugin(pluginName)) {
       return loadJsPlugin(name, srcPlugin, snapshot);
+    } else if (serverPluginFactory.handles(srcPlugin)) {
+      return loadServerPlugin(srcPlugin, snapshot);
     } else {
       throw new InvalidPluginException(String.format(
           "Unsupported plugin type: %s", srcPlugin.getName()));
     }
   }
 
-  private Plugin loadJarPlugin(String name, File srcJar, FileSnapshot snapshot,
-      File tmp) throws IOException, InvalidPluginException,
-      MalformedURLException, ClassNotFoundException {
-    JarFile jarFile = new JarFile(tmp);
-    boolean keep = false;
-    try {
-      Manifest manifest = jarFile.getManifest();
-      Plugin.ApiType type = Plugin.getApiType(manifest);
-      Attributes main = manifest.getMainAttributes();
-      String sysName = main.getValue("Gerrit-Module");
-      String sshName = main.getValue("Gerrit-SshModule");
-      String httpName = main.getValue("Gerrit-HttpModule");
+  private File getPluginDataDir(String name) {
+    return new File(dataDir, name);
+  }
 
-      if (!Strings.isNullOrEmpty(sshName) && type != Plugin.ApiType.PLUGIN) {
-        throw new InvalidPluginException(String.format(
-            "Using Gerrit-SshModule requires Gerrit-ApiType: %s",
-            Plugin.ApiType.PLUGIN));
-      }
-
-      List<URL> urls = new ArrayList<>(2);
-      String overlay = System.getProperty("gerrit.plugin-classes");
-      if (overlay != null) {
-        File classes = new File(new File(new File(overlay), name), "main");
-        if (classes.isDirectory()) {
-          log.info(String.format(
-              "plugin %s: including %s",
-              name, classes.getPath()));
-          urls.add(classes.toURI().toURL());
-        }
-      }
-      urls.add(tmp.toURI().toURL());
-
-      ClassLoader pluginLoader = new URLClassLoader(
-          urls.toArray(new URL[urls.size()]),
-          parentFor(type));
-      Class<? extends Module> sysModule = load(sysName, pluginLoader);
-      Class<? extends Module> sshModule = load(sshName, pluginLoader);
-      Class<? extends Module> httpModule = load(httpName, pluginLoader);
-
-      String url = String.format("%s/plugins/%s/",
-          CharMatcher.is('/').trimTrailingFrom(urlProvider.get()),
-          name);
-
-      Plugin plugin = new JarPlugin(name, url,
-          pluginUserFactory.create(name),
-          srcJar, snapshot,
-          jarFile, manifest,
-          new File(dataDir, name), type, pluginLoader,
-          sysModule, sshModule, httpModule);
-      cleanupHandles.put(plugin, new CleanupHandle(tmp, jarFile));
-      keep = true;
-      return plugin;
-    } finally {
-      if (!keep) {
-        jarFile.close();
-      }
-    }
+  private String getPluginCanonicalWebUrl(String name) {
+    String url = String.format("%s/plugins/%s/",
+        CharMatcher.is('/').trimTrailingFrom(urlProvider.get()),
+        name);
+    return url;
   }
 
   private Plugin loadJsPlugin(String name, File srcJar, FileSnapshot snapshot) {
     return new JsPlugin(name, srcJar, pluginUserFactory.create(name), snapshot);
   }
 
-  private static ClassLoader parentFor(Plugin.ApiType type)
+  private ServerPlugin loadServerPlugin(File scriptFile,
+      FileSnapshot snapshot) throws InvalidPluginException {
+    String name = serverPluginFactory.getPluginName(scriptFile);
+    return serverPluginFactory.get(scriptFile, snapshot, new PluginDescription(
+        pluginUserFactory.create(name), getPluginCanonicalWebUrl(name),
+        getPluginDataDir(name)));
+  }
+
+  static ClassLoader parentFor(Plugin.ApiType type)
       throws InvalidPluginException {
     switch (type) {
       case EXTENSION:
@@ -611,56 +594,37 @@
     }
   }
 
-  private static String tempNameFor(String name) {
-    SimpleDateFormat fmt = new SimpleDateFormat("yyMMdd_HHmm");
-    return PLUGIN_TMP_PREFIX + name + "_" + fmt.format(new Date()) + "_";
-  }
-
-  private static Class<? extends Module> load(String name, ClassLoader pluginLoader)
-      throws ClassNotFoundException {
-    if (Strings.isNullOrEmpty(name)) {
-      return null;
-    }
-
-    @SuppressWarnings("unchecked")
-    Class<? extends Module> clazz =
-        (Class<? extends Module>) Class.forName(name, false, pluginLoader);
-    if (!Module.class.isAssignableFrom(clazz)) {
-      throw new ClassCastException(String.format(
-          "Class %s does not implement %s",
-          name, Module.class.getName()));
-    }
-    return clazz;
-  }
-
   // Only one active plugin per plugin name can exist for each plugin name.
   // Filter out disabled plugins and transform the multimap to a map
   private static Map<String, File> filterDisabled(
-      Multimap<String, File> jars) {
+      Multimap<String, File> pluginFiles) {
     Map<String, File> activePlugins = Maps.newHashMapWithExpectedSize(
-        jars.keys().size());
-    for (String name : jars.keys()) {
-      for (File jar : jars.asMap().get(name)) {
-        if (!jar.getName().endsWith(".disabled")) {
+        pluginFiles.keys().size());
+    for (String name : pluginFiles.keys()) {
+      for (File pluginFile : pluginFiles.asMap().get(name)) {
+        if (!pluginFile.getName().endsWith(".disabled")) {
           assert(!activePlugins.containsKey(name));
-          activePlugins.put(name, jar);
+          activePlugins.put(name, pluginFile);
         }
       }
     }
     return activePlugins;
   }
 
-  // Scan the $site_path/plugins directory and fetch all files that end
-  // with *.jar. The Key in returned multimap is the plugin name. Values are
-  // the files. Plugins can optionally provide their name in MANIFEST file.
+  // Scan the $site_path/plugins directory and fetch all files and directories.
+  // The Key in returned multimap is the plugin name initially assigned from its filename.
+  // Values are the files. Plugins can optionally provide their name in MANIFEST file.
   // If multiple plugin files provide the same plugin name, then only
   // the first plugin remains active and all other plugins with the same
   // name are disabled.
-  private static Multimap<String, File> prunePlugins(File pluginsDir) {
-    List<File> jars = scanJarsInPluginsDirectory(pluginsDir);
+  //
+  // NOTE: Bear in mind that the plugin name can be reassigned after load by the
+  //       Server plugin provider.
+  public Multimap<String, File> prunePlugins(File pluginsDir) {
+    List<File> pluginFiles = scanFilesInPluginsDirectory(pluginsDir);
     Multimap<String, File> map;
     try {
-      map = asMultimap(jars);
+      map = asMultimap(pluginFiles);
       for (String plugin : map.keySet()) {
         Collection<File> files = map.asMap().get(plugin);
         if (files.size() == 1) {
@@ -700,7 +664,7 @@
     return map;
   }
 
-  private static List<File> scanJarsInPluginsDirectory(File pluginsDir) {
+  private List<File> scanFilesInPluginsDirectory(File pluginsDir) {
     if (pluginsDir == null || !pluginsDir.exists()) {
       return Collections.emptyList();
     }
@@ -708,10 +672,8 @@
       @Override
       public boolean accept(File pathname) {
         String n = pathname.getName();
-        return (isJarPlugin(n) || isJsPlugin(n))
-            && !n.startsWith(".last_")
-            && !n.startsWith(".next_")
-            && pathname.isFile();
+        return !n.startsWith(".last_")
+            && !n.startsWith(".next_");
       }
     });
     if (matches == null) {
@@ -731,37 +693,26 @@
     });
   }
 
-  public static String getGerritPluginName(File srcFile) throws IOException {
+  public String getGerritPluginName(File srcFile) throws IOException {
     String fileName = srcFile.getName();
-    if (isJarPlugin(fileName)) {
-      JarFile jarFile = new JarFile(srcFile);
-      try {
-        return jarFile.getManifest().getMainAttributes()
-            .getValue("Gerrit-PluginName");
-      } finally {
-        jarFile.close();
-      }
-    }
     if (isJsPlugin(fileName)) {
       return fileName.substring(0, fileName.length() - 3);
     }
+    if (serverPluginFactory.handles(srcFile)) {
+      return serverPluginFactory.getPluginName(srcFile);
+    }
     return null;
   }
 
-  private static Multimap<String, File> asMultimap(List<File> plugins)
+  private Multimap<String, File> asMultimap(List<File> plugins)
       throws IOException {
     Multimap<String, File> map = LinkedHashMultimap.create();
     for (File srcFile : plugins) {
-      map.put(Objects.firstNonNull(getGerritPluginName(srcFile),
-          nameOf(srcFile)), srcFile);
+      map.put(getPluginName(srcFile), srcFile);
     }
     return map;
   }
 
-  private static boolean isJarPlugin(String name) {
-    return isPlugin(name, "jar");
-  }
-
   private static boolean isJsPlugin(String name) {
     return isPlugin(name, "js");
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginModule.java
index 055baf1..32722da 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginModule.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.plugins;
 
+import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.systemstatus.ServerInformation;
 import com.google.gerrit.lifecycle.LifecycleModule;
 
@@ -28,5 +29,10 @@
     bind(PluginLoader.class);
     bind(CopyConfigModule.class);
     listener().to(PluginLoader.class);
+
+    DynamicSet.setOf(binder(), ServerPluginProvider.class);
+    DynamicSet.bind(binder(), ServerPluginProvider.class).to(
+        JarPluginProvider.class);
+    bind(UniversalServerPluginProvider.class);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginsCollection.java
index 076bfdd..a4834ba 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginsCollection.java
@@ -24,7 +24,9 @@
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
+@Singleton
 public class PluginsCollection implements
     RestCollection<TopLevelResource, PluginResource>,
     AcceptsCreate<TopLevelResource> {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ReloadPlugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ReloadPlugin.java
index 32e8b24..e9c5aa2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ReloadPlugin.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ReloadPlugin.java
@@ -22,11 +22,13 @@
 import com.google.gerrit.server.plugins.ListPlugins.PluginInfo;
 import com.google.gerrit.server.plugins.ReloadPlugin.Input;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
 import java.io.PrintWriter;
 import java.io.StringWriter;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@Singleton
 class ReloadPlugin implements RestModifyView<PluginResource, Input> {
   static class Input {
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarPlugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPlugin.java
similarity index 77%
rename from gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarPlugin.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPlugin.java
index 1568997..ee5e90a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarPlugin.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPlugin.java
@@ -35,12 +35,12 @@
 import org.eclipse.jgit.internal.storage.file.FileSnapshot;
 
 import java.io.File;
+import java.io.IOException;
 import java.util.List;
 import java.util.jar.Attributes;
-import java.util.jar.JarFile;
 import java.util.jar.Manifest;
 
-class JarPlugin extends Plugin {
+public class ServerPlugin extends Plugin {
 
   /** Unique key that changes whenever a plugin reloads. */
   public static final class CacheKey {
@@ -57,8 +57,8 @@
     }
   }
 
-  private final JarFile jarFile;
   private final Manifest manifest;
+  private final PluginContentScanner scanner;
   private final File dataDir;
   private final String pluginCanonicalWebUrl;
   private final ClassLoader classLoader;
@@ -72,34 +72,74 @@
   private LifecycleManager manager;
   private List<ReloadableRegistrationHandle<?>> reloadableHandles;
 
-  public JarPlugin(String name,
+  public ServerPlugin(String name,
       String pluginCanonicalWebUrl,
       PluginUser pluginUser,
       File srcJar,
       FileSnapshot snapshot,
-      JarFile jarFile,
-      Manifest manifest,
+      PluginContentScanner scanner,
       File dataDir,
-      ApiType apiType,
-      ClassLoader classLoader,
-      @Nullable Class<? extends Module> sysModule,
-      @Nullable Class<? extends Module> sshModule,
-      @Nullable Class<? extends Module> httpModule) {
-    super(name, srcJar, pluginUser, snapshot, apiType);
+      ClassLoader classLoader) throws InvalidPluginException {
+    super(name, srcJar, pluginUser, snapshot, Plugin.getApiType(getPluginManifest(scanner)));
     this.pluginCanonicalWebUrl = pluginCanonicalWebUrl;
-    this.jarFile = jarFile;
-    this.manifest = manifest;
+    this.scanner = scanner;
     this.dataDir = dataDir;
     this.classLoader = classLoader;
-    this.sysModule = sysModule;
-    this.sshModule = sshModule;
-    this.httpModule = httpModule;
+    this.manifest = getPluginManifest(scanner);
+    loadGuiceModules(manifest, classLoader);
+  }
+
+  private void loadGuiceModules(Manifest manifest, ClassLoader classLoader) throws InvalidPluginException {
+    Attributes main = manifest.getMainAttributes();
+    String sysName = main.getValue("Gerrit-Module");
+    String sshName = main.getValue("Gerrit-SshModule");
+    String httpName = main.getValue("Gerrit-HttpModule");
+
+    if (!Strings.isNullOrEmpty(sshName) && getApiType() != Plugin.ApiType.PLUGIN) {
+      throw new InvalidPluginException(String.format(
+          "Using Gerrit-SshModule requires Gerrit-ApiType: %s",
+          Plugin.ApiType.PLUGIN));
+    }
+
+    try {
+      this.sysModule = load(sysName, classLoader);
+      this.sshModule = load(sshName, classLoader);
+      this.httpModule = load(httpName, classLoader);
+    } catch(ClassNotFoundException e) {
+      throw new InvalidPluginException("Unable to load plugin Guice Modules", e);
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  private static Class<? extends Module> load(String name, ClassLoader pluginLoader)
+      throws ClassNotFoundException {
+    if (Strings.isNullOrEmpty(name)) {
+      return null;
+    }
+
+    Class<?> clazz =
+        Class.forName(name, false, pluginLoader);
+    if (!Module.class.isAssignableFrom(clazz)) {
+      throw new ClassCastException(String.format(
+          "Class %s does not implement %s",
+          name, Module.class.getName()));
+    }
+    return (Class<? extends Module>) clazz;
   }
 
   File getSrcJar() {
     return getSrcFile();
   }
 
+  private static Manifest getPluginManifest(PluginContentScanner scanner)
+      throws InvalidPluginException {
+    try {
+       return scanner.getManifest();
+    } catch (IOException e) {
+      throw new InvalidPluginException("Cannot get plugin manifest", e);
+    }
+  }
+
   @Nullable
   public String getVersion() {
     Attributes main = manifest.getMainAttributes();
@@ -136,7 +176,7 @@
 
     AutoRegisterModules auto = null;
     if (sysModule == null && sshModule == null && httpModule == null) {
-      auto = new AutoRegisterModules(getName(), env, jarFile, classLoader);
+      auto = new AutoRegisterModules(getName(), env, scanner, classLoader);
       auto.discover();
     }
 
@@ -241,10 +281,6 @@
     }
   }
 
-  public JarFile getJarFile() {
-    return jarFile;
-  }
-
   public Injector getSysInjector() {
     return sysInjector;
   }
@@ -270,4 +306,9 @@
       manager.add(handle);
     }
   }
+
+  @Override
+  public PluginContentScanner getContentScanner() {
+    return scanner;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPluginProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPluginProvider.java
new file mode 100644
index 0000000..37fed9b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPluginProvider.java
@@ -0,0 +1,107 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.plugins;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.server.PluginUser;
+
+import org.eclipse.jgit.internal.storage.file.FileSnapshot;
+
+import java.io.File;
+
+/**
+ * Provider of one Server plugin from one external file
+ *
+ * Allows to load one plugin from one external file or
+ * one directory by declaring the ability to handle it.
+ *
+ * In order to load multiple files into a single plugin,
+ * group them into a directory tree and then load the directory
+ * root as a single plugin.
+ */
+@ExtensionPoint
+public interface ServerPluginProvider {
+
+  /**
+   * Descriptor of the Plugin that ServerPluginProvider has to load.
+   */
+  public class PluginDescription {
+    public final PluginUser user;
+    public final String canonicalUrl;
+    public final File dataDir;
+
+    /**
+     * Creates a new PluginDescription for ServerPluginProvider.
+     *
+     * @param user Gerrit user for interacting with plugins
+     * @param canonicalUrl plugin root Web URL
+     * @param dataDir directory for plugin data
+     */
+    public PluginDescription(PluginUser user, String canonicalUrl, File dataDir) {
+      this.user = user;
+      this.canonicalUrl = canonicalUrl;
+      this.dataDir = dataDir;
+    }
+  }
+
+  /**
+   * Declares the availability to manage an external file or directory
+   *
+   * @param srcFile the external file or directory
+   * @return true if file or directory can be loaded into a Server Plugin
+   */
+  boolean handles(File srcFile);
+
+  /**
+   * Returns the plugin name of an external file or directory
+   *
+   * Should be called only if {@link #handles(File) handles(srcFile)}
+   * returns true and thus srcFile is a supported plugin format.
+   * An IllegalArgumentException is thrown otherwise as srcFile
+   * is not a valid file format for extracting its plugin name.
+   *
+   * @param srcFile external file or directory
+   * @return plugin name
+   */
+  String getPluginName(File srcFile);
+
+  /**
+   * Loads an external file or directory into a Server plugin.
+   *
+   * Should be called only if {@link #handles(File) handles(srcFile)}
+   * returns true and thus srcFile is a supported plugin format.
+   * An IllegalArgumentException is thrown otherwise as srcFile
+   * is not a valid file format for extracting its plugin name.
+   *
+   * @param srcFile external file or directory
+   * @param snapshot snapshot of the external file
+   * @param pluginDescriptor descriptor of the ServerPlugin to load
+   * @throws InvalidPluginException if plugin is supposed to be handled
+   *         but cannot be loaded for any other reason
+   */
+  ServerPlugin get(File srcFile, FileSnapshot snapshot,
+      PluginDescription pluginDescriptor) throws InvalidPluginException;
+
+  /**
+   * Returns the plugin name of this provider.
+   *
+   * Allows to identify which plugin provided the current ServerPluginProvider
+   * by returning the plugin name. Helpful for troubleshooting plugin loading
+   * problems.
+   *
+   * @return plugin name of this provider
+   */
+  String getProviderPluginName();
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_58.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/StopPluginListener.java
similarity index 65%
copy from gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_58.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/plugins/StopPluginListener.java
index 72b0c8b..24bd655 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_58.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/StopPluginListener.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2011 The Android Open Source Project
+// Copyright (C) 2014 The Android Open 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,14 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.schema;
+package com.google.gerrit.server.plugins;
 
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_58 extends SchemaVersion {
-  @Inject
-  Schema_58(Provider<Schema_57> prior) {
-    super(prior);
-  }
+/** Broadcasts event indicating a plugin was unloaded. */
+public interface StopPluginListener {
+  public void onStopPlugin(Plugin plugin);
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/UniversalServerPluginProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/UniversalServerPluginProvider.java
new file mode 100644
index 0000000..0e8bd87
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/UniversalServerPluginProvider.java
@@ -0,0 +1,98 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.plugins;
+
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.internal.storage.file.FileSnapshot;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+@Singleton
+class UniversalServerPluginProvider implements ServerPluginProvider {
+  private static final Logger log = LoggerFactory.getLogger(UniversalServerPluginProvider.class);
+
+  private final DynamicSet<ServerPluginProvider> serverPluginProviders;
+
+  @Inject
+  UniversalServerPluginProvider(DynamicSet<ServerPluginProvider> sf) {
+    this.serverPluginProviders = sf;
+  }
+
+  @Override
+  public ServerPlugin get(File srcFile, FileSnapshot snapshot,
+      PluginDescription pluginDescription) throws InvalidPluginException {
+    return providerOf(srcFile).get(srcFile, snapshot, pluginDescription);
+  }
+
+  @Override
+  public String getPluginName(File srcFile) {
+    return providerOf(srcFile).getPluginName(srcFile);
+  }
+
+  @Override
+  public boolean handles(File srcFile) {
+    List<ServerPluginProvider> providers =
+        providersForHandlingPlugin(srcFile);
+    switch (providers.size()) {
+      case 1:
+        return true;
+      case 0:
+        return false;
+      default:
+        throw new MultipleProvidersForPluginException(srcFile, providers);
+    }
+  }
+
+  @Override
+  public String getProviderPluginName() {
+    return "gerrit";
+  }
+
+  private ServerPluginProvider providerOf(File srcFile) {
+    List<ServerPluginProvider> providers =
+        providersForHandlingPlugin(srcFile);
+    switch (providers.size()) {
+      case 1:
+        return providers.get(0);
+      case 0:
+        throw new IllegalArgumentException(
+            "No ServerPluginProvider found/loaded to handle plugin file "
+                + srcFile.getAbsolutePath());
+      default:
+        throw new MultipleProvidersForPluginException(srcFile, providers);
+    }
+  }
+
+  private List<ServerPluginProvider> providersForHandlingPlugin(
+      final File srcFile) {
+    List<ServerPluginProvider> providers = new ArrayList<>();
+    for (ServerPluginProvider serverPluginProvider : serverPluginProviders) {
+      boolean handles = serverPluginProvider.handles(srcFile);
+      log.debug("File {} handled by {} ? => {}", srcFile,
+          serverPluginProvider.getProviderPluginName(), handles);
+      if (handles) {
+        providers.add(serverPluginProvider);
+      }
+    }
+    return providers;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/BanCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/BanCommit.java
new file mode 100644
index 0000000..fab1ed3
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/BanCommit.java
@@ -0,0 +1,106 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Lists;
+import com.google.gerrit.common.errors.PermissionDeniedException;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.server.git.BanCommitResult;
+import com.google.gerrit.server.git.MergeException;
+import com.google.gerrit.server.project.BanCommit.Input;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
+import org.eclipse.jgit.lib.ObjectId;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+@Singleton
+public class BanCommit implements RestModifyView<ProjectResource, Input> {
+  public static class Input {
+    public List<String> commits;
+    public String reason;
+
+    public static Input fromCommits(String firstCommit, String... moreCommits) {
+      return fromCommits(Lists.asList(firstCommit, moreCommits));
+    }
+
+    public static Input fromCommits(List<String> commits) {
+      Input in = new Input();
+      in.commits = commits;
+      return in;
+    }
+  }
+
+  @Inject
+  private com.google.gerrit.server.git.BanCommit banCommit;
+
+  @Override
+  public BanResultInfo apply(ProjectResource rsrc, Input input)
+      throws UnprocessableEntityException, AuthException,
+      ResourceConflictException, IOException, InterruptedException {
+    BanResultInfo r = new BanResultInfo();
+    if (input != null && input.commits != null && !input.commits.isEmpty()) {
+      List<ObjectId> commitsToBan = new ArrayList<>(input.commits.size());
+      for (String c : input.commits) {
+        try {
+          commitsToBan.add(ObjectId.fromString(c));
+        } catch (IllegalArgumentException e) {
+          throw new UnprocessableEntityException(e.getMessage());
+        }
+      }
+
+      try {
+        BanCommitResult result =
+            banCommit.ban(rsrc.getControl(), commitsToBan, input.reason);
+        r.newlyBanned = transformCommits(result.getNewlyBannedCommits());
+        r.alreadyBanned = transformCommits(result.getAlreadyBannedCommits());
+        r.ignored = transformCommits(result.getIgnoredObjectIds());
+      } catch (PermissionDeniedException e) {
+        throw new AuthException(e.getMessage());
+      } catch (MergeException | ConcurrentRefUpdateException e) {
+        throw new ResourceConflictException(e.getMessage(), e);
+      }
+    }
+    return r;
+  }
+
+  private static List<String> transformCommits(List<ObjectId> commits) {
+    if (commits == null || commits.isEmpty()) {
+      return null;
+    }
+
+    return Lists.transform(commits,
+        new Function<ObjectId, String>() {
+          @Override
+          public String apply(ObjectId id) {
+            return id.getName();
+          }
+        });
+  }
+
+  public static class BanResultInfo {
+    public List<String> newlyBanned;
+    public List<String> alreadyBanned;
+    public List<String> ignored;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchesCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchesCollection.java
index 8ae07de..3bf3522 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchesCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchesCollection.java
@@ -22,23 +22,24 @@
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.server.project.ListBranches.BranchInfo;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import org.eclipse.jgit.lib.Constants;
 
 import java.io.IOException;
 import java.util.List;
 
+@Singleton
 public class BranchesCollection implements
     ChildCollection<ProjectResource, BranchResource>,
     AcceptsCreate<ProjectResource> {
   private final DynamicMap<RestView<BranchResource>> views;
-  private final Provider<ListBranches> list;
+  private final ListBranches list;
   private final CreateBranch.Factory createBranchFactory;
 
   @Inject
   BranchesCollection(DynamicMap<RestView<BranchResource>> views,
-      Provider<ListBranches> list, CreateBranch.Factory createBranchFactory) {
+      ListBranches list, CreateBranch.Factory createBranchFactory) {
     this.views = views;
     this.list = list;
     this.createBranchFactory = createBranchFactory;
@@ -46,7 +47,7 @@
 
   @Override
   public RestView<ProjectResource> list() {
-    return list.get();
+    return list;
   }
 
   @Override
@@ -57,7 +58,7 @@
         && !branchName.equals(Constants.HEAD)) {
       branchName = Constants.R_HEADS + branchName;
     }
-    List<BranchInfo> branches = list.get().apply(parent);
+    List<BranchInfo> branches = list.apply(parent);
     for (BranchInfo b : branches) {
       if (branchName.equals(b.ref)) {
         return new BranchResource(parent.getControl(), b);
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 8e3c1e9..1878c02 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
@@ -22,13 +22,13 @@
 import com.google.gerrit.common.data.RefConfigSection;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.data.SubmitTypeRecord;
+import com.google.gerrit.extensions.common.SubmitType;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.notedb.ChangeNotes;
@@ -84,20 +84,6 @@
       }
     }
 
-    public ChangeControl controlFor(Change.Id id, CurrentUser user)
-        throws NoSuchChangeException {
-      final Change change;
-      try {
-        change = db.get().changes().get(id);
-        if (change == null) {
-          throw new NoSuchChangeException(id);
-        }
-      } catch (OrmException e) {
-        throw new NoSuchChangeException(id, e);
-      }
-      return controlFor(change, user);
-    }
-
     public ChangeControl validateFor(Change change, CurrentUser user)
         throws NoSuchChangeException, OrmException {
       ChangeControl c = controlFor(change, user);
@@ -106,15 +92,6 @@
       }
       return c;
     }
-
-    public ChangeControl validateFor(Change.Id id, CurrentUser user)
-        throws NoSuchChangeException, OrmException {
-      ChangeControl c = controlFor(id, user);
-      if (!c.isVisible(db.get())) {
-        throw new NoSuchChangeException(c.getChange().getId());
-      }
-      return c;
-    }
   }
 
   public static class Factory {
@@ -170,7 +147,7 @@
     }
   }
 
-  interface AssistedFactory {
+  public interface AssistedFactory {
     ChangeControl create(RefControl refControl, Change change);
     ChangeControl create(RefControl refControl, ChangeNotes notes);
   }
@@ -188,29 +165,25 @@
     }
   }
 
-  private final ApprovalsUtil approvalsUtil;
   private final ChangeData.Factory changeDataFactory;
   private final RefControl refControl;
   private final ChangeNotes notes;
 
   @AssistedInject
   ChangeControl(
-      ApprovalsUtil approvalsUtil,
       ChangeData.Factory changeDataFactory,
       ChangeNotes.Factory notesFactory,
       @Assisted RefControl refControl,
       @Assisted Change change) {
-    this(approvalsUtil, changeDataFactory, refControl,
+    this(changeDataFactory, refControl,
         notesFactory.create(change));
   }
 
   @AssistedInject
   ChangeControl(
-      ApprovalsUtil approvalsUtil,
       ChangeData.Factory changeDataFactory,
       @Assisted RefControl refControl,
       @Assisted ChangeNotes notes) {
-    this.approvalsUtil = approvalsUtil;
     this.changeDataFactory = changeDataFactory;
     this.refControl = refControl;
     this.notes = notes;
@@ -220,7 +193,7 @@
     if (getCurrentUser().equals(who)) {
       return this;
     }
-    return new ChangeControl(approvalsUtil, changeDataFactory,
+    return new ChangeControl(changeDataFactory,
         getRefControl().forUser(who), notes);
   }
 
@@ -264,7 +237,7 @@
 
   /** Can this user see the given patchset? */
   public boolean isPatchVisible(PatchSet ps, ReviewDb db) throws OrmException {
-    if (ps.isDraft() && !isDraftVisible(db, null)) {
+    if (ps != null && ps.isDraft() && !isDraftVisible(db, null)) {
       return false;
     }
     return isVisible(db);
@@ -509,7 +482,7 @@
    * the out collection is reversed to restore it to the original ordering.
    */
   public List<SubmitRecord> resultsToSubmitRecord(Term submitRule, List<Term> results) {
-    List<SubmitRecord> out = new ArrayList<SubmitRecord>(results.size());
+    List<SubmitRecord> out = new ArrayList<>(results.size());
     for (int resultIdx = results.size() - 1; 0 <= resultIdx; resultIdx--) {
       Term submitRecord = results.get(resultIdx);
       SubmitRecord rec = new SubmitRecord();
@@ -538,7 +511,7 @@
         return logInvalidResult(submitRule, submitRecord);
       }
 
-      rec.labels = new ArrayList<SubmitRecord.Label> (submitRecord.arity());
+      rec.labels = new ArrayList<>(submitRecord.arity());
 
       for (Term state : ((StructureTerm) submitRecord).args()) {
         if (!state.isStructure() || 2 != state.arity() || !"label".equals(state.name())) {
@@ -640,7 +613,7 @@
     String typeName = ((SymbolTerm)typeTerm).name();
     try {
       return SubmitTypeRecord.OK(
-          Project.SubmitType.valueOf(typeName.toUpperCase()));
+          SubmitType.valueOf(typeName.toUpperCase()));
     } catch (IllegalArgumentException e) {
       return logInvalidType(evaluator.getSubmitRule(), typeName);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectsCollection.java
index 27f1648..ca98cf6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectsCollection.java
@@ -23,18 +23,20 @@
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import java.io.IOException;
 
+@Singleton
 public class ChildProjectsCollection implements
     ChildCollection<ProjectResource, ChildProjectResource> {
   private final Provider<ListChildProjects> list;
-  private final Provider<ProjectsCollection> projectsCollection;
+  private final ProjectsCollection projectsCollection;
   private final DynamicMap<RestView<ChildProjectResource>> views;
 
   @Inject
   ChildProjectsCollection(Provider<ListChildProjects> list,
-      Provider<ProjectsCollection> projectsCollection,
+      ProjectsCollection projectsCollection,
       DynamicMap<RestView<ChildProjectResource>> views) {
     this.list = list;
     this.projectsCollection = projectsCollection;
@@ -51,7 +53,7 @@
   public ChildProjectResource parse(ProjectResource parent, IdString id)
       throws ResourceNotFoundException, IOException {
     ProjectResource p =
-        projectsCollection.get().parse(TopLevelResource.INSTANCE, id);
+        projectsCollection.parse(TopLevelResource.INSTANCE, id);
     for (ProjectState pp : p.getControl().getProjectState().parents()) {
       if (parent.getNameKey().equals(pp.getProject().getNameKey())) {
         return new ChildProjectResource(parent, p.getControl());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CommitResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommitResource.java
new file mode 100644
index 0000000..fc9c807
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommitResource.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.inject.TypeLiteral;
+
+import org.eclipse.jgit.revwalk.RevCommit;
+
+public class CommitResource extends ProjectResource {
+  public static final TypeLiteral<RestView<CommitResource>> COMMIT_KIND =
+      new TypeLiteral<RestView<CommitResource>>() {};
+
+  private final RevCommit commit;
+
+  public CommitResource(ProjectControl control, RevCommit commit) {
+    super(control);
+    this.commit = commit;
+  }
+
+  public RevCommit getCommit() {
+    return commit;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CommitsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommitsCollection.java
new file mode 100644
index 0000000..f82142a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommitsCollection.java
@@ -0,0 +1,89 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+import java.io.IOException;
+
+@Singleton
+public class CommitsCollection implements
+    ChildCollection<ProjectResource, CommitResource> {
+  private final DynamicMap<RestView<CommitResource>> views;
+  private final GitRepositoryManager repoManager;
+
+  @Inject
+  public CommitsCollection(DynamicMap<RestView<CommitResource>> views,
+      GitRepositoryManager repoManager) {
+    this.views = views;
+    this.repoManager = repoManager;
+  }
+
+  @Override
+  public RestView<ProjectResource> list() throws ResourceNotFoundException {
+    throw new ResourceNotFoundException();
+  }
+
+  @Override
+  public CommitResource parse(ProjectResource parent, IdString id)
+      throws ResourceNotFoundException, IOException {
+    ObjectId objectId;
+    try {
+      objectId = ObjectId.fromString(id.get());
+    } catch (IllegalArgumentException e) {
+      throw new ResourceNotFoundException(id);
+    }
+
+    Repository repo = repoManager.openRepository(parent.getNameKey());
+    try {
+      RevWalk rw = new RevWalk(repo);
+      try {
+        RevCommit commit = rw.parseCommit(objectId);
+        if (!parent.getControl().canReadCommit(rw, commit)) {
+          throw new ResourceNotFoundException(id);
+        }
+        for (int i = 0; i < commit.getParentCount(); i++) {
+          rw.parseCommit(commit.getParent(i));
+        }
+        return new CommitResource(parent.getControl(), commit);
+      } catch (MissingObjectException | IncorrectObjectTypeException e) {
+        throw new ResourceNotFoundException(id);
+      } finally {
+        rw.close();
+      }
+    } finally {
+      repo.close();
+    }
+  }
+
+  @Override
+  public DynamicMap<RestView<CommitResource>> views() {
+    return views;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfo.java
index bb620a0..358e023 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfo.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfo.java
@@ -18,13 +18,13 @@
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Maps;
 import com.google.gerrit.extensions.common.ActionInfo;
+import com.google.gerrit.extensions.common.InheritableBoolean;
+import com.google.gerrit.extensions.common.SubmitType;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.registration.DynamicMap.Entry;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
-import com.google.gerrit.reviewdb.client.Project.SubmitType;
 import com.google.gerrit.server.config.AllProjectsNameProvider;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gerrit.server.config.PluginConfigFactory;
@@ -39,8 +39,6 @@
 import java.util.TreeMap;
 
 public class ConfigInfo {
-  public final String kind = "gerritcodereview#project_config";
-
   public String description;
   public InheritedBooleanInfo useContributorAgreements;
   public InheritedBooleanInfo useContentMerge;
@@ -48,7 +46,7 @@
   public InheritedBooleanInfo requireChangeId;
   public MaxObjectSizeLimitInfo maxObjectSizeLimit;
   public SubmitType submitType;
-  public Project.State state;
+  public com.google.gerrit.extensions.api.projects.ProjectState state;
   public Map<String, Map<String, ConfigParameterInfo>> pluginConfig;
   public Map<String, ActionInfo> actions;
 
@@ -108,7 +106,7 @@
     this.maxObjectSizeLimit = maxObjectSizeLimit;
 
     this.submitType = p.getSubmitType();
-    this.state = p.getState() != Project.State.ACTIVE ? p.getState() : null;
+    this.state = p.getState() != com.google.gerrit.extensions.api.projects.ProjectState.ACTIVE ? p.getState() : null;
 
     this.commentlinks = Maps.newLinkedHashMap();
     for (CommentLinkInfo cl : projectState.getCommentLinks()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java
index 3dcf9f4..4a3b415 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java
@@ -14,8 +14,7 @@
 
 package com.google.gerrit.server.project;
 
-import static org.eclipse.jgit.lib.RefDatabase.ALL;
-
+import com.google.common.collect.Iterables;
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.errors.InvalidRevisionException;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -25,6 +24,7 @@
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -41,6 +41,7 @@
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.ObjectWalk;
@@ -50,6 +51,7 @@
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.util.Collections;
 
 public class CreateBranch implements RestModifyView<ProjectResource, Input> {
   private static final Logger log = LoggerFactory.getLogger(CreateBranch.class);
@@ -214,7 +216,15 @@
       } catch (IncorrectObjectTypeException err) {
         throw new InvalidRevisionException();
       }
-      for (final Ref r : repo.getRefDatabase().getRefs(ALL).values()) {
+      RefDatabase refDb = repo.getRefDatabase();
+      Iterable<Ref> refs = Iterables.concat(
+          refDb.getRefs(Constants.R_HEADS).values(),
+          refDb.getRefs(Constants.R_TAGS).values());
+      Ref rc = refDb.getRef(RefNames.REFS_CONFIG);
+      if (rc != null) {
+        refs = Iterables.concat(refs, Collections.singleton(rc));
+      }
+      for (Ref r : refs) {
         try {
           rw.markUninteresting(rw.parseAny(r.getObjectId()));
         } catch (MissingObjectException err) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
index 5f3d2da..e69b62c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
@@ -20,6 +20,10 @@
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.errors.ProjectCreationFailedException;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.api.projects.ProjectInput;
+import com.google.gerrit.extensions.common.InheritableBoolean;
+import com.google.gerrit.extensions.common.ProjectInfo;
+import com.google.gerrit.extensions.common.SubmitType;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -30,14 +34,9 @@
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
-import com.google.gerrit.reviewdb.client.Project.SubmitType;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.group.GroupsCollection;
-import com.google.gerrit.server.project.CreateProject.Input;
-import com.google.gerrit.server.project.ProjectJson.ProjectInfo;
-import com.google.gerrit.server.project.PutConfig.ConfigValue;
 import com.google.gerrit.server.validators.ProjectCreationValidationListener;
 import com.google.gerrit.server.validators.ValidationException;
 import com.google.inject.Inject;
@@ -48,27 +47,9 @@
 
 import java.io.IOException;
 import java.util.List;
-import java.util.Map;
 
 @RequiresCapability(GlobalCapability.CREATE_PROJECT)
-public class CreateProject implements RestModifyView<TopLevelResource, Input> {
-  public static class Input {
-    public String name;
-    public String parent;
-    public String description;
-    public boolean permissionsOnly;
-    public boolean createEmptyCommit;
-    public SubmitType submitType;
-    public List<String> branches;
-    public List<String> owners;
-    public InheritableBoolean useContributorAgreements;
-    public InheritableBoolean useSignedOffBy;
-    public InheritableBoolean useContentMerge;
-    public InheritableBoolean requireChangeId;
-    public String maxObjectSizeLimit;
-    public Map<String, Map<String, ConfigValue>> pluginConfigValues;
-  }
-
+public class CreateProject implements RestModifyView<TopLevelResource, ProjectInput> {
   public static interface Factory {
     CreateProject create(String name);
   }
@@ -103,12 +84,12 @@
   }
 
   @Override
-  public Response<ProjectInfo> apply(TopLevelResource resource, Input input)
+  public Response<ProjectInfo> apply(TopLevelResource resource, ProjectInput input)
       throws BadRequestException, UnprocessableEntityException,
       ResourceConflictException, ProjectCreationFailedException,
       ResourceNotFoundException, IOException {
     if (input == null) {
-      input = new Input();
+      input = new ProjectInput();
     }
     if (input.name != null && !name.equals(input.name)) {
       throw new BadRequestException("name must match URL");
@@ -166,7 +147,7 @@
             projectControlFactory.controlFor(p.getNameKey(), currentUser.get());
         PutConfig.Input in = new PutConfig.Input();
         in.pluginConfigValues = input.pluginConfigValues;
-        putConfig.get().apply(new ProjectResource(projectControl), in);
+        putConfig.get().apply(projectControl, in);
       } catch (NoSuchProjectException e) {
         throw new ResourceNotFoundException(p.getName());
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProjectArgs.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProjectArgs.java
index ea20cea..e937e0f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProjectArgs.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProjectArgs.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.gerrit.extensions.common.InheritableBoolean;
+import com.google.gerrit.extensions.common.SubmitType;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
-import com.google.gerrit.reviewdb.client.Project.SubmitType;
 
 import java.util.List;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java
index 3df8e2f..061e992 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java
@@ -37,6 +37,7 @@
 import com.google.gson.annotations.SerializedName;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.AmbiguousObjectException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -51,9 +52,10 @@
 import java.io.UnsupportedEncodingException;
 import java.util.List;
 
+@Singleton
 class DashboardsCollection implements
     ChildCollection<ProjectResource, DashboardResource>,
-    AcceptsCreate<ProjectResource>{
+    AcceptsCreate<ProjectResource> {
   private final GitRepositoryManager gitManager;
   private final DynamicMap<RestView<DashboardResource>> views;
   private final Provider<ListDashboards> list;
@@ -203,7 +205,6 @@
   }
 
   static class DashboardInfo {
-    final String kind = "gerritcodereview#dashboard";
     String id;
     String project;
     String definingProject;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java
index 041fd4a..162a97d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java
@@ -29,6 +29,7 @@
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
@@ -37,6 +38,7 @@
 
 import java.io.IOException;
 
+@Singleton
 public class DeleteBranch implements RestModifyView<BranchResource, Input>{
   private static final Logger log = LoggerFactory.getLogger(DeleteBranch.class);
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteDashboard.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteDashboard.java
index 669f024..b525399 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteDashboard.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteDashboard.java
@@ -25,9 +25,11 @@
 import com.google.gerrit.server.project.DeleteDashboard.Input;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import java.io.IOException;
 
+@Singleton
 class DeleteDashboard implements RestModifyView<DashboardResource, Input> {
   static class Input {
     String commitMessage;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/FileResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/FileResource.java
index 29506c9..c7c6675 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/FileResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/FileResource.java
@@ -14,21 +14,33 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.server.project.BranchResource;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.inject.TypeLiteral;
 
-public class FileResource extends BranchResource {
+public class FileResource implements RestResource {
   public static final TypeLiteral<RestView<FileResource>> FILE_KIND =
       new TypeLiteral<RestView<FileResource>>() {};
 
+  private final Project.NameKey project;
+  private final String rev;
   private final String path;
 
-  public FileResource(BranchResource rsrc, String path) {
-    super(rsrc.getControl(), rsrc.getBranchInfo());
+  public FileResource(Project.NameKey project, String rev, String path) {
+    this.project = project;
+    this.rev = rev;
     this.path = path;
   }
 
+  public Project.NameKey getProject() {
+    return project;
+  }
+
+  public String getRev() {
+    return rev;
+  }
+
   public String getPath() {
     return path;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesCollection.java
index 389189e..2d0af29 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesCollection.java
@@ -21,7 +21,9 @@
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.server.project.BranchResource;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
+@Singleton
 public class FilesCollection implements
     ChildCollection<BranchResource, FileResource> {
   private final DynamicMap<RestView<FileResource>> views;
@@ -38,7 +40,7 @@
 
   @Override
   public FileResource parse(BranchResource parent, IdString id) {
-    return new FileResource(parent, id.get());
+    return new FileResource(parent.getNameKey(), parent.getRef(), id.get());
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesInCommitCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesInCommitCollection.java
new file mode 100644
index 0000000..4dc4338
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesInCommitCollection.java
@@ -0,0 +1,51 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+@Singleton
+public class FilesInCommitCollection implements
+    ChildCollection<CommitResource, FileResource> {
+  private final DynamicMap<RestView<FileResource>> views;
+
+  @Inject
+  FilesInCommitCollection(DynamicMap<RestView<FileResource>> views) {
+    this.views = views;
+  }
+
+  @Override
+  public RestView<CommitResource> list() throws ResourceNotFoundException {
+    throw new ResourceNotFoundException();
+  }
+
+  @Override
+  public FileResource parse(CommitResource parent, IdString id)
+      throws ResourceNotFoundException {
+    return new FileResource(parent.getNameKey(), parent.getCommit().getName(),
+        id.get());
+  }
+
+  @Override
+  public DynamicMap<RestView<FileResource>> views() {
+    return views;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GarbageCollect.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GarbageCollect.java
index 2911f6e..fe1086b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GarbageCollect.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GarbageCollect.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.server.git.LocalDiskRepositoryManager;
 import com.google.gerrit.server.project.GarbageCollect.Input;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
 import java.io.IOException;
 import java.io.OutputStream;
@@ -35,6 +36,7 @@
 import java.util.Collections;
 
 @RequiresCapability(GlobalCapability.RUN_GC)
+@Singleton
 public class GarbageCollect implements RestModifyView<ProjectResource, Input>,
     UiAction<ProjectResource> {
   public static class Input {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetBranch.java
index 781cf01..59b15d8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetBranch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetBranch.java
@@ -16,7 +16,9 @@
 
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.project.ListBranches.BranchInfo;
+import com.google.inject.Singleton;
 
+@Singleton
 public class GetBranch implements RestReadView<BranchResource> {
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetChildProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetChildProject.java
index 1659cb7..815653f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetChildProject.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetChildProject.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.project.ProjectJson.ProjectInfo;
 import com.google.inject.Inject;
 
 import org.kohsuke.args4j.Option;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetCommit.java
new file mode 100644
index 0000000..d43ea68
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetCommit.java
@@ -0,0 +1,64 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.gerrit.extensions.common.CommitInfo;
+import com.google.gerrit.extensions.common.GitPerson;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+import java.sql.Timestamp;
+import java.util.ArrayList;
+
+@Singleton
+public class GetCommit implements RestReadView<CommitResource> {
+
+  @Override
+  public CommitInfo apply(CommitResource rsrc) {
+    RevCommit c = rsrc.getCommit();
+    CommitInfo info = toCommitInfo(c);
+    return info;
+  }
+
+  private static CommitInfo toCommitInfo(RevCommit commit) {
+    CommitInfo info = new CommitInfo();
+    info.commit = commit.getName();
+    info.author = toGitPerson(commit.getAuthorIdent());
+    info.committer = toGitPerson(commit.getCommitterIdent());
+    info.subject = commit.getShortMessage();
+    info.message = commit.getFullMessage();
+    info.parents = new ArrayList<>(commit.getParentCount());
+    for (int i = 0; i < commit.getParentCount(); i++) {
+      RevCommit p = commit.getParent(i);
+      CommitInfo parentInfo = new CommitInfo();
+      parentInfo.commit = p.getName();
+      parentInfo.subject = p.getShortMessage();
+      info.parents.add(parentInfo);
+    }
+    return info;
+  }
+
+  private static GitPerson toGitPerson(PersonIdent ident) {
+    GitPerson gp = new GitPerson();
+    gp.name = ident.getName();
+    gp.email = ident.getEmailAddress();
+    gp.date = new Timestamp(ident.getWhen().getTime());
+    gp.tz = ident.getTimeZoneOffset();
+    return gp;
+  }
+}
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
index 88ac83f..e0bc7e6 100644
--- 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
@@ -24,7 +24,9 @@
 import com.google.gerrit.server.git.TransferConfig;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
+@Singleton
 public class GetConfig implements RestReadView<ProjectResource> {
 
   private final TransferConfig config;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetContent.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetContent.java
index 48a3ced..5a9221b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetContent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetContent.java
@@ -19,9 +19,11 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import java.io.IOException;
 
+@Singleton
 public class GetContent implements RestReadView<FileResource> {
   private final Provider<com.google.gerrit.server.change.GetContent> getContent;
 
@@ -33,7 +35,7 @@
   @Override
   public BinaryResult apply(FileResource rsrc)
       throws ResourceNotFoundException, IOException {
-    return getContent.get().apply(rsrc.getNameKey(), rsrc.getRef(),
+    return getContent.get().apply(rsrc.getProject(), rsrc.getRev(),
         rsrc.getPath());
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetDescription.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetDescription.java
index aeff72f..5241c69 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetDescription.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetDescription.java
@@ -17,7 +17,9 @@
 import com.google.common.base.Strings;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.inject.Singleton;
 
+@Singleton
 class GetDescription implements RestReadView<ProjectResource> {
   @Override
   public String apply(ProjectResource resource) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetHead.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetHead.java
index 623b8c2..29c44b5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetHead.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetHead.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
@@ -31,6 +32,7 @@
 
 import java.io.IOException;
 
+@Singleton
 public class GetHead implements RestReadView<ProjectResource> {
 
   private GitRepositoryManager repoManager;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetParent.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetParent.java
index f790b7e..8161cfd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetParent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetParent.java
@@ -18,7 +18,9 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
+@Singleton
 class GetParent implements RestReadView<ProjectResource> {
   private final AllProjectsName allProjectsName;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetProject.java
index 961f7b2..e782cb1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetProject.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetProject.java
@@ -14,10 +14,12 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.project.ProjectJson.ProjectInfo;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
+@Singleton
 class GetProject implements RestReadView<ProjectResource> {
 
   private final ProjectJson json;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetReflog.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetReflog.java
new file mode 100644
index 0000000..b6415b5
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetReflog.java
@@ -0,0 +1,136 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.common.GitPerson;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.args4j.TimestampHandler;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.ReflogEntry;
+import org.eclipse.jgit.lib.ReflogReader;
+import org.eclipse.jgit.lib.Repository;
+import org.kohsuke.args4j.Option;
+
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.List;
+
+public class GetReflog implements RestReadView<BranchResource> {
+  private final GitRepositoryManager repoManager;
+
+  @Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT",
+      usage = "maximum number of reflog entries to list")
+  public GetReflog setLimit(int limit) {
+    this.limit = limit;
+    return this;
+  }
+
+  @Option(name = "--from", metaVar = "TIMESTAMP",
+      usage = "timestamp from which the reflog entries should be listed (UTC, format: "
+          + TimestampHandler.TIMESTAMP_FORMAT + ")")
+  public GetReflog setFrom(Timestamp from) {
+    this.from = from;
+    return this;
+  }
+
+  @Option(name = "--to", metaVar = "TIMESTAMP",
+      usage = "timestamp until which the reflog entries should be listed (UTC, format: "
+          + TimestampHandler.TIMESTAMP_FORMAT + ")")
+  public GetReflog setTo(Timestamp to) {
+    this.to = to;
+    return this;
+  }
+
+  private int limit;
+  private Timestamp from;
+  private Timestamp to;
+
+  @Inject
+  public GetReflog(GitRepositoryManager repoManager) {
+    this.repoManager = repoManager;
+  }
+
+  @Override
+  public List<ReflogEntryInfo> apply(BranchResource rsrc) throws AuthException,
+      ResourceNotFoundException, RepositoryNotFoundException, IOException {
+    if (!rsrc.getControl().isOwner()) {
+      throw new AuthException("no project owner");
+    }
+
+    Repository repo = repoManager.openRepository(rsrc.getNameKey());
+    try {
+      ReflogReader r = repo.getReflogReader(rsrc.getRef());
+      if (r == null) {
+        throw new ResourceNotFoundException(rsrc.getRef());
+      }
+      List<ReflogEntry> entries;
+      if (from == null && to == null) {
+        entries =
+            limit > 0 ? r.getReverseEntries(limit) : r.getReverseEntries();
+      } else {
+        entries = limit > 0
+            ? new ArrayList<ReflogEntry>(limit)
+            : new ArrayList<ReflogEntry>();
+        for (ReflogEntry e : r.getReverseEntries()) {
+          Timestamp timestamp = new Timestamp(e.getWho().getWhen().getTime());
+          if ((from == null || from.before(timestamp)) &&
+              (to == null || to.after(timestamp))) {
+            entries.add(e);
+          }
+          if (limit > 0 && entries.size() >= limit) {
+            break;
+          }
+        }
+      }
+      return Lists.transform(entries, new Function<ReflogEntry, ReflogEntryInfo>() {
+        @Override
+        public ReflogEntryInfo apply(ReflogEntry e) {
+          return new ReflogEntryInfo(e);
+        }});
+    } finally {
+      repo.close();
+    }
+  }
+
+  public static class ReflogEntryInfo {
+    public String oldId;
+    public String newId;
+    public GitPerson who;
+    public String comment;
+
+    public ReflogEntryInfo(ReflogEntry e) {
+      oldId = e.getOldId().getName();
+      newId = e.getNewId().getName();
+
+      PersonIdent ident = e.getWho();
+      who = new GitPerson();
+      who.name = ident.getName();
+      who.email = ident.getEmailAddress();
+      who.date = new Timestamp(ident.getWhen().getTime());
+      who.tz = ident.getTimeZoneOffset();
+
+      comment = e.getComment();
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetStatistics.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetStatistics.java
index 548b85a..2b9d11f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetStatistics.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetStatistics.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
 import org.eclipse.jgit.api.GarbageCollectCommand;
 import org.eclipse.jgit.api.Git;
@@ -31,6 +32,7 @@
 import java.io.IOException;
 
 @RequiresCapability(GlobalCapability.RUN_GC)
+@Singleton
 public class GetStatistics implements RestReadView<ProjectResource> {
 
   private final GitRepositoryManager repoManager;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
index b39e362..b09e39f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
@@ -16,32 +16,43 @@
 
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
+import com.google.gerrit.extensions.common.ActionInfo;
+import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.extensions.webui.UiActions;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.google.inject.util.Providers;
 
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefDatabase;
 import org.eclipse.jgit.lib.Repository;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.TreeMap;
 
+@Singleton
 public class ListBranches implements RestReadView<ProjectResource> {
-
   private final GitRepositoryManager repoManager;
+  private final DynamicMap<RestView<BranchResource>> branchViews;
 
   @Inject
-  public ListBranches(GitRepositoryManager repoManager) {
+  public ListBranches(GitRepositoryManager repoManager,
+      DynamicMap<RestView<BranchResource>> branchViews) {
     this.repoManager = repoManager;
+    this.branchViews = branchViews;
   }
 
   @Override
@@ -61,30 +72,34 @@
     }
 
     try {
-      final Map<String, Ref> all = db.getRefDatabase().getRefs(RefDatabase.ALL);
+      List<Ref> refs =
+          new ArrayList<>(db.getRefDatabase().getRefs(Constants.R_HEADS)
+              .values());
 
-      if (!all.containsKey(Constants.HEAD)) {
-        // The branch pointed to by HEAD doesn't exist yet, so getAllRefs
-        // filtered it out. If we ask for it individually we can find the
-        // underlying target and put it into the map anyway.
-        //
         try {
           Ref head = db.getRef(Constants.HEAD);
           if (head != null) {
-            all.put(Constants.HEAD, head);
+            refs.add(head);
           }
         } catch (IOException e) {
           // Ignore the failure reading HEAD.
         }
-      }
+        try {
+          Ref config = db.getRef(RefNames.REFS_CONFIG);
+          if (config != null) {
+            refs.add(config);
+          }
+        } catch (IOException e) {
+          // Ignore the failure reading refs/meta/config.
+        }
 
-      for (final Ref ref : all.values()) {
+      for (Ref ref : refs) {
         if (ref.isSymbolic()) {
           targets.add(ref.getTarget().getName());
         }
       }
 
-      for (final Ref ref : all.values()) {
+      for (Ref ref : refs) {
         if (ref.isSymbolic()) {
           // A symbolic reference to another branch, instead of
           // showing the resolved value, show the name it references.
@@ -111,10 +126,10 @@
 
         final RefControl refControl = rsrc.getControl().controlForRef(ref.getName());
         if (refControl.isVisible()) {
-          if (ref.getName().startsWith(Constants.R_HEADS)) {
-            branches.add(createBranchInfo(ref, refControl, targets));
-          } else if (RefNames.REFS_CONFIG.equals(ref.getName())) {
+          if (RefNames.REFS_CONFIG.equals(ref.getName())) {
             configBranch = createBranchInfo(ref, refControl, targets);
+          } else {
+            branches.add(createBranchInfo(ref, refControl, targets));
           }
         }
       }
@@ -136,17 +151,28 @@
     return branches;
   }
 
-  private static BranchInfo createBranchInfo(Ref ref, RefControl refControl,
+  private BranchInfo createBranchInfo(Ref ref, RefControl refControl,
       Set<String> targets) {
-    return new BranchInfo(ref.getName(),
+    BranchInfo info = new BranchInfo(ref.getName(),
         ref.getObjectId() != null ? ref.getObjectId().name() : null,
         !targets.contains(ref.getName()) && refControl.canDelete());
+    for (UiAction.Description d : UiActions.from(
+        branchViews,
+        new BranchResource(refControl.getProjectControl(), info),
+        Providers.of(refControl.getCurrentUser()))) {
+      if (info.actions == null) {
+        info.actions = new TreeMap<>();
+      }
+      info.actions.put(d.getId(), new ActionInfo(d));
+    }
+    return info;
   }
 
   public static class BranchInfo {
     public String ref;
     public String revision;
     public Boolean canDelete;
+    public Map<String, ActionInfo> actions;
 
     public BranchInfo(String ref, String revision, boolean canDelete) {
       this.ref = ref;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListChildProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListChildProjects.java
index 5f21e70..8d1e95f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListChildProjects.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListChildProjects.java
@@ -16,11 +16,12 @@
 
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.AllProjectsName;
-import com.google.gerrit.server.project.ProjectJson.ProjectInfo;
+import com.google.gerrit.server.config.AllProjectsNameProvider;
 import com.google.inject.Inject;
 
 import org.kohsuke.args4j.Option;
@@ -40,10 +41,11 @@
   private final ProjectNode.Factory projectNodeFactory;
 
   @Inject
-  ListChildProjects(ProjectCache projectCache, AllProjectsName allProjects,
+  ListChildProjects(ProjectCache projectCache,
+      AllProjectsNameProvider allProjectsNameProvider,
       ProjectJson json, ProjectNode.Factory projectNodeFactory) {
     this.projectCache = projectCache;
-    this.allProjects = allProjects;
+    this.allProjects = allProjectsNameProvider.get();
     this.json = json;
     this.projectNodeFactory = projectNodeFactory;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
index 3f5b9e8..3f829c9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
@@ -21,22 +21,30 @@
 import com.google.common.collect.Maps;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.extensions.common.ProjectInfo;
+import com.google.gerrit.extensions.common.WebLinkInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.OutputFormat;
 import com.google.gerrit.server.StringUtil;
+import com.google.gerrit.server.WebLinks;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.project.ProjectJson.ProjectInfo;
 import com.google.gerrit.server.util.TreeFormatter;
 import com.google.gson.reflect.TypeToken;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import dk.brics.automaton.RegExp;
+import dk.brics.automaton.RunAutomaton;
 
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Constants;
@@ -105,6 +113,7 @@
   private final GroupControl.Factory groupControlFactory;
   private final GitRepositoryManager repoManager;
   private final ProjectNode.Factory projectNodeFactory;
+  private final Provider<WebLinks> webLinks;
 
   @Deprecated
   @Option(name = "--format", usage = "(deprecated) output format")
@@ -158,6 +167,11 @@
     this.matchSubstring = matchSubstring;
   }
 
+  @Option(name = "-r", metaVar = "REGEX", usage = "match project regex")
+  public void setMatchRegex(String matchRegex) {
+    this.matchRegex = matchRegex;
+  }
+
   @Option(name = "--has-acl-for", metaVar = "GROUP", usage =
       "displays only projects on which access rights for this group are directly assigned")
   public void setGroupUuid(AccountGroup.UUID groupUuid) {
@@ -173,18 +187,21 @@
   private int start;
   private String matchPrefix;
   private String matchSubstring;
+  private String matchRegex;
   private AccountGroup.UUID groupUuid;
 
   @Inject
   protected ListProjects(CurrentUser currentUser, ProjectCache projectCache,
       GroupCache groupCache, GroupControl.Factory groupControlFactory,
-      GitRepositoryManager repoManager, ProjectNode.Factory projectNodeFactory) {
+      GitRepositoryManager repoManager, ProjectNode.Factory projectNodeFactory,
+      Provider<WebLinks> webLinks) {
     this.currentUser = currentUser;
     this.projectCache = projectCache;
     this.groupCache = groupCache;
     this.groupControlFactory = groupControlFactory;
     this.repoManager = repoManager;
     this.projectNodeFactory = projectNodeFactory;
+    this.webLinks = webLinks;
   }
 
   public List<String> getShowBranch() {
@@ -209,7 +226,7 @@
   }
 
   @Override
-  public Object apply(TopLevelResource resource) {
+  public Object apply(TopLevelResource resource) throws BadRequestException {
     if (format == OutputFormat.TEXT) {
       ByteArrayOutputStream buf = new ByteArrayOutputStream();
       display(buf);
@@ -220,12 +237,13 @@
     return apply();
   }
 
-  public Map<String, ProjectInfo> apply() {
+  public Map<String, ProjectInfo> apply() throws BadRequestException {
     format = OutputFormat.JSON;
     return display(null);
   }
 
-  public Map<String, ProjectInfo> display(OutputStream displayOutputStream) {
+  public Map<String, ProjectInfo> display(OutputStream displayOutputStream)
+      throws BadRequestException {
     PrintWriter stdout = null;
     if (displayOutputStream != null) {
       try {
@@ -240,10 +258,9 @@
     int found = 0;
     Map<String, ProjectInfo> output = Maps.newTreeMap();
     Map<String, String> hiddenNames = Maps.newHashMap();
-    Set<String> rejected = new HashSet<String>();
+    Set<String> rejected = new HashSet<>();
 
-    final TreeMap<Project.NameKey, ProjectNode> treeMap =
-        new TreeMap<Project.NameKey, ProjectNode>();
+    final TreeMap<Project.NameKey, ProjectNode> treeMap = new TreeMap<>();
     try {
       for (final Project.NameKey projectName : scan()) {
         final ProjectState e = projectCache.get(projectName);
@@ -367,6 +384,13 @@
             log.warn("Unexpected error reading " + projectName, err);
             continue;
           }
+
+          info.webLinks = Lists.newArrayList();
+          for (WebLinkInfo link : webLinks.get().getProjectLinks(projectName.get())) {
+            if (!Strings.isNullOrEmpty(link.name) && !Strings.isNullOrEmpty(link.url)) {
+              info.webLinks.add(link);
+            }
+          }
         }
 
         if (foundIndex++ < start) {
@@ -403,7 +427,7 @@
       }
 
       for (ProjectInfo info : output.values()) {
-        info.finish();
+        info.id = Url.encode(info.name);
         info.name = null;
       }
       if (stdout == null) {
@@ -423,7 +447,7 @@
     }
   }
 
-  private Iterable<Project.NameKey> scan() {
+  private Iterable<Project.NameKey> scan() throws BadRequestException {
     if (matchPrefix != null) {
       return projectCache.byName(matchPrefix);
     } else if (matchSubstring != null) {
@@ -434,6 +458,28 @@
                   .contains(matchSubstring.toLowerCase(Locale.US));
             }
           });
+    } else if (matchRegex != null) {
+      if (matchRegex.startsWith("^")) {
+        matchRegex = matchRegex.substring(1);
+        if (matchRegex.endsWith("$") && !matchRegex.endsWith("\\$")) {
+          matchRegex = matchRegex.substring(0, matchRegex.length() - 1);
+        }
+      }
+      if (matchRegex.equals(".*")) {
+        return projectCache.all();
+      }
+      try {
+        final RunAutomaton a =
+            new RunAutomaton(new RegExp(matchRegex).toAutomaton());
+        return Iterables.filter(projectCache.all(),
+            new Predicate<Project.NameKey>() {
+              public boolean apply(Project.NameKey in) {
+                return a.run(in.get());
+              }
+            });
+      } catch (IllegalArgumentException e) {
+        throw new BadRequestException(e.getMessage());
+      }
     } else {
       return projectCache.all();
     }
@@ -441,7 +487,7 @@
 
   private void printProjectTree(final PrintWriter stdout,
       final TreeMap<Project.NameKey, ProjectNode> treeMap) {
-    final SortedSet<ProjectNode> sortedNodes = new TreeSet<ProjectNode>();
+    final SortedSet<ProjectNode> sortedNodes = new TreeSet<>();
 
     // Builds the inheritance tree using a list.
     //
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 1b6c410..7b50b0f 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
@@ -16,9 +16,10 @@
 
 import static com.google.gerrit.server.project.BranchResource.BRANCH_KIND;
 import static com.google.gerrit.server.project.ChildProjectResource.CHILD_PROJECT_KIND;
+import static com.google.gerrit.server.project.CommitResource.COMMIT_KIND;
 import static com.google.gerrit.server.project.DashboardResource.DASHBOARD_KIND;
-import static com.google.gerrit.server.project.ProjectResource.PROJECT_KIND;
 import static com.google.gerrit.server.project.FileResource.FILE_KIND;
+import static com.google.gerrit.server.project.ProjectResource.PROJECT_KIND;
 
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.RestApiModule;
@@ -35,6 +36,7 @@
     DynamicMap.mapOf(binder(), BRANCH_KIND);
     DynamicMap.mapOf(binder(), DASHBOARD_KIND);
     DynamicMap.mapOf(binder(), FILE_KIND);
+    DynamicMap.mapOf(binder(), COMMIT_KIND);
 
     put(PROJECT_KIND).to(PutProject.class);
     get(PROJECT_KIND).to(GetProject.class);
@@ -51,6 +53,8 @@
     get(PROJECT_KIND, "HEAD").to(GetHead.class);
     put(PROJECT_KIND, "HEAD").to(SetHead.class);
 
+    put(PROJECT_KIND, "ban").to(BanCommit.class);
+
     get(PROJECT_KIND, "statistics.git").to(GetStatistics.class);
     post(PROJECT_KIND, "gc").to(GarbageCollect.class);
 
@@ -59,9 +63,14 @@
     get(BRANCH_KIND).to(GetBranch.class);
     delete(BRANCH_KIND).to(DeleteBranch.class);
     install(new FactoryModuleBuilder().build(CreateBranch.Factory.class));
+    get(BRANCH_KIND, "reflog").to(GetReflog.class);
     child(BRANCH_KIND, "files").to(FilesCollection.class);
     get(FILE_KIND, "content").to(GetContent.class);
 
+    child(PROJECT_KIND, "commits").to(CommitsCollection.class);
+    get(COMMIT_KIND).to(GetCommit.class);
+    child(COMMIT_KIND, "files").to(FilesInCommitCollection.class);
+
     child(PROJECT_KIND, "dashboards").to(DashboardsCollection.class);
     get(DASHBOARD_KIND).to(GetDashboard.class);
     put(DASHBOARD_KIND).to(SetDashboard.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PerRequestProjectControlCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PerRequestProjectControlCache.java
index 19a42ae..f46a881 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PerRequestProjectControlCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PerRequestProjectControlCache.java
@@ -34,7 +34,7 @@
       CurrentUser userProvider) {
     this.projectCache = projectCache;
     this.user = userProvider;
-    this.controls = new HashMap<Project.NameKey, ProjectControl>();
+    this.controls = new HashMap<>();
   }
 
   ProjectControl get(Project.NameKey nameKey) throws NoSuchProjectException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PerformCreateProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PerformCreateProject.java
index 68f868f..43bdd22 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PerformCreateProject.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PerformCreateProject.java
@@ -22,12 +22,12 @@
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.errors.ProjectCreationFailedException;
+import com.google.gerrit.extensions.common.SubmitType;
 import com.google.gerrit.extensions.events.NewProjectCreatedListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.client.Project.SubmitType;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.GroupBackend;
@@ -240,11 +240,10 @@
 
     if (createProjectArgs.ownerIds == null
         || createProjectArgs.ownerIds.isEmpty()) {
-      createProjectArgs.ownerIds =
-          new ArrayList<AccountGroup.UUID>(projectOwnerGroups);
+      createProjectArgs.ownerIds = new ArrayList<>(projectOwnerGroups);
     }
 
-    List<String> transformedBranches = new ArrayList<String>();
+    List<String> transformedBranches = new ArrayList<>();
     if (createProjectArgs.branch == null ||
         createProjectArgs.branch.isEmpty()) {
       createProjectArgs.branch = Collections.singletonList(Constants.MASTER);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java
index dbfe34b..321c2ea 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.project;
 
+import static com.google.common.base.Objects.firstNonNull;
 import static com.google.gerrit.server.project.RefControl.isRE;
 
 import com.google.common.collect.Lists;
@@ -110,10 +111,10 @@
       sorter.sort(ref, sections);
 
       Set<SeenRule> seen = new HashSet<SeenRule>();
-      Set<String> exclusiveGroupPermissions = new HashSet<String>();
+      Set<String> exclusiveGroupPermissions = new HashSet<>();
 
-      HashMap<String, List<PermissionRule>> permissions =
-          new HashMap<String, List<PermissionRule>>();
+      HashMap<String, List<PermissionRule>> permissions = new HashMap<>();
+      HashMap<String, List<PermissionRule>> overridden = new HashMap<>();
       Map<PermissionRule, ProjectRef> ruleProps = Maps.newIdentityHashMap();
       for (AccessSection section : sections) {
         Project.NameKey project = sectionToProject.get(section);
@@ -129,11 +130,19 @@
             } else {
               addRule = seen.add(s) && !rule.isDeny() && !exclusivePermissionExists;
             }
+
+            HashMap<String, List<PermissionRule>> p = null;
             if (addRule) {
-              List<PermissionRule> r = permissions.get(permission.getName());
+              p = permissions;
+            } else if (!rule.isDeny() && !exclusivePermissionExists) {
+              p = overridden;
+            }
+
+            if (p != null) {
+              List<PermissionRule> r = p.get(permission.getName());
               if (r == null) {
-                r = new ArrayList<PermissionRule>(2);
-                permissions.put(permission.getName(), r);
+                r = new ArrayList<>(2);
+                p.put(permission.getName(), r);
               }
               r.add(rule);
               ruleProps.put(rule, new ProjectRef(project, section.getName()));
@@ -146,18 +155,22 @@
         }
       }
 
-      return new PermissionCollection(permissions, ruleProps, perUser);
+      return new PermissionCollection(permissions, overridden, ruleProps,
+          perUser);
     }
   }
 
   private final Map<String, List<PermissionRule>> rules;
+  private final Map<String, List<PermissionRule>> overridden;
   private final Map<PermissionRule, ProjectRef> ruleProps;
   private final boolean perUser;
 
   private PermissionCollection(Map<String, List<PermissionRule>> rules,
+      Map<String, List<PermissionRule>> overridden,
       Map<PermissionRule, ProjectRef> ruleProps,
       boolean perUser) {
     this.rules = rules;
+    this.overridden = overridden;
     this.ruleProps = ruleProps;
     this.perUser = perUser;
   }
@@ -183,6 +196,11 @@
     return r != null ? r : Collections.<PermissionRule> emptyList();
   }
 
+  List<PermissionRule> getOverridden(String permissionName) {
+    return firstNonNull(
+        overridden.get(permissionName), Collections.<PermissionRule> emptyList());
+  }
+
   ProjectRef getRuleProps(PermissionRule rule) {
     return ruleProps.get(rule);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java
index 0e5cecb..d451b46 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java
@@ -25,11 +25,14 @@
   /** @return the parent state for all projects on this server. */
   public ProjectState getAllProjects();
 
+  /** @return the project state of the project storing meta data for all users. */
+  public ProjectState getAllUsers();
+
   /**
    * Get the cached data for a project by its unique name.
    *
    * @param projectName name of the project.
-   * @return the cached data; null if no such project exists or a error occured.
+   * @return the cached data; null if no such project exists or a error occurred.
    * @see #checkedGet(com.google.gerrit.reviewdb.client.Project.NameKey)
    */
   public ProjectState get(Project.NameKey projectName);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index 8ccbca3..1e7a221 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.inject.Inject;
@@ -74,6 +75,7 @@
   }
 
   private final AllProjectsName allProjectsName;
+  private final AllUsersName allUsersName;
   private final LoadingCache<String, ProjectState> byName;
   private final LoadingCache<ListKey, SortedSet<Project.NameKey>> list;
   private final Lock listLock;
@@ -82,10 +84,12 @@
   @Inject
   ProjectCacheImpl(
       final AllProjectsName allProjectsName,
+      final AllUsersName allUsersName,
       @Named(CACHE_NAME) LoadingCache<String, ProjectState> byName,
       @Named(CACHE_LIST) LoadingCache<ListKey, SortedSet<Project.NameKey>> list,
       ProjectCacheClock clock) {
     this.allProjectsName = allProjectsName;
+    this.allUsersName = allUsersName;
     this.byName = byName;
     this.list = list;
     this.listLock = new ReentrantLock(true /* fair */);
@@ -104,6 +108,16 @@
   }
 
   @Override
+  public ProjectState getAllUsers() {
+    ProjectState state = get(allUsersName);
+    if (state == null) {
+      // This should never occur.
+      throw new IllegalStateException("Missing project " + allUsersName);
+    }
+    return state;
+  }
+
+  @Override
   public ProjectState get(final Project.NameKey projectName) {
      try {
       return checkedGet(projectName);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
index f71c4c9..7eda31f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
@@ -134,7 +134,7 @@
     }
   }
 
-  interface AssistedFactory {
+  public interface AssistedFactory {
     ProjectControl create(CurrentUser who, ProjectState ps);
   }
 
@@ -193,7 +193,7 @@
 
   public RefControl controlForRef(String refName) {
     if (refControls == null) {
-      refControls = new HashMap<String, RefControl>();
+      refControls = new HashMap<>();
     }
     RefControl ctl = refControls.get(refName);
     if (ctl == null) {
@@ -232,7 +232,8 @@
   }
 
   private boolean isHidden() {
-    return getProject().getState().equals(Project.State.HIDDEN);
+    return getProject().getState().equals(
+        com.google.gerrit.extensions.api.projects.ProjectState.HIDDEN);
   }
 
   /** Can this user see this project exists? */
@@ -312,7 +313,7 @@
 
   private static Set<GroupReference> getGroups(
       final List<SectionMatcher> sectionMatcherList) {
-    final Set<GroupReference> all = new HashSet<GroupReference>();
+    final Set<GroupReference> all = new HashSet<>();
     for (final SectionMatcher matcher : sectionMatcherList) {
       final AccessSection section = matcher.section;
       for (final Permission permission : section.getPermissions()) {
@@ -447,7 +448,7 @@
   }
 
   private Set<String> allRefPatterns(String permissionName) {
-    Set<String> all = new HashSet<String>();
+    Set<String> all = new HashSet<>();
     for (SectionMatcher matcher : access()) {
       AccessSection section = matcher.section;
       Permission permission = section.getPermission(permissionName);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectJson.java
index 72910a3..4cafc0a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectJson.java
@@ -15,20 +15,29 @@
 package com.google.gerrit.server.project;
 
 import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.common.ProjectInfo;
+import com.google.gerrit.extensions.common.WebLinkInfo;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.WebLinks;
 import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AllProjectsNameProvider;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
-import java.util.Map;
-
+@Singleton
 public class ProjectJson {
 
   private final AllProjectsName allProjects;
+  private final Provider<WebLinks> webLinks;
 
   @Inject
-  ProjectJson(AllProjectsName allProjects) {
-    this.allProjects = allProjects;
+  ProjectJson(AllProjectsNameProvider allProjectsNameProvider,
+      Provider<WebLinks> webLinks) {
+    this.allProjects = allProjectsNameProvider.get();
+    this.webLinks = webLinks;
   }
 
   public ProjectInfo format(ProjectResource rsrc) {
@@ -42,21 +51,15 @@
     info.parent = parentName != null ? parentName.get() : null;
     info.description = Strings.emptyToNull(p.getDescription());
     info.state = p.getState();
-    info.finish();
-    return info;
-  }
+    info.id = Url.encode(info.name);
 
-  public static class ProjectInfo {
-    public final String kind = "gerritcodereview#project";
-    public String id;
-    public String name;
-    public String parent;
-    public String description;
-    public Project.State state;
-    public Map<String, String> branches;
-
-    void finish() {
-      id = Url.encode(name);
+    info.webLinks = Lists.newArrayList();
+    for (WebLinkInfo link : webLinks.get().getProjectLinks(p.getName())) {
+      if (!Strings.isNullOrEmpty(link.name) && !Strings.isNullOrEmpty(link.url)) {
+        info.webLinks.add(link);
+      }
     }
+
+    return info;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectNode.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectNode.java
index cc85312..e74511a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectNode.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectNode.java
@@ -33,7 +33,7 @@
   private final Project project;
   private final boolean isVisible;
 
-  private final SortedSet<ProjectNode> children = new TreeSet<ProjectNode>();
+  private final SortedSet<ProjectNode> children = new TreeSet<>();
 
   @Inject
   protected ProjectNode(final AllProjectsName allProjectsName,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectResource.java
index 459e392..d6a2e09 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectResource.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.gerrit.extensions.api.projects.ProjectState;
 import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.reviewdb.client.Project;
@@ -41,7 +42,7 @@
     return control.getProject().getNameKey();
   }
 
-  public Project.State getState() {
+  public ProjectState getState() {
     return control.getProject().getState();
   }
 
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 a7a96f0..b9cead3 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
@@ -30,9 +30,9 @@
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.extensions.common.InheritableBoolean;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.rules.PrologEnvironment;
 import com.google.gerrit.rules.RulesCache;
@@ -41,6 +41,7 @@
 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.BranchOrderSection;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.git.ProjectLevelConfig;
@@ -135,7 +136,7 @@
     if (isAllProjects && !Permission.canBeOnAllProjects(AccessSection.ALL, Permission.OWNER)) {
       localOwners = Collections.emptySet();
     } else {
-      HashSet<AccountGroup.UUID> groups = new HashSet<AccountGroup.UUID>();
+      HashSet<AccountGroup.UUID> groups = new HashSet<>();
       AccessSection all = config.getAccessSection(AccessSection.ALL);
       if (all != null) {
         Permission owner = all.getPermission(Permission.OWNER);
@@ -255,7 +256,7 @@
     List<SectionMatcher> sm = localAccessSections;
     if (sm == null) {
       Collection<AccessSection> fromConfig = config.getAccessSections();
-      sm = new ArrayList<SectionMatcher>(fromConfig.size());
+      sm = new ArrayList<>(fromConfig.size());
       for (AccessSection section : fromConfig) {
         if (isAllProjects) {
           List<Permission> copy =
@@ -446,6 +447,16 @@
     return ImmutableList.copyOf(cls.values());
   }
 
+  public BranchOrderSection getBranchOrderSection() {
+    for (ProjectState s : tree()) {
+      BranchOrderSection section = s.getConfig().getBranchOrderSection();
+      if (section != null) {
+        return section;
+      }
+    }
+    return null;
+  }
+
   public ThemeInfo getTheme() {
     ThemeInfo theme = this.theme;
     if (theme == null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectsCollection.java
index 6e9c5d9..519f4f2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectsCollection.java
@@ -27,9 +27,11 @@
 import com.google.gerrit.server.OutputFormat;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import java.io.IOException;
 
+@Singleton
 public class ProjectsCollection implements
     RestCollection<TopLevelResource, ProjectResource>,
     AcceptsCreate<TopLevelResource> {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutBranch.java
index 2cd5659..f8b201b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutBranch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutBranch.java
@@ -17,7 +17,9 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.project.CreateBranch.Input;
+import com.google.inject.Singleton;
 
+@Singleton
 public class PutBranch implements RestModifyView<BranchResource, Input> {
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
index c481eb4..3aed95d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
@@ -19,6 +19,9 @@
 import com.google.common.base.Objects;
 import com.google.common.base.Strings;
 import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.extensions.api.projects.ProjectInput.ConfigValue;
+import com.google.gerrit.extensions.common.InheritableBoolean;
+import com.google.gerrit.extensions.common.SubmitType;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -27,8 +30,6 @@
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
-import com.google.gerrit.reviewdb.client.Project.SubmitType;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.AllProjectsNameProvider;
@@ -43,6 +44,7 @@
 import com.google.gerrit.server.project.PutConfig.Input;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -56,12 +58,9 @@
 import java.util.Map;
 import java.util.Map.Entry;
 
+@Singleton
 public class PutConfig implements RestModifyView<ProjectResource, Input> {
   private static final Logger log = LoggerFactory.getLogger(PutConfig.class);
-  public static class ConfigValue {
-    public String value;
-    public List<String> values;
-  }
   public static class Input {
     public String description;
     public InheritableBoolean useContributorAgreements;
@@ -70,7 +69,7 @@
     public InheritableBoolean requireChangeId;
     public String maxObjectSizeLimit;
     public SubmitType submitType;
-    public Project.State state;
+    public com.google.gerrit.extensions.api.projects.ProjectState state;
     public Map<String, Map<String, ConfigValue>> pluginConfigValues;
   }
 
@@ -115,11 +114,16 @@
   public ConfigInfo apply(ProjectResource rsrc, Input input)
       throws ResourceNotFoundException, BadRequestException,
       ResourceConflictException {
-    Project.NameKey projectName = rsrc.getNameKey();
     if (!rsrc.getControl().isOwner()) {
-      throw new ResourceNotFoundException(projectName.get());
+      throw new ResourceNotFoundException(rsrc.getName());
     }
+    return apply(rsrc.getControl(), input);
+  }
 
+  public ConfigInfo apply(ProjectControl ctrl, Input input)
+      throws ResourceNotFoundException, BadRequestException,
+      ResourceConflictException {
+    Project.NameKey projectName = ctrl.getProject().getNameKey();
     if (input == null) {
       throw new BadRequestException("config is required");
     }
@@ -164,7 +168,7 @@
       }
 
       if (input.pluginConfigValues != null) {
-        setPluginConfigValues(rsrc.getControl().getProjectState(),
+        setPluginConfigValues(ctrl.getProjectState(),
             projectConfig, input.pluginConfigValues);
       }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutDescription.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutDescription.java
index 17ab7b3..b8a4c7c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutDescription.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutDescription.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.project.PutDescription.Input;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -39,6 +40,7 @@
 
 import java.io.IOException;
 
+@Singleton
 class PutDescription implements RestModifyView<ProjectResource, Input> {
   static class Input {
     @DefaultInput
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutProject.java
index 836899a2..fc397e1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutProject.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutProject.java
@@ -14,14 +14,16 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.gerrit.extensions.api.projects.ProjectInput;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.server.project.CreateProject.Input;
+import com.google.inject.Singleton;
 
-public class PutProject implements RestModifyView<ProjectResource, Input> {
+@Singleton
+public class PutProject implements RestModifyView<ProjectResource, ProjectInput> {
   @Override
-  public Response<?> apply(ProjectResource resource, Input input)
+  public Response<?> apply(ProjectResource resource, ProjectInput input)
       throws ResourceConflictException {
     throw new ResourceConflictException("Project \"" + resource.getName()
         + "\" already exists");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
index 7c956da..7dc7a2a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
@@ -22,7 +22,7 @@
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.data.RefConfigSection;
 import com.google.gerrit.common.errors.InvalidNameException;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.extensions.api.projects.ProjectState;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
@@ -68,7 +68,7 @@
     this.projectControl = projectControl;
     this.refName = ref;
     this.relevant = relevant;
-    this.effective = new HashMap<String, List<PermissionRule>>();
+    this.effective = new HashMap<>();
   }
 
   public String getRefName() {
@@ -120,6 +120,7 @@
    */
   public boolean isVisibleByRegisteredUsers() {
     List<PermissionRule> access = relevant.getPermission(Permission.READ);
+    List<PermissionRule> overridden = relevant.getOverridden(Permission.READ);
     Set<ProjectRef> allows = Sets.newHashSet();
     Set<ProjectRef> blocks = Sets.newHashSet();
     for (PermissionRule rule : access) {
@@ -129,6 +130,11 @@
         allows.add(relevant.getRuleProps(rule));
       }
     }
+    for (PermissionRule rule : overridden) {
+      if (SystemGroupBackend.isAnonymousOrRegistered(rule.getGroup())) {
+        blocks.remove(relevant.getRuleProps(rule));
+      }
+    }
     blocks.removeAll(allows);
     return blocks.isEmpty() && !allows.isEmpty();
   }
@@ -207,12 +213,12 @@
 
   public boolean canWrite() {
     return getProjectControl().getProject().getState().equals(
-        Project.State.ACTIVE);
+        ProjectState.ACTIVE);
   }
 
   public boolean canRead() {
     return getProjectControl().getProject().getState().equals(
-        Project.State.READ_ONLY) || canWrite();
+        ProjectState.READ_ONLY) || canWrite();
   }
 
   private boolean canPushWithForce() {
@@ -387,7 +393,7 @@
 
   /** All value ranges of any allowed label permission. */
   public List<PermissionRange> getLabelRanges(boolean isChangeOwner) {
-    List<PermissionRange> r = new ArrayList<PermissionRange>();
+    List<PermissionRange> r = new ArrayList<>();
     for (Map.Entry<String, List<PermissionRule>> e : relevant.getDeclaredPermissions()) {
       if (Permission.isLabel(e.getKey())) {
         int min = 0;
@@ -492,6 +498,7 @@
 
   private boolean doCanPerform(String permissionName, boolean blockOnly) {
     List<PermissionRule> access = access(permissionName);
+    List<PermissionRule> overridden = relevant.getOverridden(permissionName);
     Set<ProjectRef> allows = Sets.newHashSet();
     Set<ProjectRef> blocks = Sets.newHashSet();
     for (PermissionRule rule : access) {
@@ -501,6 +508,9 @@
         allows.add(relevant.getRuleProps(rule));
       }
     }
+    for (PermissionRule rule : overridden) {
+      blocks.remove(relevant.getRuleProps(rule));
+    }
     blocks.removeAll(allows);
     return blocks.isEmpty() && (!allows.isEmpty() || blockOnly);
   }
@@ -508,6 +518,7 @@
   /** True if the user has force this permission. Works only for non labels. */
   private boolean canForcePerform(String permissionName) {
     List<PermissionRule> access = access(permissionName);
+    List<PermissionRule> overridden = relevant.getOverridden(permissionName);
     Set<ProjectRef> allows = Sets.newHashSet();
     Set<ProjectRef> blocks = Sets.newHashSet();
     for (PermissionRule rule : access) {
@@ -517,6 +528,11 @@
         allows.add(relevant.getRuleProps(rule));
       }
     }
+    for (PermissionRule rule : overridden) {
+      if (rule.getForce()) {
+        blocks.remove(relevant.getRuleProps(rule));
+      }
+    }
     blocks.removeAll(allows);
     return blocks.isEmpty() && !allows.isEmpty();
   }
@@ -524,6 +540,7 @@
   /** True if for this permission force is blocked for the user. Works only for non labels. */
   private boolean isForceBlocked(String permissionName) {
     List<PermissionRule> access = access(permissionName);
+    List<PermissionRule> overridden = relevant.getOverridden(permissionName);
     Set<ProjectRef> allows = Sets.newHashSet();
     Set<ProjectRef> blocks = Sets.newHashSet();
     for (PermissionRule rule : access) {
@@ -533,6 +550,11 @@
         allows.add(relevant.getRuleProps(rule));
       }
     }
+    for (PermissionRule rule : overridden) {
+      if (rule.getForce()) {
+        blocks.remove(relevant.getRuleProps(rule));
+      }
+    }
     blocks.removeAll(allows);
     return !blocks.isEmpty();
   }
@@ -565,7 +587,7 @@
       return rules;
     }
 
-    List<PermissionRule> mine = new ArrayList<PermissionRule>(rules.size());
+    List<PermissionRule> mine = new ArrayList<>(rules.size());
     for (PermissionRule rule : rules) {
       if (projectControl.match(rule, isChangeOwner)) {
         mine.add(rule);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java
index aeb92d3..c012bd5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java
@@ -77,8 +77,7 @@
 
     } else {
       boolean poison = false;
-      IdentityHashMap<AccessSection, Integer> srcMap =
-          new IdentityHashMap<AccessSection, Integer>();
+      IdentityHashMap<AccessSection, Integer> srcMap = new IdentityHashMap<>();
       for (int i = 0; i < cnt; i++) {
         poison |= srcMap.put(sections.get(i), i) != null;
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDashboard.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDashboard.java
index 8319bbd..cda548a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDashboard.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDashboard.java
@@ -24,9 +24,11 @@
 import com.google.gerrit.server.project.SetDashboard.Input;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import java.io.IOException;
 
+@Singleton
 class SetDashboard implements RestModifyView<DashboardResource, Input> {
   static class Input {
     @DefaultInput
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java
index 05b392b..7efc1b7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.server.project.SetHead.Input;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Constants;
@@ -38,6 +39,7 @@
 
 import java.io.IOException;
 
+@Singleton
 public class SetHead implements RestModifyView<ProjectResource, Input> {
   private static final Logger log = LoggerFactory.getLogger(SetHead.class);
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java
index 4f0941d..81ce582 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java
@@ -31,12 +31,14 @@
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.project.SetParent.Input;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 
 import java.io.IOException;
 
+@Singleton
 public class SetParent implements RestModifyView<ProjectResource, Input> {
   public static class Input {
     @DefaultInput
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 f7c3c9d..d79716c 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
@@ -134,7 +134,7 @@
         env.once("gerrit", "assume_range_from_label");
       }
 
-      List<Term> results = new ArrayList<Term>();
+      List<Term> results = new ArrayList<>();
       try {
         for (Term[] template : env.all("gerrit", userRuleWrapperName,
             submitRule, new VariableTerm())) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SuggestParentCandidates.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SuggestParentCandidates.java
index c73de60..4b8d2a4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SuggestParentCandidates.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SuggestParentCandidates.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
 import java.util.ArrayList;
 import java.util.Comparator;
@@ -25,11 +26,8 @@
 import java.util.Set;
 import java.util.TreeSet;
 
+@Singleton
 public class SuggestParentCandidates {
-  public interface Factory {
-    SuggestParentCandidates create();
-  }
-
   private final ProjectControl.Factory projectControlFactory;
   private final ProjectCache projectCache;
   private final AllProjectsName allProject;
@@ -45,8 +43,7 @@
   public List<Project.NameKey> getNameKeys() throws OrmException,
       NoSuchProjectException {
     List<Project> pList = getProjects();
-    final List<Project.NameKey> nameKeys =
-        new ArrayList<Project.NameKey>(pList.size());
+    final List<Project.NameKey> nameKeys = new ArrayList<>(pList.size());
     for (Project p : pList) {
       nameKeys.add(p.getNameKey());
     }
@@ -55,7 +52,7 @@
 
   public List<Project> getProjects() throws OrmException,
       NoSuchProjectException {
-    Set<Project> projects = new TreeSet<Project>(new Comparator<Project>() {
+    Set<Project> projects = new TreeSet<>(new Comparator<Project>() {
       @Override
       public int compare(Project o1, Project o2) {
         return o1.getName().compareTo(o2.getName());
@@ -76,6 +73,6 @@
       }
     }
     projects.add(projectControlFactory.controlFor(allProject).getProject());
-    return new ArrayList<Project>(projects);
+    return new ArrayList<>(projects);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java
index 915a364..953dabf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java
@@ -33,7 +33,7 @@
   }
 
   protected AndPredicate(final Collection<? extends Predicate<T>> that) {
-    final ArrayList<Predicate<T>> t = new ArrayList<Predicate<T>>(that.size());
+    List<Predicate<T>> t = new ArrayList<>(that.size());
     int c = 0;
     for (Predicate<T> p : that) {
       if (getClass() == p.getClass()) {
@@ -67,7 +67,7 @@
 
   @Override
   public Predicate<T> copy(final Collection<? extends Predicate<T>> children) {
-    return new AndPredicate<T>(children);
+    return new AndPredicate<>(children);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/DataSource.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/DataSource.java
new file mode 100644
index 0000000..83cdc80
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/DataSource.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query;
+
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
+
+public interface DataSource<T> {
+  /** @return an estimate of the number of results from {@link #read()}. */
+  public int getCardinality();
+
+  /** @return read from the database and return the results. */
+  public ResultSet<T> read() throws OrmException;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/NotPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/NotPredicate.java
index f94e1f6..6a9a877 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/NotPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/NotPredicate.java
@@ -54,7 +54,7 @@
     if (children.size() != 1) {
       throw new IllegalArgumentException("Expected exactly one child");
     }
-    return new NotPredicate<T>(children.iterator().next());
+    return new NotPredicate<>(children.iterator().next());
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/OrPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/OrPredicate.java
index 2c91809..845c805 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/OrPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/OrPredicate.java
@@ -33,7 +33,7 @@
   }
 
   protected OrPredicate(final Collection<? extends Predicate<T>> that) {
-    final ArrayList<Predicate<T>> t = new ArrayList<Predicate<T>>(that.size());
+    List<Predicate<T>> t = new ArrayList<>(that.size());
     int c = 0;
     for (Predicate<T> p : that) {
       if (getClass() == p.getClass()) {
@@ -67,7 +67,7 @@
 
   @Override
   public Predicate<T> copy(final Collection<? extends Predicate<T>> children) {
-    return new OrPredicate<T>(children);
+    return new OrPredicate<>(children);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java
index c134458..7b43572 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java
@@ -55,7 +55,7 @@
     if (that.length == 1) {
       return that[0];
     }
-    return new AndPredicate<T>(that);
+    return new AndPredicate<>(that);
   }
 
   /** Combine the passed predicates into a single AND node. */
@@ -64,7 +64,7 @@
     if (that.size() == 1) {
       return Iterables.getOnlyElement(that);
     }
-    return new AndPredicate<T>(that);
+    return new AndPredicate<>(that);
   }
 
   /** Combine the passed predicates into a single OR node. */
@@ -73,7 +73,7 @@
     if (that.length == 1) {
       return that[0];
     }
-    return new OrPredicate<T>(that);
+    return new OrPredicate<>(that);
   }
 
   /** Combine the passed predicates into a single OR node. */
@@ -82,7 +82,7 @@
     if (that.size() == 1) {
       return Iterables.getOnlyElement(that);
     }
-    return new OrPredicate<T>(that);
+    return new OrPredicate<>(that);
   }
 
   /** Invert the passed node. */
@@ -130,7 +130,7 @@
   public abstract boolean equals(Object other);
 
   private static class Any<T> extends Predicate<T> {
-    private static final Any<Object> INSTANCE = new Any<Object>();
+    private static final Any<Object> INSTANCE = new Any<>();
 
     private Any() {
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java
index a276992..5be42be 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java
@@ -84,7 +84,7 @@
    */
   public static class Definition<T, Q extends QueryBuilder<T>> {
     private final Map<String, OperatorFactory<T, Q>> opFactories =
-        new HashMap<String, OperatorFactory<T, Q>>();
+        new HashMap<>();
 
     public Definition(Class<Q> clazz) {
       // Guess at the supported operators by scanning methods.
@@ -204,11 +204,11 @@
           final Tree val = onlyChildOf(opTree);
           if (val.getType() == SINGLE_WORD && "*".equals(val.getText())) {
             final String op = opTree.getText();
-            final WildPatternPredicate<T> pat = new WildPatternPredicate<T>(op);
-            return new VariablePredicate<T>(var, pat);
+            final WildPatternPredicate<T> pat = new WildPatternPredicate<>(op);
+            return new VariablePredicate<>(var, pat);
           }
         }
-        return new VariablePredicate<T>(var, toPredicate(opTree));
+        return new VariablePredicate<>(var, toPredicate(opTree));
       }
 
       default:
@@ -224,7 +224,7 @@
       //
       case AND:
       case OR: {
-        List<Predicate<T>> p = new ArrayList<Predicate<T>>(val.getChildCount());
+        List<Predicate<T>> p = new ArrayList<>(val.getChildCount());
         for (int i = 0; i < val.getChildCount(); i++) {
           final Tree c = val.getChild(i);
           if (c.getType() != DEFAULT_FIELD) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryRewriter.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryRewriter.java
index c0f8414..6173fb6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryRewriter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryRewriter.java
@@ -76,7 +76,7 @@
           if ((m.getModifiers() & Modifier.ABSTRACT) != Modifier.ABSTRACT
               && (m.getModifiers() & Modifier.PUBLIC) == Modifier.PUBLIC
               && rp != null) {
-            rewriteRules.add(new MethodRewrite<T>(qb, rp.value(), m));
+            rewriteRules.add(new MethodRewrite<>(qb, rp.value(), m));
           }
         }
         c = c.getSuperclass();
@@ -138,7 +138,7 @@
       in = rewriteOne(in);
 
       if (old.equals(in) && in.getChildCount() > 0) {
-        List<Predicate<T>> n = new ArrayList<Predicate<T>>(in.getChildCount());
+        List<Predicate<T>> n = new ArrayList<>(in.getChildCount());
         for (Predicate<T> p : in.getChildren()) {
           n.add(rewriteImpl(p));
         }
@@ -159,14 +159,14 @@
       return not(replaceGenericNodes(in.getChild(0)));
 
     } else if (in instanceof AndPredicate) {
-      List<Predicate<T>> n = new ArrayList<Predicate<T>>(in.getChildCount());
+      List<Predicate<T>> n = new ArrayList<>(in.getChildCount());
       for (Predicate<T> c : in.getChildren()) {
         n.add(replaceGenericNodes(c));
       }
       return and(n);
 
     } else if (in instanceof OrPredicate) {
-      List<Predicate<T>> n = new ArrayList<Predicate<T>>(in.getChildCount());
+      List<Predicate<T>> n = new ArrayList<>(in.getChildCount());
       for (Predicate<T> c : in.getChildren()) {
         n.add(replaceGenericNodes(c));
       }
@@ -198,8 +198,8 @@
   }
 
   private static class MatchResult<T> {
-    private static final MatchResult<?> FAIL = new MatchResult<Object>(null);
-    private static final MatchResult<?> OK = new MatchResult<Object>(null);
+    private static final MatchResult<?> FAIL = new MatchResult<>(null);
+    private static final MatchResult<?> OK = new MatchResult<>(null);
 
     @SuppressWarnings("unchecked")
     static <T> MatchResult<T> fail() {
@@ -251,7 +251,7 @@
       // but in any order.
       //
       final LinkedList<Predicate<T>> have = dup(actual);
-      final LinkedList<Predicate<T>> extra = new LinkedList<Predicate<T>>();
+      final LinkedList<Predicate<T>> extra = new LinkedList<>();
       for (final Predicate<T> pat : pattern.getChildren()) {
         boolean found = false;
         for (final Iterator<Predicate<T>> i = have.iterator(); i.hasNext();) {
@@ -275,11 +275,11 @@
           return MatchResult.ok();
         case 1:
           if (isNOT(actual)) {
-            return new MatchResult<T>(actual.copy(have));
+            return new MatchResult<>(actual.copy(have));
           }
-          return new MatchResult<T>(have.get(0));
+          return new MatchResult<>(have.get(0));
         default:
-          return new MatchResult<T>(actual.copy(have));
+          return new MatchResult<>(actual.copy(have));
       }
 
     } else if (pattern.equals(actual)) {
@@ -297,7 +297,7 @@
   }
 
   private static <T> LinkedList<Predicate<T>> dup(final Predicate<T> actual) {
-    return new LinkedList<Predicate<T>>(actual.getChildren());
+    return new LinkedList<>(actual.getChildren());
   }
 
   /**
@@ -412,8 +412,7 @@
     @Override
     public Predicate<T> rewrite(QueryRewriter<T> rewriter,
         final Predicate<T> input) {
-      final HashMap<String, Predicate<T>> args =
-          new HashMap<String, Predicate<T>>();
+      final HashMap<String, Predicate<T>> args = new HashMap<>();
       final MatchResult<T> res = rewriter.match(args, pattern, input);
       if (!res.success()) {
         return null;
@@ -483,7 +482,7 @@
   }
 
   private static <T> List<Predicate<T>> removeDuplicates(List<Predicate<T>> n) {
-    List<Predicate<T>> r = new ArrayList<Predicate<T>>();
+    List<Predicate<T>> r = new ArrayList<>();
     for (Predicate<T> p : n) {
       if (!r.contains(p)) {
         r.add(p);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/VariablePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/VariablePredicate.java
index 45b27e5..d6d0f9c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/VariablePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/VariablePredicate.java
@@ -61,7 +61,7 @@
     if (children.size() != 1) {
       throw new IllegalArgumentException("Expected exactly one child");
     }
-    return new VariablePredicate<T>(getName(), children.iterator().next());
+    return new VariablePredicate<>(getName(), children.iterator().next());
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AbstractResultSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AbstractResultSet.java
index e072760..9bbc02f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AbstractResultSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AbstractResultSet.java
@@ -22,7 +22,7 @@
 abstract class AbstractResultSet<T> implements ResultSet<T> {
   @Override
   public List<T> toList() {
-    ArrayList<T> r = new ArrayList<T>();
+    ArrayList<T> r = new ArrayList<>();
     for (T t : this) {
       r.add(t);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AddedPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AddedPredicate.java
new file mode 100644
index 0000000..95da72a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AddedPredicate.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IntegerRangePredicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gwtorm.server.OrmException;
+
+public class AddedPredicate extends IntegerRangePredicate<ChangeData> {
+  AddedPredicate(String value) throws QueryParseException {
+    super(ChangeField.ADDED, value);
+  }
+
+  @Override
+  protected int getValueInt(ChangeData changeData) throws OrmException {
+    return changeData.changedLines().insertions;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java
index ad0ec3c..55fd281 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java
@@ -68,8 +68,7 @@
 
   private static List<Predicate<ChangeData>> sort(
       Collection<? extends Predicate<ChangeData>> that) {
-    ArrayList<Predicate<ChangeData>> r =
-        new ArrayList<Predicate<ChangeData>>(that);
+    List<Predicate<ChangeData>> r = new ArrayList<>(that);
     Collections.sort(r, CMP);
     return r;
   }
@@ -158,7 +157,7 @@
     } else if (start > 0) {
       r = ImmutableList.copyOf(r.subList(start, r.size()));
     }
-    return new ListResultSet<ChangeData>(r);
+    return new ListResultSet<>(r);
   }
 
   private Iterable<ChangeData> buffer(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
index 87d4564..f726fe3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -62,6 +63,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -150,8 +152,11 @@
    * @param id change ID
    * @return instance for testing.
    */
-  static ChangeData createForTest(Change.Id id) {
-    return new ChangeData(null, null, null, null, null, null, null, null, id);
+  static ChangeData createForTest(Change.Id id, int currentPatchSetId) {
+    ChangeData cd = new ChangeData(null, null, null, null, null,
+        null, null, null, null, id);
+    cd.currentPatchSet = new PatchSet(new PatchSet.Id(id, currentPatchSetId));
+    return cd;
   }
 
   private final ReviewDb db;
@@ -160,6 +165,7 @@
   private final IdentifiedUser.GenericFactory userFactory;
   private final ChangeNotes.Factory notesFactory;
   private final ApprovalsUtil approvalsUtil;
+  private final ChangeMessagesUtil cmUtil;
   private final PatchListCache patchListCache;
   private final NotesMigration notesMigration;
   private final Change.Id legacyId;
@@ -172,7 +178,7 @@
   private Collection<PatchSet> patches;
   private ListMultimap<PatchSet.Id, PatchSetApproval> allApprovals;
   private List<PatchSetApproval> currentApprovals;
-  private List<String> currentFiles;
+  private Map<Integer, List<String>> files = new HashMap<>();
   private Collection<PatchLineComment> comments;
   private CurrentUser visibleTo;
   private ChangeControl changeControl;
@@ -187,6 +193,7 @@
       IdentifiedUser.GenericFactory userFactory,
       ChangeNotes.Factory notesFactory,
       ApprovalsUtil approvalsUtil,
+      ChangeMessagesUtil cmUtil,
       PatchListCache patchListCache,
       NotesMigration notesMigration,
       @Assisted ReviewDb db,
@@ -197,6 +204,7 @@
     this.userFactory = userFactory;
     this.notesFactory = notesFactory;
     this.approvalsUtil = approvalsUtil;
+    this.cmUtil = cmUtil;
     this.patchListCache = patchListCache;
     this.notesMigration = notesMigration;
     legacyId = id;
@@ -209,6 +217,7 @@
       IdentifiedUser.GenericFactory userFactory,
       ChangeNotes.Factory notesFactory,
       ApprovalsUtil approvalsUtil,
+      ChangeMessagesUtil cmUtil,
       PatchListCache patchListCache,
       NotesMigration notesMigration,
       @Assisted ReviewDb db,
@@ -219,6 +228,7 @@
     this.userFactory = userFactory;
     this.notesFactory = notesFactory;
     this.approvalsUtil = approvalsUtil;
+    this.cmUtil = cmUtil;
     this.patchListCache = patchListCache;
     this.notesMigration = notesMigration;
     legacyId = c.getId();
@@ -232,6 +242,7 @@
       IdentifiedUser.GenericFactory userFactory,
       ChangeNotes.Factory notesFactory,
       ApprovalsUtil approvalsUtil,
+      ChangeMessagesUtil cmUtil,
       PatchListCache patchListCache,
       NotesMigration notesMigration,
       @Assisted ReviewDb db,
@@ -242,6 +253,7 @@
     this.userFactory = userFactory;
     this.notesFactory = notesFactory;
     this.approvalsUtil = approvalsUtil;
+    this.cmUtil = cmUtil;
     this.patchListCache = patchListCache;
     this.notesMigration = notesMigration;
     legacyId = c.getChange().getId();
@@ -258,30 +270,38 @@
     returnedBySource = s;
   }
 
-  public void setCurrentFilePaths(List<String> filePaths) {
-    currentFiles = ImmutableList.copyOf(filePaths);
+  public void setCurrentFilePaths(List<String> filePaths) throws OrmException {
+    PatchSet ps = currentPatchSet();
+    if (ps != null) {
+      files.put(ps.getPatchSetId(), ImmutableList.copyOf(filePaths));
+    }
   }
 
   public List<String> currentFilePaths() throws OrmException {
-    if (currentFiles == null) {
+    PatchSet ps = currentPatchSet();
+    if (ps == null) {
+      return null;
+    }
+    return filePaths(currentPatchSet);
+  }
+
+  public List<String> filePaths(PatchSet ps) throws OrmException {
+    if (!files.containsKey(ps.getPatchSetId())) {
       Change c = change();
       if (c == null) {
         return null;
       }
-      PatchSet ps = currentPatchSet();
-      if (ps == null) {
-        return null;
-      }
 
       PatchList p;
       try {
         p = patchListCache.get(c, ps);
       } catch (PatchListNotAvailableException e) {
-        currentFiles = Collections.emptyList();
-        return currentFiles;
+        List<String> emptyFileList = Collections.emptyList();
+        files.put(ps.getPatchSetId(), emptyFileList);
+        return emptyFileList;
       }
 
-      List<String> r = new ArrayList<String>(p.getPatches().size());
+      List<String> r = new ArrayList<>(p.getPatches().size());
       for (PatchListEntry e : p.getPatches()) {
         if (Patch.COMMIT_MSG.equals(e.getNewName())) {
           continue;
@@ -302,9 +322,9 @@
         }
       }
       Collections.sort(r);
-      currentFiles = Collections.unmodifiableList(r);
+      files.put(ps.getPatchSetId(), Collections.unmodifiableList(r));
     }
-    return currentFiles;
+    return files.get(ps.getPatchSetId());
   }
 
   public ChangedLines changedLines() throws OrmException {
@@ -331,6 +351,10 @@
     return changedLines;
   }
 
+  public void setChangedLines(int insertions, int deletions) {
+    changedLines = new ChangedLines(insertions, deletions);
+  }
+
   public Change.Id getId() {
     return legacyId;
   }
@@ -343,12 +367,15 @@
     return changeControl != null;
   }
 
-  public ChangeControl changeControl() throws NoSuchChangeException,
-      OrmException {
+  public ChangeControl changeControl() throws OrmException {
     if (changeControl == null) {
       Change c = change();
-      changeControl =
-          changeControlFactory.controlFor(c, userFactory.create(c.getOwner()));
+      try {
+        changeControl =
+            changeControlFactory.controlFor(c, userFactory.create(c.getOwner()));
+      } catch (NoSuchChangeException e) {
+        throw new OrmException(e);
+      }
     }
     return changeControl;
   }
@@ -394,11 +421,9 @@
       Change c = change();
       if (c == null) {
         currentApprovals = Collections.emptyList();
-      } else if (allApprovals != null) {
-        return allApprovals.get(c.currentPatchSetId());
       } else {
-        currentApprovals = approvalsUtil.byPatchSet(
-            db, notes(), c.currentPatchSetId());
+        currentApprovals = ImmutableList.copyOf(approvalsUtil.byPatchSet(
+            db, changeControl(), c.currentPatchSetId()));
       }
     }
     return currentApprovals;
@@ -505,7 +530,7 @@
   public List<ChangeMessage> messages()
       throws OrmException {
     if (messages == null) {
-      messages = db.changeMessages().byChange(legacyId).toList();
+      messages = cmUtil.byChange(db, notes());
     }
     return messages;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeDataResultSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeDataResultSet.java
index f469228..52a5b7b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeDataResultSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeDataResultSet.java
@@ -23,6 +23,7 @@
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.NoSuchElementException;
+import java.util.Set;
 
 abstract class ChangeDataResultSet<T> extends AbstractResultSet<ChangeData> {
   static ResultSet<ChangeData> change(final ChangeData.Factory factory,
@@ -78,7 +79,7 @@
     } else {
       return new Iterator<ChangeData>() {
         private final Iterator<T> itr = source.iterator();
-        private final HashSet<Change.Id> seen = new HashSet<Change.Id>();
+        private final Set<Change.Id> seen = new HashSet<>();
         private ChangeData next;
 
         @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeDataSource.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeDataSource.java
index ecfd48c..47bf82d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeDataSource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeDataSource.java
@@ -14,16 +14,9 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
+import com.google.gerrit.server.query.DataSource;
 
-public interface ChangeDataSource {
-  /** @return an estimate of the number of results from {@link #read()}. */
-  public int getCardinality();
-
+public interface ChangeDataSource extends DataSource<ChangeData> {
   /** @return true if all returned ChangeData.hasChange() will be true. */
   public boolean hasChange();
-
-  /** @return read from the database and return the changes. */
-  public abstract ResultSet<ChangeData> read() throws OrmException;
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 4a6381e..ac7b9ae 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -22,8 +22,6 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
@@ -36,6 +34,7 @@
 import com.google.gerrit.server.config.TrackingFooters;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.strategy.SubmitStrategyFactory;
+import com.google.gerrit.server.index.ChangeField;
 import com.google.gerrit.server.index.ChangeIndex;
 import com.google.gerrit.server.index.IndexCollection;
 import com.google.gerrit.server.index.Schema;
@@ -55,7 +54,6 @@
 import org.eclipse.jgit.lib.AbbreviatedObjectId;
 import org.eclipse.jgit.lib.Config;
 
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
@@ -74,17 +72,10 @@
   private static final Pattern DEF_CHANGE =
       Pattern.compile("^([1-9][0-9]*|[iI][0-9a-f]{4,}.*)$");
 
-  private static final Pattern PAT_COMMIT =
-      Pattern.compile("^([0-9a-fA-F]{4," + RevId.LEN + "})$");
-  private static final Pattern PAT_EMAIL =
-      Pattern.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+");
-
-  private static final Pattern PAT_LABEL =
-      Pattern.compile("^[a-zA-Z][a-zA-Z0-9]*((=|>=|<=)[+-]?|[+-])\\d+$");
-
   // NOTE: As new search operations are added, please keep the
   // SearchSuggestOracle up to date.
 
+  public static final String FIELD_ADDED = "added";
   public static final String FIELD_AFTER = "after";
   public static final String FIELD_AGE = "age";
   public static final String FIELD_BEFORE = "before";
@@ -93,6 +84,8 @@
   public static final String FIELD_COMMENT = "comment";
   public static final String FIELD_COMMIT = "commit";
   public static final String FIELD_CONFLICTS = "conflicts";
+  public static final String FIELD_DELETED = "deleted";
+  public static final String FIELD_DELTA = "delta";
   public static final String FIELD_DRAFTBY = "draftby";
   public static final String FIELD_FILE = "file";
   public static final String FIELD_IS = "is";
@@ -106,6 +99,7 @@
   public static final String FIELD_PARENTPROJECT = "parentproject";
   public static final String FIELD_PATH = "path";
   public static final String FIELD_PROJECT = "project";
+  public static final String FIELD_PROJECTS = "projects";
   public static final String FIELD_REF = "ref";
   public static final String FIELD_REVIEWER = "reviewer";
   public static final String FIELD_REVIEWERIN = "reviewerin";
@@ -278,7 +272,7 @@
 
   @Operator
   public Predicate<ChangeData> status(String statusName) {
-    if ("open".equals(statusName)) {
+    if ("open".equals(statusName) || "pending".equals(statusName)) {
       return status_open();
 
     } else if ("closed".equals(statusName)) {
@@ -372,6 +366,14 @@
   }
 
   @Operator
+  public Predicate<ChangeData> projects(String name) throws QueryParseException {
+    if (!schema(args.indexes).hasField(ChangeField.PROJECTS)) {
+      throw new QueryParseException("Unsupported operator: " + FIELD_PROJECTS);
+    }
+    return new ProjectPrefixPredicate(name);
+  }
+
+  @Operator
   public Predicate<ChangeData> parentproject(String name) {
     return new ParentProjectPredicate(args.db, args.projectCache,
         args.listChildProjects, args.self, name);
@@ -393,8 +395,8 @@
   @Operator
   public Predicate<ChangeData> topic(String name) {
     if (name.startsWith("^"))
-      return new RegexTopicPredicate(name);
-    return new TopicPredicate(name);
+      return new RegexTopicPredicate(schema(args.indexes), name);
+    return new TopicPredicate(schema(args.indexes), name);
   }
 
   @Operator
@@ -549,7 +551,7 @@
     //
     Collection<GroupReference> suggestions = args.groupBackend.suggest(who, null);
     if (!suggestions.isEmpty()) {
-      HashSet<AccountGroup.UUID> ids = new HashSet<AccountGroup.UUID>();
+      HashSet<AccountGroup.UUID> ids = new HashSet<>();
       for (GroupReference ref : suggestions) {
         ids.add(ref.getUUID());
       }
@@ -677,53 +679,83 @@
     return sortkey_before(sortKey);
   }
 
-  @Override
-  protected Predicate<ChangeData> defaultField(String query)
+  @Operator
+  public Predicate<ChangeData> added(String value)
       throws QueryParseException {
+    return new AddedPredicate(value);
+  }
+
+  @Operator
+  public Predicate<ChangeData> deleted(String value)
+      throws QueryParseException {
+    return new DeletedPredicate(value);
+  }
+
+  @Operator
+  public Predicate<ChangeData> size(String value)
+      throws QueryParseException {
+    return delta(value);
+  }
+
+  @Operator
+  public Predicate<ChangeData> delta(String value)
+      throws QueryParseException {
+    return new DeltaPredicate(value);
+  }
+
+  @Override
+  protected Predicate<ChangeData> defaultField(String query) {
     if (query.startsWith("refs/")) {
       return ref(query);
-
     } else if (DEF_CHANGE.matcher(query).matches()) {
       return change(query);
-
-    } else if (PAT_COMMIT.matcher(query).matches()) {
-      return commit(query);
-
-    } else if (PAT_EMAIL.matcher(query).find()) {
-      try {
-        return Predicate.or(owner(query), reviewer(query));
-      } catch (OrmException err) {
-        throw error("Cannot lookup user", err);
-      }
-
-    } else if (PAT_LABEL.matcher(query).find()) {
-      try {
-        return label(query);
-      } catch (OrmException err) {
-        throw error("Cannot lookup user", err);
-      }
-
-    } else {
-      // Try to match a project name by substring query.
-      final List<ProjectPredicate> predicate =
-          new ArrayList<ProjectPredicate>();
-      for (Project.NameKey name : args.projectCache.all()) {
-        if (name.get().toLowerCase().contains(query.toLowerCase())) {
-          predicate.add(new ProjectPredicate(name.get()));
-        }
-      }
-
-      // If two or more projects contains "query" as substring create an
-      // OrPredicate holding predicates for all these projects, otherwise if
-      // only one contains that, return only that one predicate by itself.
-      if (predicate.size() == 1) {
-        return predicate.get(0);
-      } else if (predicate.size() > 1) {
-        return Predicate.or(predicate);
-      }
-
-      throw error("Unsupported query:" + query);
     }
+
+    List<Predicate<ChangeData>> predicates = Lists.newArrayListWithCapacity(9);
+    try {
+      predicates.add(commit(query));
+    } catch (IllegalArgumentException e) {
+      // Skip.
+    }
+    try {
+      predicates.add(owner(query));
+    } catch (OrmException | QueryParseException e) {
+      // Skip.
+    }
+    try {
+      predicates.add(reviewer(query));
+    } catch (OrmException | QueryParseException e) {
+      // Skip.
+    }
+    try {
+      predicates.add(file(query));
+    } catch (QueryParseException e) {
+      // Skip.
+    }
+    try {
+      predicates.add(label(query));
+    } catch (OrmException | QueryParseException e) {
+      // Skip.
+    }
+    try {
+      predicates.add(message(query));
+    } catch (QueryParseException e) {
+      // Skip.
+    }
+    try {
+      predicates.add(comment(query));
+    } catch (QueryParseException e) {
+      // Skip.
+    }
+    try {
+      predicates.add(projects(query));
+    } catch (QueryParseException e) {
+      // Skip.
+    }
+    predicates.add(ref(query));
+    predicates.add(branch(query));
+    predicates.add(topic(query));
+    return Predicate.or(predicates);
   }
 
   private Set<Account.Id> parseAccount(String who)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
index 9ff416f..cea6af8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
@@ -49,7 +49,7 @@
   }
 
   public static Predicate<ChangeData> open(Provider<ReviewDb> dbProvider) {
-    List<Predicate<ChangeData>> r = new ArrayList<Predicate<ChangeData>>(4);
+    List<Predicate<ChangeData>> r = new ArrayList<>(4);
     for (final Change.Status e : Change.Status.values()) {
       if (e.isOpen()) {
         r.add(new ChangeStatusPredicate(e));
@@ -59,7 +59,7 @@
   }
 
   public static Predicate<ChangeData> closed(Provider<ReviewDb> dbProvider) {
-    List<Predicate<ChangeData>> r = new ArrayList<Predicate<ChangeData>>(4);
+    List<Predicate<ChangeData>> r = new ArrayList<>(4);
     for (final Change.Status e : Change.Status.values()) {
       if (e.isClosed()) {
         r.add(new ChangeStatusPredicate(e));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictKey.java
index e177e37..e64ff13 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictKey.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictKey.java
@@ -15,14 +15,14 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.common.base.Objects;
-import com.google.gerrit.reviewdb.client.Project.SubmitType;
+import com.google.gerrit.extensions.common.SubmitType;
 
 import org.eclipse.jgit.lib.ObjectId;
 
 import java.io.Serializable;
 
 public class ConflictKey implements Serializable {
-  private static final long serialVersionUID = 1L;
+  private static final long serialVersionUID = 2L;
 
   private final ObjectId commit;
   private final ObjectId otherCommit;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
index b1f8406..ca958cf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
@@ -17,8 +17,8 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.data.SubmitTypeRecord;
+import com.google.gerrit.extensions.common.SubmitType;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project.SubmitType;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.MergeException;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/DeletedPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/DeletedPredicate.java
new file mode 100644
index 0000000..478990d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/DeletedPredicate.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IntegerRangePredicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gwtorm.server.OrmException;
+
+public class DeletedPredicate extends IntegerRangePredicate<ChangeData> {
+  DeletedPredicate(String value) throws QueryParseException {
+    super(ChangeField.DELETED, value);
+  }
+
+  @Override
+  protected int getValueInt(ChangeData changeData) throws OrmException {
+    return changeData.changedLines().deletions;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/DeltaPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/DeltaPredicate.java
new file mode 100644
index 0000000..39b860a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/DeltaPredicate.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IntegerRangePredicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.change.ChangeData.ChangedLines;
+import com.google.gwtorm.server.OrmException;
+
+public class DeltaPredicate extends IntegerRangePredicate<ChangeData> {
+  DeltaPredicate(String value) throws QueryParseException {
+    super(ChangeField.DELTA, value);
+  }
+
+  @Override
+  protected int getValueInt(ChangeData changeData) throws OrmException {
+    ChangedLines changedLines = changeData.changedLines();
+    return changedLines.insertions + changedLines.deletions;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
index 54e5a7d..173060b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
@@ -63,13 +63,19 @@
       //
       return false;
     }
+
     ProjectState project = projectCache.get(c.getDest().getParentKey());
     if (project == null) {
       // The project has disappeared.
       //
       return false;
     }
+
     LabelType labelType = type(project.getLabelTypes(), label);
+    if (labelType == null) {
+      return false; // Label is not defined by this project.
+    }
+
     boolean hasVote = false;
     for (PatchSetApproval p : object.currentApprovals()) {
       if (labelType.matches(p)) {
@@ -97,14 +103,7 @@
         return lt;
       }
     }
-
-    for (LabelType lt : types.getLabelTypes()) {
-      if (toFind.equalsIgnoreCase(lt.getAbbreviation())) {
-        return lt;
-      }
-    }
-
-    return LabelType.withDefaultValues(toFind);
+    return null;
   }
 
   private boolean match(Change change, int value, Account.Id approver,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java
index 6d44b96..53d2bbd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java
@@ -25,6 +25,8 @@
 
 import java.util.ArrayList;
 import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
 
 class HasDraftByPredicate extends OperatorPredicate<ChangeData> implements
     ChangeDataSource {
@@ -50,17 +52,17 @@
 
   @Override
   public ResultSet<ChangeData> read() throws OrmException {
-    HashSet<Change.Id> ids = new HashSet<Change.Id>();
+    Set<Change.Id> ids = new HashSet<>();
     for (PatchLineComment sc : args.db.get().patchComments()
         .draftByAuthor(accountId)) {
       ids.add(sc.getKey().getParentKey().getParentKey().getParentKey());
     }
 
-    ArrayList<ChangeData> r = new ArrayList<ChangeData>(ids.size());
+    List<ChangeData> r = new ArrayList<>(ids.size());
     for (Change.Id id : ids) {
       r.add(args.changeDataFactory.create(args.db.get(), id));
     }
-    return new ListResultSet<ChangeData>(r);
+    return new ListResultSet<>(r);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java
index 60f7ffa..9b5aae3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java
@@ -24,12 +24,12 @@
 import com.google.gerrit.server.query.OrPredicate;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.util.LabelVote;
+import com.google.gerrit.server.util.RangeUtil;
+import com.google.gerrit.server.util.RangeUtil.Range;
 import com.google.inject.Provider;
 
 import java.util.List;
 import java.util.Set;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 
 public class LabelPredicate extends OrPredicate<ChangeData> {
   private static final int MAX_LABEL_VALUE = 4;
@@ -102,43 +102,28 @@
       // Try next format.
     }
 
+    Range range;
     if (parsed == null) {
-      Matcher m = Pattern.compile("(>|>=|=|<|<=)([+-]?\\d+)$").matcher(v);
-      if (m.find()) {
-        parsed = new Parsed(v.substring(0, m.start()), m.group(1),
-            value(m.group(2)));
-      } else {
-        parsed = new Parsed(v, "=", 1);
+      range = RangeUtil.getRange(v, -MAX_LABEL_VALUE, MAX_LABEL_VALUE);
+      if (range == null) {
+        range = new Range(v, 1, 1);
       }
+    } else {
+      range = RangeUtil.getRange(
+          parsed.label,
+          parsed.test,
+          parsed.expVal,
+          -MAX_LABEL_VALUE,
+          MAX_LABEL_VALUE);
     }
+    String prefix = range.prefix;
+    int min = range.min;
+    int max = range.max;
 
-    int min, max;
-    switch (parsed.test) {
-      case "=":
-      default:
-        min = max = parsed.expVal;
-        break;
-      case ">":
-        min = parsed.expVal + 1;
-        max = MAX_LABEL_VALUE;
-        break;
-      case ">=":
-        min = parsed.expVal;
-        max = MAX_LABEL_VALUE;
-        break;
-      case "<":
-        min = -MAX_LABEL_VALUE;
-        max = parsed.expVal - 1;
-        break;
-      case "<=":
-        min = -MAX_LABEL_VALUE;
-        max = parsed.expVal;
-        break;
-    }
     List<Predicate<ChangeData>> r =
         Lists.newArrayListWithCapacity(max - min + 1);
     for (int i = min; i <= max; i++) {
-      r.add(onePredicate(args, parsed.label, i));
+      r.add(onePredicate(args, prefix, i));
     }
     return r;
   }
@@ -152,13 +137,6 @@
     }
   }
 
-  private static int value(String value) {
-    if (value.startsWith("+")) {
-      value = value.substring(1);
-    }
-    return Integer.parseInt(value);
-  }
-
   private static Predicate<ChangeData> noLabelQuery(Args args, String label) {
     List<Predicate<ChangeData>> r =
         Lists.newArrayListWithCapacity(2 * MAX_LABEL_VALUE);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java
index c62d7ca..1cbb499 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java
@@ -44,10 +44,10 @@
   public ResultSet<ChangeData> read() throws OrmException {
     Change c = args.db.get().changes().get(id);
     if (c != null) {
-      return new ListResultSet<ChangeData>(Collections.singletonList(
+      return new ListResultSet<>(Collections.singletonList(
           args.changeDataFactory.create(args.db.get(), c)));
     } else {
-      return new ListResultSet<ChangeData>(Collections.<ChangeData> emptyList());
+      return new ListResultSet<>(Collections.<ChangeData> emptyList());
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OrSource.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OrSource.java
index 4f36777..28aef5e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OrSource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OrSource.java
@@ -24,6 +24,8 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
 
 public class OrSource extends OrPredicate<ChangeData> implements ChangeDataSource {
   private int cardinality = -1;
@@ -36,8 +38,8 @@
   public ResultSet<ChangeData> read() throws OrmException {
     // TODO(spearce) This probably should be more lazy.
     //
-    ArrayList<ChangeData> r = new ArrayList<ChangeData>();
-    HashSet<Change.Id> have = new HashSet<Change.Id>();
+    List<ChangeData> r = new ArrayList<>();
+    Set<Change.Id> have = new HashSet<>();
     for (Predicate<ChangeData> p : getChildren()) {
       if (p instanceof ChangeDataSource) {
         for (ChangeData cd : ((ChangeDataSource) p).read()) {
@@ -49,7 +51,7 @@
         throw new OrmException("No ChangeDataSource: " + p);
       }
     }
-    return new ListResultSet<ChangeData>(r);
+    return new ListResultSet<>(r);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java
index 822ffc2..253f719 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java
@@ -15,12 +15,12 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.project.ListChildProjects;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectJson.ProjectInfo;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.OrPredicate;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ProjectPrefixPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ProjectPrefixPredicate.java
new file mode 100644
index 0000000..d0faf0f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ProjectPrefixPredicate.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gwtorm.server.OrmException;
+
+class ProjectPrefixPredicate extends IndexPredicate<ChangeData> {
+  ProjectPrefixPredicate(String prefix) {
+    super(ChangeField.PROJECTS, prefix);
+  }
+
+  @Override
+  public boolean match(ChangeData object) throws OrmException {
+    Change c = object.change();
+    return c != null && c.getDest().getParentKey().get().startsWith(getValue());
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java
index 5673cfc..4808b47 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java
@@ -106,7 +106,7 @@
   }
 
   @Override
-  public Object apply(TopLevelResource rsrc)
+  public List<?> apply(TopLevelResource rsrc)
       throws BadRequestException, AuthException, OrmException {
     List<List<ChangeInfo>> out;
     try {
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 58e5f85..7927cbb 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
@@ -551,7 +551,7 @@
   }
 
   private List<Field> fieldsOf(Class<?> type) {
-    List<Field> r = new ArrayList<Field>();
+    List<Field> r = new ArrayList<>();
     if (type.getSuperclass() != null) {
       r.addAll(fieldsOf(type.getSuperclass()));
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java
index 3a9604f..7d5f1dc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.index.ChangeField;
 import com.google.gerrit.server.index.RegexPredicate;
+import com.google.gerrit.server.index.Schema;
 import com.google.gwtorm.server.OrmException;
 
 import dk.brics.automaton.RegExp;
@@ -25,8 +25,8 @@
 class RegexTopicPredicate extends RegexPredicate<ChangeData> {
   private final RunAutomaton pattern;
 
-  RegexTopicPredicate(String re) {
-    super(ChangeField.TOPIC, re);
+  RegexTopicPredicate(Schema<ChangeData> schema, String re) {
+    super(TopicPredicate.topicField(schema), re);
 
     if (re.startsWith("^")) {
       re = re.substring(1);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TopicPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TopicPredicate.java
index ee4e2ef..7196c9f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TopicPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TopicPredicate.java
@@ -14,14 +14,30 @@
 
 package com.google.gerrit.server.query.change;
 
+import static com.google.gerrit.server.index.ChangeField.TOPIC;
+
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.FieldDef;
 import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gerrit.server.index.Schema;
 import com.google.gwtorm.server.OrmException;
 
 class TopicPredicate extends IndexPredicate<ChangeData> {
-  TopicPredicate(String topic) {
-    super(ChangeField.TOPIC, topic);
+  @SuppressWarnings("deprecation")
+  static FieldDef<ChangeData, ?> topicField(Schema<ChangeData> schema) {
+    if (schema == null) {
+      return ChangeField.LEGACY_TOPIC;
+    }
+    FieldDef<ChangeData, ?> f = schema.getFields().get(TOPIC.getName());
+    if (f != null) {
+      return f;
+    }
+    return schema.getFields().get(ChangeField.LEGACY_TOPIC.getName());
+  }
+
+  TopicPredicate(Schema<ChangeData> schema, String topic) {
+    super(topicField(schema), topic);
   }
 
   @Override
@@ -30,7 +46,11 @@
     if (change == null) {
       return false;
     }
-    return getValue().equals(change.getTopic());
+    String t = change.getTopic();
+    if (t == null && getField() == TOPIC) {
+      t = "";
+    }
+    return getValue().equals(t);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java
index f592530..df49a01 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java
@@ -28,9 +28,9 @@
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.data.PermissionRule.Action;
+import com.google.gerrit.extensions.common.InheritableBoolean;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
@@ -170,7 +170,7 @@
     grant(config, meta, Permission.PUSH, admin, owners);
     grant(config, meta, Permission.SUBMIT, admin, owners);
 
-    config.commit(md);
+    config.commitToNewRef(md, RefNames.REFS_CONFIG);
   }
 
   private void grant(ProjectConfig config, AccessSection section,
@@ -215,7 +215,6 @@
         new LabelValue((short) 0, "No score"),
         new LabelValue((short) -1, "I would prefer this is not merged as is"),
         new LabelValue((short) -2, "This shall not be merged")));
-    type.setAbbreviation("CR");
     type.setCopyMinScore(true);
     c.getLabelSections().put(type.getName(), type);
     return type;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllUsersCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllUsersCreator.java
new file mode 100644
index 0000000..fda5306
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllUsersCreator.java
@@ -0,0 +1,91 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import com.google.gerrit.common.Version;
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+
+/** Creates the {@code All-Users} repository. */
+public class AllUsersCreator {
+  private final GitRepositoryManager mgr;
+  private final AllUsersName allUsersName;
+  private final PersonIdent serverUser;
+
+  @Inject
+  AllUsersCreator(
+      GitRepositoryManager mgr,
+      AllUsersName allUsersName,
+      @GerritPersonIdent PersonIdent serverUser) {
+    this.mgr = mgr;
+    this.allUsersName = allUsersName;
+    this.serverUser = serverUser;
+  }
+
+  public void create() throws IOException, ConfigInvalidException {
+    Repository git = null;
+    try {
+      git = mgr.openRepository(allUsersName);
+      initAllUsers(git);
+    } catch (RepositoryNotFoundException notFound) {
+      try {
+        git = mgr.createRepository(allUsersName);
+        initAllUsers(git);
+      } catch (RepositoryNotFoundException err) {
+        String name = allUsersName.get();
+        throw new IOException("Cannot create repository " + name, err);
+      }
+    } finally {
+      if (git != null) {
+        git.close();
+      }
+    }
+  }
+
+  private void initAllUsers(Repository git)
+      throws IOException, ConfigInvalidException {
+    MetaDataUpdate md = new MetaDataUpdate(
+        GitReferenceUpdated.DISABLED,
+        allUsersName,
+        git);
+    md.getCommitBuilder().setAuthor(serverUser);
+    md.getCommitBuilder().setCommitter(serverUser);
+    md.setMessage("Initialized Gerrit Code Review " + Version.getVersion());
+
+    ProjectConfig config = ProjectConfig.read(md);
+    Project project = config.getProject();
+    project.setDescription("Individual user settings and preferences.");
+
+    AccessSection all = config.getAccessSection(RefNames.REFS_USER + "*", true);
+    all.getPermission(Permission.READ, true).setExclusiveGroup(true);
+    config.commit(md);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceModule.java
index 9aeda09..f500444 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceModule.java
@@ -26,5 +26,12 @@
     bind(DataSourceType.class).annotatedWith(Names.named("mysql")).to(MySql.class);
     bind(DataSourceType.class).annotatedWith(Names.named("oracle")).to(Oracle.class);
     bind(DataSourceType.class).annotatedWith(Names.named("postgresql")).to(PostgreSQL.class);
+    /*
+     * DatabaseMetaData.getDatabaseProductName() returns "sap db" for MaxDB.
+     * For auto-detection of the DB type (com.google.gerrit.pgm.util.SiteProgram#getDbType)
+     * we have to map "sap db" additionally to "maxdb", which is used for explicit configuration.
+     */
+    bind(DataSourceType.class).annotatedWith(Names.named("maxdb")).to(MaxDb.class);
+    bind(DataSourceType.class).annotatedWith(Names.named("sap db")).to(MaxDb.class);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/MaxDb.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/MaxDb.java
new file mode 100644
index 0000000..7ef88f0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/MaxDb.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import static com.google.gerrit.server.schema.JdbcUtil.hostname;
+
+import com.google.gerrit.server.config.ConfigSection;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.io.IOException;
+
+class MaxDb extends BaseDataSourceType {
+
+  private Config cfg;
+
+  @Inject
+  public MaxDb(@GerritServerConfig final Config cfg) {
+    super("com.sap.dbtech.jdbc.DriverSapDB");
+    this.cfg = cfg;
+  }
+
+  @Override
+  public String getUrl() {
+    final StringBuilder b = new StringBuilder();
+    final ConfigSection dbs = new ConfigSection(cfg, "database");
+    b.append("jdbc:sapdb://");
+    b.append(hostname(dbs.optional("hostname")));
+    b.append("/");
+    b.append(dbs.required("database"));
+    return b.toString();
+  }
+
+  @Override
+  public ScriptRunner getIndexScript() throws IOException {
+    return getScriptRunner("index_maxdb.sql");
+  }
+}
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/ReviewDbDatabaseProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/ReviewDbDatabaseProvider.java
index ed77fff..9ceaf1c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/ReviewDbDatabaseProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/ReviewDbDatabaseProvider.java
@@ -36,7 +36,7 @@
   @Override
   public Database<ReviewDb> get() {
     try {
-      return new Database<ReviewDb>(datasource, ReviewDb.class);
+      return new Database<>(datasource, ReviewDb.class);
     } catch (OrmException e) {
       throw new ProvisionException("Cannot create ReviewDb", e);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
index 589d177..298c0d8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
@@ -42,6 +42,7 @@
   File site_path;
 
   private final AllProjectsCreator allProjectsCreator;
+  private final AllUsersCreator allUsersCreator;
   private final PersonIdent serverUser;
   private final DataSourceType dataSourceType;
 
@@ -54,18 +55,21 @@
   public SchemaCreator(SitePaths site,
       @Current SchemaVersion version,
       AllProjectsCreator ap,
+      AllUsersCreator auc,
       @GerritPersonIdent PersonIdent au,
       DataSourceType dst) {
-    this(site.site_path, version, ap, au, dst);
+    this(site.site_path, version, ap, auc, au, dst);
   }
 
   public SchemaCreator(@SitePath File site,
       @Current SchemaVersion version,
       AllProjectsCreator ap,
+      AllUsersCreator auc,
       @GerritPersonIdent PersonIdent au,
       DataSourceType dst) {
     site_path = site;
     allProjectsCreator = ap;
+    allUsersCreator = auc;
     serverUser = au;
     dataSourceType = dst;
     versionNbr = version.getVersionNbr();
@@ -90,6 +94,7 @@
       .setAdministrators(GroupReference.forGroup(admin))
       .setBatchUsers(GroupReference.forGroup(batch))
       .create();
+    allUsersCreator.create();
     dataSourceType.getIndexScript().run(db);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaModule.java
index 5ba7d4c..aaf4607 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaModule.java
@@ -20,6 +20,8 @@
 import com.google.gerrit.server.GerritPersonIdentProvider;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AllProjectsNameProvider;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.AllUsersNameProvider;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.AnonymousCowardNameProvider;
 import com.google.gerrit.server.config.FactoryModule;
@@ -39,6 +41,10 @@
       .toProvider(AllProjectsNameProvider.class)
       .in(SINGLETON);
 
+    bind(AllUsersName.class)
+      .toProvider(AllUsersNameProvider.class)
+      .in(SINGLETON);
+
     bind(String.class).annotatedWith(AnonymousCowardName.class).toProvider(
         AnonymousCowardNameProvider.class);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
index 8800929..11479cc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
@@ -32,7 +32,7 @@
 /** A version of the database schema. */
 public abstract class SchemaVersion {
   /** The current schema version. */
-  public static final Class<Schema_93> C = Schema_93.class;
+  public static final Class<Schema_98> C = Schema_98.class;
 
   public static class Module extends AbstractModule {
     @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_52.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_52.java
deleted file mode 100644
index 76dbdf5..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_52.java
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.ProvisionException;
-
-public class Schema_52 extends SchemaVersion {
-  @Inject
-  Schema_52() {
-    super(new Provider<SchemaVersion>() {
-      public SchemaVersion get() {
-        throw new ProvisionException("Cannot upgrade from 51");
-      }
-    });
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_53.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_53.java
deleted file mode 100644
index cbcda9f..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_53.java
+++ /dev/null
@@ -1,489 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import static com.google.gerrit.common.data.Permission.CREATE;
-import static com.google.gerrit.common.data.Permission.FORGE_AUTHOR;
-import static com.google.gerrit.common.data.Permission.FORGE_COMMITTER;
-import static com.google.gerrit.common.data.Permission.FORGE_SERVER;
-import static com.google.gerrit.common.data.Permission.LABEL;
-import static com.google.gerrit.common.data.Permission.OWNER;
-import static com.google.gerrit.common.data.Permission.PUSH;
-import static com.google.gerrit.common.data.Permission.PUSH_MERGE;
-import static com.google.gerrit.common.data.Permission.PUSH_TAG;
-import static com.google.gerrit.common.data.Permission.READ;
-import static com.google.gerrit.common.data.Permission.SUBMIT;
-
-import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.common.data.PermissionRule;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.client.PatchSetApproval.LabelId;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
-import com.google.gerrit.reviewdb.client.SystemConfig;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.account.GroupUUID;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.schema.Schema_77.LegacyLabelTypes;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Repository;
-
-import java.io.IOException;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-class Schema_53 extends SchemaVersion {
-  private final GitRepositoryManager mgr;
-  private final PersonIdent serverUser;
-
-  private SystemConfig systemConfig;
-  private Map<AccountGroup.Id, GroupReference> groupMap;
-  private LegacyLabelTypes labelTypes;
-  private GroupReference projectOwners;
-
-  private Map<Project.NameKey, Project.NameKey> parentsByProject;
-  private Map<Project.NameKey, List<OldRefRight>> rightsByProject;
-
-  private final String OLD_SUBMIT = "SUBM";
-  private final String OLD_READ = "READ";
-  private final String OLD_OWN = "OWN";
-  private final String OLD_PUSH_TAG = "pTAG";
-  private final String OLD_PUSH_HEAD = "pHD";
-  private final String OLD_FORGE_IDENTITY = "FORG";
-
-  @Inject
-  Schema_53(Provider<Schema_52> prior, GitRepositoryManager mgr,
-      @GerritPersonIdent PersonIdent serverUser) {
-    super(prior);
-    this.mgr = mgr;
-    this.serverUser = serverUser;
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException,
-      SQLException {
-    systemConfig = db.systemConfig().get(new SystemConfig.Key());
-    labelTypes = Schema_77.getLegacyTypes(db);
-
-    assignGroupUUIDs(db);
-    readOldRefRights(db);
-    readProjectParents(db);
-    exportProjectConfig(db);
-
-    deleteActionCategories(db);
-  }
-
-  private void deleteActionCategories(ReviewDb db) throws OrmException {
-    try {
-      Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-      try {
-        stmt.executeUpdate(
-            "DELETE FROM approval_categories WHERE position < 0");
-      } finally {
-        stmt.close();
-      }
-    } catch (SQLException e) {
-      throw new OrmException(e);
-    }
-  }
-
-  private void assignGroupUUIDs(ReviewDb db) throws OrmException {
-    groupMap = new HashMap<AccountGroup.Id, GroupReference>();
-    List<AccountGroup> groups = db.accountGroups().all().toList();
-    for (AccountGroup g : groups) {
-      if (g.getId().equals(systemConfig.ownerGroupId)) {
-        g.setGroupUUID(SystemGroupBackend.PROJECT_OWNERS);
-        projectOwners = GroupReference.forGroup(g);
-
-      } else if (g.getId().equals(systemConfig.anonymousGroupId)) {
-        g.setGroupUUID(SystemGroupBackend.ANONYMOUS_USERS);
-
-      } else if (g.getId().equals(systemConfig.registeredGroupId)) {
-        g.setGroupUUID(SystemGroupBackend.REGISTERED_USERS);
-
-      } else {
-        g.setGroupUUID(GroupUUID.make(g.getName(), serverUser));
-      }
-      groupMap.put(g.getId(), GroupReference.forGroup(g));
-    }
-    db.accountGroups().update(groups);
-
-    systemConfig.adminGroupUUID = toUUID(systemConfig.adminGroupId);
-    systemConfig.batchUsersGroupUUID = toUUID(systemConfig.batchUsersGroupId);
-    db.systemConfig().update(Collections.singleton(systemConfig));
-  }
-
-  private AccountGroup.UUID toUUID(AccountGroup.Id id) {
-    return groupMap.get(id).getUUID();
-  }
-
-  private void exportProjectConfig(ReviewDb db) throws OrmException,
-      SQLException {
-    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-    ResultSet rs = stmt.executeQuery("SELECT * FROM projects ORDER BY name");
-    while (rs.next()) {
-      final String name = rs.getString("name");
-      final Project.NameKey nameKey = new Project.NameKey(name);
-
-      Repository git;
-      try {
-        git = mgr.openRepository(nameKey);
-      } catch (RepositoryNotFoundException notFound) {
-        // A repository may be missing if this project existed only to store
-        // inheritable permissions. For example 'All-Projects'.
-        try {
-          git = mgr.createRepository(nameKey);
-        } catch (IOException err) {
-          throw new OrmException("Cannot create repository " + name, err);
-        }
-      } catch (IOException e) {
-        throw new OrmException(e);
-      }
-      try {
-        MetaDataUpdate md =
-            new MetaDataUpdate(GitReferenceUpdated.DISABLED, nameKey, git);
-        md.getCommitBuilder().setAuthor(serverUser);
-        md.getCommitBuilder().setCommitter(serverUser);
-
-        ProjectConfig config = ProjectConfig.read(md);
-        loadProject(rs, config.getProject());
-        config.getAccessSections().clear();
-        convertRights(config);
-
-        // Grant out read on the config branch by default.
-        //
-        if (config.getProject().getNameKey().equals(systemConfig.wildProjectName)) {
-          AccessSection meta = config.getAccessSection(RefNames.REFS_CONFIG, true);
-          Permission read = meta.getPermission(READ, true);
-          read.getRule(config.resolve(projectOwners), true);
-        }
-
-        md.setMessage("Import project configuration from SQL\n");
-        config.commit(md);
-      } catch (ConfigInvalidException err) {
-        throw new OrmException("Cannot read project " + name, err);
-      } catch (IOException err) {
-        throw new OrmException("Cannot export project " + name, err);
-      } finally {
-        git.close();
-      }
-    }
-    rs.close();
-    stmt.close();
-  }
-
-  private void loadProject(ResultSet rs, Project project) throws SQLException,
-      OrmException {
-    project.setDescription(rs.getString("description"));
-    project.setUseContributorAgreements(asInheritableBoolean(rs, "use_contributor_agreements"));
-
-    switch (rs.getString("submit_type").charAt(0)) {
-      case 'F':
-        project.setSubmitType(Project.SubmitType.FAST_FORWARD_ONLY);
-        break;
-      case 'M':
-        project.setSubmitType(Project.SubmitType.MERGE_IF_NECESSARY);
-        break;
-      case 'A':
-        project.setSubmitType(Project.SubmitType.MERGE_ALWAYS);
-        break;
-      case 'C':
-        project.setSubmitType(Project.SubmitType.CHERRY_PICK);
-        break;
-      default:
-        throw new OrmException("Unsupported submit_type="
-            + rs.getString("submit_type") + " on project " + project.getName());
-    }
-
-    project.setUseSignedOffBy(asInheritableBoolean(rs, "use_signed_off_by"));
-    project.setRequireChangeID(asInheritableBoolean(rs, "require_change_id"));
-    project.setUseContentMerge(asInheritableBoolean(rs, "use_content_merge"));
-    project.setParentName(rs.getString("parent_name"));
-  }
-
-  private static InheritableBoolean asInheritableBoolean(ResultSet rs, String col)
-      throws SQLException {
-    return "Y".equals(rs.getString(col))
-        ? Project.InheritableBoolean.TRUE
-        : Project.InheritableBoolean.INHERIT;
-  }
-
-  private void readOldRefRights(ReviewDb db) throws SQLException {
-    rightsByProject = new HashMap<Project.NameKey, List<OldRefRight>>();
-
-    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-    ResultSet rs = stmt.executeQuery("SELECT * FROM ref_rights");
-    while (rs.next()) {
-      OldRefRight right = new OldRefRight(rs);
-      if (right.group == null || right.category == null) {
-        continue;
-      }
-
-      List<OldRefRight> list;
-
-      list = rightsByProject.get(right.project);
-      if (list == null) {
-        list = new ArrayList<OldRefRight>();
-        rightsByProject.put(right.project, list);
-      }
-      list.add(right);
-    }
-    rs.close();
-    stmt.close();
-  }
-
-  private void readProjectParents(ReviewDb db) throws SQLException {
-    parentsByProject = new HashMap<Project.NameKey, Project.NameKey>();
-    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-    ResultSet rs = stmt.executeQuery("SELECT * FROM projects");
-    while (rs.next()) {
-      String name = rs.getString("name");
-      String parent_name = rs.getString("parent_name");
-      if (parent_name == null) {
-        parent_name = systemConfig.wildProjectName.get();
-      }
-      parentsByProject.put(new Project.NameKey(name), //
-          new Project.NameKey(parent_name));
-    }
-    rs.close();
-    stmt.close();
-  }
-
-  private void convertRights(ProjectConfig config) {
-    List<OldRefRight> myRights =
-        rightsByProject.get(config.getProject().getNameKey());
-    if (myRights == null) {
-      return;
-    }
-
-    for (OldRefRight old : myRights) {
-      AccessSection section = config.getAccessSection(old.ref_pattern, true);
-      GroupReference group = config.resolve(old.group);
-
-      if (OLD_SUBMIT.equals(old.category)) {
-        PermissionRule submit = rule(group);
-        if (old.max_value <= 0) {
-          submit.setDeny();
-        }
-        add(section, SUBMIT, old.exclusive, submit);
-
-      } else if (OLD_READ.equals(old.category)) {
-        if (old.exclusive) {
-          section.getPermission(READ, true).setExclusiveGroup(true);
-          newChangePermission(config, old.ref_pattern).setExclusiveGroup(true);
-        }
-
-        PermissionRule read = rule(group);
-        if (old.max_value <= 0) {
-          read.setDeny();
-        }
-        add(section, READ, old.exclusive, read);
-
-        if (3 <= old.max_value) {
-          newMergePermission(config, old.ref_pattern).add(rule(group));
-        } else if (3 <= inheritedMax(config, old)) {
-          newMergePermission(config, old.ref_pattern).add(deny(group));
-        }
-
-        if (2 <= old.max_value) {
-          newChangePermission(config, old.ref_pattern).add(rule(group));
-        } else if (2 <= inheritedMax(config, old)) {
-          newChangePermission(config, old.ref_pattern).add(deny(group));
-        }
-
-      } else if (OLD_OWN.equals(old.category)) {
-        add(section, OWNER, false, rule(group));
-
-      } else if (OLD_PUSH_TAG.equals(old.category)) {
-        PermissionRule push = rule(group);
-        if (old.max_value <= 0) {
-          push.setDeny();
-        }
-        add(section, PUSH_TAG, old.exclusive, push);
-
-      } else if (OLD_PUSH_HEAD.equals(old.category)) {
-        if (old.exclusive) {
-          section.getPermission(PUSH, true).setExclusiveGroup(true);
-          section.getPermission(CREATE, true).setExclusiveGroup(true);
-        }
-
-        PermissionRule push = rule(group);
-        if (old.max_value <= 0) {
-          push.setDeny();
-        }
-        push.setForce(3 <= old.max_value);
-        add(section, PUSH, old.exclusive, push);
-
-        if (2 <= old.max_value) {
-          add(section, CREATE, old.exclusive, rule(group));
-        } else if (2 <= inheritedMax(config, old)) {
-          add(section, CREATE, old.exclusive, deny(group));
-        }
-
-      } else if (OLD_FORGE_IDENTITY.equals(old.category)) {
-        if (old.exclusive) {
-          section.getPermission(FORGE_AUTHOR, true).setExclusiveGroup(true);
-          section.getPermission(FORGE_COMMITTER, true).setExclusiveGroup(true);
-          section.getPermission(FORGE_SERVER, true).setExclusiveGroup(true);
-        }
-
-        if (1 <= old.max_value) {
-          add(section, FORGE_AUTHOR, old.exclusive, rule(group));
-        }
-
-        if (2 <= old.max_value) {
-          add(section, FORGE_COMMITTER, old.exclusive, rule(group));
-        } else if (2 <= inheritedMax(config, old)) {
-          add(section, FORGE_COMMITTER, old.exclusive, deny(group));
-        }
-
-        if (3 <= old.max_value) {
-          add(section, FORGE_SERVER, old.exclusive, rule(group));
-        } else if (3 <= inheritedMax(config, old)) { add(section, FORGE_SERVER, old.exclusive, deny(group));
-        }
-
-      } else {
-        PermissionRule rule = rule(group);
-        rule.setRange(old.min_value, old.max_value);
-        if (old.min_value == 0 && old.max_value == 0) {
-          rule.setDeny();
-        }
-        LabelType type = labelTypes.byLabel(new LabelId(old.category));
-        String name = type != null ? type.getName() : old.category;
-        add(section, LABEL + name, old.exclusive, rule);
-      }
-    }
-  }
-
-  private static Permission newChangePermission(ProjectConfig config,
-      String name) {
-    if (name.startsWith(AccessSection.REGEX_PREFIX)) {
-      name = AccessSection.REGEX_PREFIX
-          + "refs/for/"
-          + name.substring(AccessSection.REGEX_PREFIX.length());
-    } else {
-      name = "refs/for/" + name;
-    }
-    return config.getAccessSection(name, true).getPermission(PUSH, true);
-  }
-
-  private static Permission newMergePermission(ProjectConfig config,
-      String name) {
-    if (name.startsWith(AccessSection.REGEX_PREFIX)) {
-      name = AccessSection.REGEX_PREFIX
-          + "refs/for/"
-          + name.substring(AccessSection.REGEX_PREFIX.length());
-    } else {
-      name = "refs/for/" + name;
-    }
-    return config.getAccessSection(name, true).getPermission(PUSH_MERGE, true);
-  }
-
-  private static PermissionRule rule(GroupReference group) {
-    return new PermissionRule(group);
-  }
-
-  private static PermissionRule deny(GroupReference group) {
-    PermissionRule rule = rule(group);
-    rule.setDeny();
-    return rule;
-  }
-
-  private int inheritedMax(ProjectConfig config, OldRefRight old) {
-    int max = 0;
-
-    String ref = old.ref_pattern;
-    String category = old.category;
-    AccountGroup.UUID group = old.group.getUUID();
-
-    Project.NameKey project = config.getProject().getParent();
-    if (project == null) {
-      project = systemConfig.wildProjectName;
-    }
-    do {
-      List<OldRefRight> rights = rightsByProject.get(project);
-      if (rights != null) {
-        for (OldRefRight r : rights) {
-          if (r.ref_pattern.equals(ref) //
-              && r.group.getUUID().equals(group) //
-              && r.category.equals(category)) {
-            max = Math.max(max, r.max_value);
-            break;
-          }
-        }
-      }
-      project = parentsByProject.get(project);
-    } while (!project.equals(systemConfig.wildProjectName));
-
-    return max;
-  }
-
-  private static void add(AccessSection section, String name,
-      boolean exclusive, PermissionRule rule) {
-    Permission p = section.getPermission(name, true);
-    if (exclusive) {
-      p.setExclusiveGroup(true);
-    }
-    p.add(rule);
-  }
-
-  private class OldRefRight {
-    final int min_value;
-    final int max_value;
-    final String ref_pattern;
-    final boolean exclusive;
-    final GroupReference group;
-    final String category;
-    final Project.NameKey project;
-
-    OldRefRight(ResultSet rs) throws SQLException {
-      min_value = rs.getInt("min_value");
-      max_value = rs.getInt("max_value");
-      project = new Project.NameKey(rs.getString("project_name"));
-
-      String r = rs.getString("ref_pattern");
-      exclusive = r.startsWith("-");
-      if (exclusive) {
-        r = r.substring(1);
-      }
-      ref_pattern = r;
-
-      category = rs.getString("category_id");
-      group = groupMap.get(new AccountGroup.Id(rs.getInt("group_id")));
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_54.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_54.java
deleted file mode 100644
index 77c6775..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_54.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_54 extends SchemaVersion {
-  @Inject
-  Schema_54(Provider<Schema_53> prior) {
-    super(prior);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_55.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_55.java
deleted file mode 100644
index 006c759..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_55.java
+++ /dev/null
@@ -1,62 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.SystemConfig;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.LocalDiskRepositoryManager;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.RepositoryCache.FileKey;
-import org.eclipse.jgit.util.FS;
-
-import java.io.File;
-import java.util.Collections;
-
-public class Schema_55 extends SchemaVersion {
-  private final GitRepositoryManager mgr;
-
-  @Inject
-  Schema_55(Provider<Schema_54> prior, GitRepositoryManager mgr) {
-    super(prior);
-    this.mgr = mgr;
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
-    SystemConfig sc = db.systemConfig().get(new SystemConfig.Key());
-    String oldName = sc.wildProjectName.get();
-    String newName = "All-Projects";
-    if ("-- All Projects --".equals(oldName)) {
-      ui.message("Renaming \"" + oldName + "\" to \"" + newName + "\"");
-
-      File base = ((LocalDiskRepositoryManager) mgr).getBasePath();
-      File oldDir = FileKey.resolve(new File(base, oldName), FS.DETECTED);
-      File newDir = new File(base, newName + Constants.DOT_GIT_EXT);
-      if (!oldDir.renameTo(newDir)) {
-        throw new OrmException("Cannot rename " + oldDir.getAbsolutePath()
-            + " to " + newDir.getAbsolutePath());
-      }
-
-      sc.wildProjectName = new Project.NameKey(newName);
-      db.systemConfig().update(Collections.singleton(sc));
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_56.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_56.java
deleted file mode 100644
index bcd5f40..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_56.java
+++ /dev/null
@@ -1,88 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefDatabase;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.Repository;
-
-import java.io.IOException;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-
-public class Schema_56 extends SchemaVersion {
-  private final GitRepositoryManager mgr;
-  private final Set<String> keysOne;
-  private final Set<String> keysTwo;
-
-  @Inject
-  Schema_56(Provider<Schema_55> prior, GitRepositoryManager mgr) {
-    super(prior);
-    this.mgr = mgr;
-
-    keysOne = new HashSet<String>();
-    keysTwo = new HashSet<String>();
-
-    keysOne.add(RefNames.REFS_CONFIG);
-    keysTwo.add(RefNames.REFS_CONFIG);
-    keysTwo.add(Constants.HEAD);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) {
-    for (Project.NameKey name : mgr.list()) {
-      Repository git;
-      try {
-        git = mgr.openRepository(name);
-      } catch (IOException e) {
-        ui.message("warning: Cannot open " + name.get());
-        continue;
-      }
-      try {
-        Map<String, Ref> all;
-        try {
-          all = git.getRefDatabase().getRefs(RefDatabase.ALL);
-        } catch (IOException e) {
-          ui.message("warning: " + name.get() + ": Cannot read refs: "
-              + e.getMessage());
-          e.printStackTrace();
-          continue;
-        }
-        if (all.keySet().equals(keysOne) || all.keySet().equals(keysTwo)) {
-          try {
-            RefUpdate update = git.updateRef(Constants.HEAD);
-            update.disableRefLog();
-            update.link(RefNames.REFS_CONFIG);
-          } catch (IOException err) {
-            ui.message("warning: " + name.get() + ": Cannot update HEAD to "
-                + RefNames.REFS_CONFIG + ": " + err.getMessage());
-          }
-        }
-      } finally {
-        git.close();
-      }
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_57.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_57.java
deleted file mode 100644
index 4b4f5456..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_57.java
+++ /dev/null
@@ -1,191 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.common.data.PermissionRule;
-import com.google.gerrit.common.data.PermissionRule.Action;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupName;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.SystemConfig;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.config.AllProjectsNameProvider;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
-import org.eclipse.jgit.util.FS;
-
-import java.io.IOException;
-import java.sql.PreparedStatement;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.util.Collections;
-
-public class Schema_57 extends SchemaVersion {
-  private final SitePaths site;
-  private final GitRepositoryManager mgr;
-  private final PersonIdent serverUser;
-
-  @Inject
-  Schema_57(Provider<Schema_56> prior, SitePaths site,
-      GitRepositoryManager mgr, @GerritPersonIdent PersonIdent serverUser) {
-    super(prior);
-    this.site = site;
-    this.mgr = mgr;
-    this.serverUser = serverUser;
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
-    SystemConfig sc = db.systemConfig().get(new SystemConfig.Key());
-    Project.NameKey allProjects = sc.wildProjectName;
-
-    FileBasedConfig cfg = new FileBasedConfig(site.gerrit_config, FS.DETECTED);
-    boolean cfgDirty = false;
-    try {
-      cfg.load();
-    } catch (ConfigInvalidException err) {
-      throw new OrmException("Cannot read " + site.gerrit_config, err);
-    } catch (IOException err) {
-      throw new OrmException("Cannot read " + site.gerrit_config, err);
-    }
-
-    if (!allProjects.get().equals(AllProjectsNameProvider.DEFAULT)) {
-      ui.message("Setting gerrit.allProjects = " + allProjects.get());
-      cfg.setString("gerrit", null, "allProjects", allProjects.get());
-      cfgDirty = true;
-    }
-
-    try {
-      Repository git = mgr.openRepository(allProjects);
-      try {
-        MetaDataUpdate md =
-            new MetaDataUpdate(GitReferenceUpdated.DISABLED, allProjects, git);
-        md.getCommitBuilder().setAuthor(serverUser);
-        md.getCommitBuilder().setCommitter(serverUser);
-
-        ProjectConfig config = ProjectConfig.read(md);
-        AccessSection cap = config.getAccessSection(AccessSection.GLOBAL_CAPABILITIES, true);
-
-        // Move the Administrators group reference to All-Projects.
-        cap.getPermission(GlobalCapability.ADMINISTRATE_SERVER, true)
-            .add(new PermissionRule(config.resolve(db.accountGroups().get(sc.adminGroupId))));
-
-        // Move the repository.*.createGroup to Create Project.
-        String[] createGroupList = cfg.getStringList("repository", "*", "createGroup");
-
-        // Prepare the account_group_includes query
-        PreparedStatement stmt = ((JdbcSchema) db).getConnection().
-            prepareStatement("SELECT COUNT(1) FROM account_group_includes WHERE group_id = ?");
-        boolean isAccountGroupEmpty = false;
-        try {
-          stmt.setInt(1, sc.batchUsersGroupId.get());
-          ResultSet rs = stmt.executeQuery();
-          if (rs.next()) {
-            isAccountGroupEmpty = rs.getInt(1) == 0;
-          }
-        } finally {
-          stmt.close();
-        }
-
-        for (String name : createGroupList) {
-          AccountGroup.NameKey key = new AccountGroup.NameKey(name);
-          AccountGroupName groupName = db.accountGroupNames().get(key);
-          if (groupName == null) {
-            continue;
-          }
-
-          AccountGroup group = db.accountGroups().get(groupName.getId());
-          if (group == null) {
-            continue;
-          }
-
-          cap.getPermission(GlobalCapability.CREATE_PROJECT, true)
-              .add(new PermissionRule(config.resolve(group)));
-        }
-        if (createGroupList.length != 0) {
-          ui.message("Moved repository.*.createGroup to 'Create Project' capability");
-          cfg.unset("repository", "*", "createGroup");
-          cfgDirty = true;
-        }
-
-        AccountGroup batch = db.accountGroups().get(sc.batchUsersGroupId);
-
-        if (batch != null
-            && db.accountGroupMembers().byGroup(sc.batchUsersGroupId).toList().isEmpty()
-            && !isAccountGroupEmpty) {
-          // If the batch user group is not used, delete it.
-          //
-          db.accountGroups().delete(Collections.singleton(batch));
-
-          AccountGroupName name = db.accountGroupNames().get(batch.getNameKey());
-          if (name != null) {
-            db.accountGroupNames().delete(Collections.singleton(name));
-          }
-        } else if (batch != null) {
-          cap.getPermission(GlobalCapability.PRIORITY, true)
-              .getRule(config.resolve(batch), true)
-              .setAction(Action.BATCH);
-        }
-
-        md.setMessage("Upgrade to Gerrit Code Review schema 57\n");
-        config.commit(md);
-      } catch (SQLException err) {
-        throw new OrmException( "Cannot read account_group_includes", err);
-      } finally {
-        git.close();
-      }
-    } catch (ConfigInvalidException err) {
-      throw new OrmException("Cannot read " + allProjects, err);
-    } catch (IOException err) {
-      throw new OrmException("Cannot update " + allProjects, err);
-    }
-
-    if (cfgDirty) {
-      try {
-        cfg.save();
-      } catch (IOException err) {
-        throw new OrmException("Cannot update " + site.gerrit_config, err);
-      }
-    }
-
-    // We cannot set the columns to NULL, so use 0 and a DELETED tag.
-    sc.adminGroupId = new AccountGroup.Id(0);
-    sc.adminGroupUUID = new AccountGroup.UUID("DELETED");
-    sc.anonymousGroupId = new AccountGroup.Id(0);
-    sc.registeredGroupId = new AccountGroup.Id(0);
-    sc.wildProjectName = new Project.NameKey("DELETED");
-    sc.ownerGroupId = new AccountGroup.Id(0);
-    sc.batchUsersGroupId = new AccountGroup.Id(0);
-    sc.batchUsersGroupUUID = new AccountGroup.UUID("DELETED");
-    sc.registerEmailPrivateKey = "DELETED";
-
-    db.systemConfig().update(Collections.singleton(sc));
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_60.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_60.java
deleted file mode 100644
index 3f49b0c..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_60.java
+++ /dev/null
@@ -1,70 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.util.LinkedList;
-import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-
-public class Schema_60 extends SchemaVersion {
-  @Inject
-  Schema_60(Provider<Schema_59> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
-    Pattern patternA = Pattern.compile("Patch Set ([0-9]+):.*", Pattern.DOTALL);
-    Pattern patternB = Pattern.compile("Uploaded patch set ([0-9]+).");
-    ResultSet<ChangeMessage> results = db.changeMessages().all();
-    List<ChangeMessage> updates = new LinkedList<ChangeMessage>();
-    for (ChangeMessage cm : results) {
-      Change.Id id = cm.getKey().getParentKey();
-      String msg = cm.getMessage();
-      Matcher matcherA = patternA.matcher(msg);
-      Matcher matcherB = patternB.matcher(msg);
-      PatchSet.Id newId = null;
-      if (matcherA.matches()) {
-        int patchSetNum = Integer.parseInt(matcherA.group(1));
-        newId = new PatchSet.Id(id, patchSetNum);
-      } else if (matcherB.matches()) {
-        int patchSetNum = Integer.parseInt(matcherB.group(1));
-        newId = new PatchSet.Id(id, patchSetNum);
-      }
-      if (newId != null) {
-        cm.setPatchSetId(newId);
-        updates.add(cm);
-      }
-      if (updates.size() >= 100) {
-        db.changeMessages().update(updates);
-        updates.clear();
-      }
-    }
-    if (!updates.isEmpty()) {
-      db.changeMessages().update(updates);
-    }
-  }
-}
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_61.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_61.java
deleted file mode 100644
index 890c824..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_61.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_61 extends SchemaVersion {
-  @Inject
-  Schema_61(Provider<Schema_60> prior) {
-    super(prior);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_62.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_62.java
deleted file mode 100644
index 4de8e55..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_62.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_62 extends SchemaVersion {
-  @Inject
-  Schema_62(Provider<Schema_61> prior) {
-    super(prior);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_64.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_64.java
deleted file mode 100644
index e665bdc..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_64.java
+++ /dev/null
@@ -1,118 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.common.collect.Lists;
-import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.config.AllProjectsName;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Repository;
-
-import java.io.IOException;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.List;
-
-public class Schema_64 extends SchemaVersion {
-  private final AllProjectsName allProjects;
-  private final GitRepositoryManager mgr;
-  private final PersonIdent serverUser;
-
-  @Inject
-  Schema_64(Provider<Schema_63> prior,
-      AllProjectsName allProjects,
-      GitRepositoryManager mgr,
-      @GerritPersonIdent PersonIdent serverUser) {
-    super(prior);
-    this.allProjects = allProjects;
-    this.mgr = mgr;
-    this.serverUser = serverUser;
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui)
-      throws OrmException, SQLException {
-    List<GroupReference> groups = Lists.newArrayList();
-    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-    try {
-      ResultSet rs = stmt.executeQuery(
-          "SELECT group_uuid, name FROM account_groups WHERE email_only_authors = 'Y'");
-      try {
-        while (rs.next()) {
-          AccountGroup.UUID uuid = new AccountGroup.UUID(rs.getString(1));
-          GroupReference group = new GroupReference(uuid, rs.getString(2));
-          groups.add(group);
-        }
-      } finally {
-        rs.close();
-      }
-    } finally {
-      stmt.close();
-    }
-
-    if (groups.isEmpty()) {
-      return;
-    }
-    ui.message("Moved account_groups.email_only_authors to 'Email Reviewers' capability");
-
-    Repository git;
-    try {
-      git = mgr.openRepository(allProjects);
-    } catch (IOException e) {
-      throw new OrmException(e);
-    }
-    try {
-      MetaDataUpdate md =
-          new MetaDataUpdate(GitReferenceUpdated.DISABLED, allProjects, git);
-      md.getCommitBuilder().setAuthor(serverUser);
-      md.getCommitBuilder().setCommitter(serverUser);
-
-      ProjectConfig config = ProjectConfig.read(md);
-      AccessSection section =
-          config.getAccessSection(AccessSection.GLOBAL_CAPABILITIES, true);
-      Permission capability =
-          section.getPermission(GlobalCapability.EMAIL_REVIEWERS, true);
-      for (GroupReference group : groups) {
-        capability.getRule(config.resolve(group), true).setDeny();
-      }
-
-      md.setMessage("Upgrade to Gerrit Code Review schema 64\n");
-      config.commit(md);
-    } catch (IOException e) {
-      throw new OrmException(e);
-    } catch (ConfigInvalidException e) {
-      throw new OrmException(e);
-    } finally {
-      git.close();
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_65.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_65.java
deleted file mode 100644
index 636e0c6..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_65.java
+++ /dev/null
@@ -1,461 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.common.base.Strings;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.ContributorAgreement;
-import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.common.data.PermissionRule;
-import com.google.gerrit.common.data.PermissionRule.Action;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
-import com.google.gerrit.reviewdb.client.AccountGroupName;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.account.GroupUUID;
-import com.google.gerrit.server.config.AllProjectsName;
-import com.google.gerrit.server.config.AnonymousCowardName;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.git.VersionedMetaData.BatchMetaDataUpdate;
-import com.google.gerrit.server.util.TimeUtil;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.CommitBuilder;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.util.SystemReader;
-
-import java.io.IOException;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.sql.Timestamp;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.Date;
-import java.util.List;
-import java.util.Map;
-import java.util.TimeZone;
-
-public class Schema_65 extends SchemaVersion {
-  private final AllProjectsName allProjects;
-  private final GitRepositoryManager mgr;
-  private final PersonIdent serverUser;
-  private final @AnonymousCowardName String anonymousCowardName;
-
-  @Inject
-  Schema_65(Provider<Schema_64> prior,
-      AllProjectsName allProjects,
-      GitRepositoryManager mgr,
-      @GerritPersonIdent PersonIdent serverUser,
-      @AnonymousCowardName String anonymousCowardName) {
-    super(prior);
-    this.allProjects = allProjects;
-    this.mgr = mgr;
-    this.serverUser = serverUser;
-    this.anonymousCowardName = anonymousCowardName;
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui)
-      throws OrmException, SQLException {
-    Repository git;
-    try {
-      git = mgr.openRepository(allProjects);
-    } catch (IOException e) {
-      throw new OrmException(e);
-    }
-    try {
-      MetaDataUpdate md =
-          new MetaDataUpdate(GitReferenceUpdated.DISABLED, allProjects, git);
-      ProjectConfig config = ProjectConfig.read(md);
-      Map<Integer, ContributorAgreement> agreements = getAgreementToAdd(db, config);
-      if (agreements.isEmpty()) {
-        return;
-      }
-      ui.message("Moved contributor agreements to project.config");
-
-      // Create the auto verify groups.
-      List<AccountGroup.UUID> adminGroupUUIDs = getAdministrateServerGroups(db, config);
-      for (ContributorAgreement agreement : agreements.values()) {
-        if (agreement.getAutoVerify() != null) {
-          getOrCreateGroupForIndividuals(db, config, adminGroupUUIDs, agreement);
-        }
-      }
-
-      // Scan AccountAgreement
-      long minTime = addAccountAgreements(db, config, adminGroupUUIDs, agreements);
-
-      ProjectConfig base = ProjectConfig.read(md, null);
-      for (ContributorAgreement agreement : agreements.values()) {
-        base.replace(agreement);
-      }
-      base.getAccountsSection().setSameGroupVisibility(
-          config.getAccountsSection().getSameGroupVisibility());
-
-      BatchMetaDataUpdate batch = base.openUpdate(md);
-      try {
-        // Scan AccountGroupAgreement
-        List<AccountGroupAgreement> groupAgreements =
-            getAccountGroupAgreements(db, agreements);
-
-        // Find the earliest change
-        for (AccountGroupAgreement aga : groupAgreements) {
-          minTime = Math.min(minTime, aga.getTime());
-        }
-        minTime -= 60 * 1000; // 1 Minute
-
-        CommitBuilder commit = new CommitBuilder();
-        commit.setAuthor(new PersonIdent(serverUser, new Date(minTime)));
-        commit.setCommitter(new PersonIdent(serverUser, new Date(minTime)));
-        commit.setMessage("Add the ContributorAgreements for upgrade to Gerrit Code Review schema 65\n");
-        batch.write(commit);
-
-        for (AccountGroupAgreement aga : groupAgreements) {
-          AccountGroup group = db.accountGroups().get(aga.groupId);
-          if (group == null) {
-            continue;
-          }
-
-          ContributorAgreement agreement = agreements.get(aga.claId);
-          agreement.getAccepted().add(new PermissionRule(config.resolve(group)));
-          base.replace(agreement);
-
-          PersonIdent ident = null;
-          if (aga.reviewedBy != null) {
-            Account ua = db.accounts().get(aga.reviewedBy);
-            if (ua != null) {
-              String name = ua.getFullName();
-              String email = ua.getPreferredEmail();
-
-              if (email == null || email.isEmpty()) {
-                // No preferred email is configured. Use a generic identity so we
-                // don't leak an address the user may have given us, but doesn't
-                // necessarily want to publish through Git records.
-                //
-                String user = ua.getUserName();
-                if (user == null || user.isEmpty()) {
-                  user = "account-" + ua.getId().toString();
-                }
-
-                String host = SystemReader.getInstance().getHostname();
-                email = user + "@" + host;
-              }
-
-              if (name == null || name.isEmpty()) {
-                final int at = email.indexOf('@');
-                if (0 < at) {
-                  name = email.substring(0, at);
-                } else {
-                  name = anonymousCowardName;
-                }
-              }
-
-              ident = new PersonIdent(name, email, new Date(aga.getTime()), TimeZone.getDefault());
-            }
-          }
-          if (ident == null) {
-            ident = new PersonIdent(serverUser, new Date(aga.getTime()));
-          }
-
-          // Build the commits such that it keeps track of the date added and
-          // who added it.
-          commit = new CommitBuilder();
-          commit.setAuthor(ident);
-          commit.setCommitter(new PersonIdent(serverUser, new Date(aga.getTime())));
-
-          String msg = String.format("Accept %s contributor agreement for %s\n",
-              agreement.getName(), group.getName());
-          if (!Strings.isNullOrEmpty(aga.reviewComments)) {
-            msg += "\n" + aga.reviewComments + "\n";
-          }
-          commit.setMessage(msg);
-          batch.write(commit);
-        }
-
-        // Merge the agreements with the other data in project.config.
-        commit = new CommitBuilder();
-        commit.setAuthor(serverUser);
-        commit.setCommitter(serverUser);
-        commit.setMessage("Upgrade to Gerrit Code Review schema 65\n");
-        commit.addParentId(config.getRevision());
-        batch.write(config, commit);
-
-        // Save the the final metadata.
-        batch.commitAt(config.getRevision());
-      } finally {
-        batch.close();
-      }
-    } catch (IOException e) {
-      throw new OrmException(e);
-    } catch (ConfigInvalidException e) {
-      throw new OrmException(e);
-    } finally {
-      git.close();
-    }
-  }
-
-  private Map<Integer, ContributorAgreement> getAgreementToAdd(
-      ReviewDb db, ProjectConfig config) throws SQLException {
-    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-    try {
-      ResultSet rs = stmt.executeQuery(
-          "SELECT short_name, id, require_contact_information," +
-          "       short_description, agreement_url, auto_verify " +
-          "FROM contributor_agreements WHERE active = 'Y'");
-      try {
-        Map<Integer, ContributorAgreement> agreements = Maps.newHashMap();
-        while (rs.next()) {
-          String name = rs.getString(1);
-          if (config.getContributorAgreement(name) != null) {
-            continue; // already exists
-          }
-          ContributorAgreement a = config.getContributorAgreement(name, true);
-          agreements.put(rs.getInt(2), a);
-
-          a.setRequireContactInformation("Y".equals(rs.getString(3)));
-          a.setDescription(rs.getString(4));
-          a.setAgreementUrl(rs.getString(5));
-          if ("Y".equals(rs.getString(6))) {
-            a.setAutoVerify(new GroupReference(null, null));
-          }
-        }
-        return agreements;
-      } finally {
-        rs.close();
-      }
-    } finally {
-      stmt.close();
-    }
-  }
-
-  private AccountGroup createGroup(ReviewDb db, String groupName,
-      AccountGroup.UUID adminGroupUUID, String description)
-          throws OrmException {
-    final AccountGroup.Id groupId =
-        new AccountGroup.Id(db.nextAccountGroupId());
-    final AccountGroup.NameKey nameKey = new AccountGroup.NameKey(groupName);
-    final AccountGroup.UUID uuid = GroupUUID.make(groupName, serverUser);
-    final AccountGroup group = new AccountGroup(nameKey, groupId, uuid);
-    group.setOwnerGroupUUID(adminGroupUUID);
-    group.setDescription(description);
-    final AccountGroupName gn = new AccountGroupName(group);
-    // first insert the group name to validate that the group name hasn't
-    // already been used to create another group
-    db.accountGroupNames().insert(Collections.singleton(gn));
-    db.accountGroups().insert(Collections.singleton(group));
-    return group;
-  }
-
-  private List<AccountGroup.UUID> getAdministrateServerGroups(
-      ReviewDb db, ProjectConfig cfg) {
-    List<PermissionRule> rules = cfg.getAccessSection(AccessSection.GLOBAL_CAPABILITIES)
-       .getPermission(GlobalCapability.ADMINISTRATE_SERVER)
-       .getRules();
-
-    List<AccountGroup.UUID> groups =
-        Lists.newArrayListWithExpectedSize(rules.size());
-    for (PermissionRule rule : rules) {
-      if (rule.getAction() == Action.ALLOW) {
-        groups.add(rule.getGroup().getUUID());
-      }
-    }
-    if (groups.isEmpty()) {
-      throw new IllegalStateException("no administrator group found");
-    }
-
-    return groups;
-  }
-
-  private GroupReference getOrCreateGroupForIndividuals(ReviewDb db,
-      ProjectConfig config, List<AccountGroup.UUID> adminGroupUUIDs,
-      ContributorAgreement agreement)
-          throws OrmException {
-    if (!agreement.getAccepted().isEmpty()) {
-      return agreement.getAccepted().get(0).getGroup();
-    }
-
-    String name = "CLA Accepted - " + agreement.getName();
-    AccountGroupName agn =
-        db.accountGroupNames().get(new AccountGroup.NameKey(name));
-    AccountGroup ag;
-    if (agn != null) {
-      ag = db.accountGroups().get(agn.getId());
-      if (ag == null) {
-        throw new IllegalStateException(
-            "account group name exists but account group does not: " + name);
-      }
-
-      if (!adminGroupUUIDs.contains(ag.getOwnerGroupUUID())) {
-        throw new IllegalStateException(
-            "individual group exists with non admin owner group: " + name);
-      }
-    } else {
-      ag = createGroup(db, name, adminGroupUUIDs.get(0),
-          String.format("Users who have accepted the %s CLA", agreement.getName()));
-    }
-    GroupReference group = config.resolve(ag);
-    agreement.setAccepted(Lists.newArrayList(new PermissionRule(group)));
-    if (agreement.getAutoVerify() != null) {
-      agreement.setAutoVerify(group);
-    }
-
-    // Don't allow accounts in the same individual CLA group to see each
-    // other in same group visibility mode.
-    List<PermissionRule> sameGroupVisibility =
-        config.getAccountsSection().getSameGroupVisibility();
-    PermissionRule rule = new PermissionRule(group);
-    rule.setDeny();
-    if (!sameGroupVisibility.contains(rule)) {
-      sameGroupVisibility.add(rule);
-    }
-    return group;
-  }
-
-  private long addAccountAgreements(ReviewDb db, ProjectConfig config,
-      List<AccountGroup.UUID> adminGroupUUIDs,
-      Map<Integer, ContributorAgreement> agreements)
-          throws SQLException, OrmException {
-    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-    try {
-      ResultSet rs = stmt.executeQuery(
-          "SELECT account_id, cla_id, accepted_on, reviewed_by," +
-          "       reviewed_on, review_comments " +
-          "FROM account_agreements WHERE status = 'V'");
-      try {
-        long minTime = TimeUtil.nowMs();
-        while (rs.next()) {
-          Account.Id accountId = new Account.Id(rs.getInt(1));
-          Account.Id reviewerId = new Account.Id(rs.getInt(4));
-          if (rs.wasNull()) {
-            reviewerId = accountId;
-          }
-
-          int claId = rs.getInt(2);
-          ContributorAgreement agreement = agreements.get(claId);
-          if (agreement == null) {
-            continue;  // Agreement is invalid
-          }
-
-          Timestamp acceptedOn = rs.getTimestamp(3);
-          minTime = Math.min(minTime, acceptedOn.getTime());
-
-          // Enter Agreement
-          GroupReference individualGroup =
-              getOrCreateGroupForIndividuals(db, config, adminGroupUUIDs, agreement);
-          AccountGroup.Id groupId = db.accountGroups()
-              .byUUID(individualGroup.getUUID())
-              .toList()
-              .get(0)
-              .getId();
-
-          final AccountGroupMember.Key key =
-              new AccountGroupMember.Key(accountId, groupId);
-          AccountGroupMember m = db.accountGroupMembers().get(key);
-          if (m == null) {
-            m = new AccountGroupMember(key);
-            db.accountGroupMembersAudit().insert(
-                Collections.singleton(
-                    new AccountGroupMemberAudit(m, reviewerId, acceptedOn)));
-            db.accountGroupMembers().insert(Collections.singleton(m));
-          }
-        }
-        return minTime;
-      } finally {
-        rs.close();
-      }
-    } finally {
-      stmt.close();
-    }
-  }
-
-  private static class AccountGroupAgreement {
-    private AccountGroup.Id groupId;
-    private int claId;
-    private Timestamp acceptedOn;
-    private Account.Id reviewedBy;
-    private Timestamp reviewedOn;
-    private String reviewComments;
-
-    private long getTime() {
-      return (reviewedOn == null) ? acceptedOn.getTime() : reviewedOn.getTime();
-    }
-  }
-
-  private List<AccountGroupAgreement> getAccountGroupAgreements(
-      ReviewDb db, Map<Integer, ContributorAgreement> agreements)
-          throws SQLException {
-
-    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-    try {
-      ResultSet rs = stmt.executeQuery(
-          "SELECT group_id, cla_id, accepted_on, reviewed_by, reviewed_on, " +
-          "       review_comments " +
-          "FROM account_group_agreements");
-      try {
-        List<AccountGroupAgreement> groupAgreements = Lists.newArrayList();
-        while (rs.next()) {
-          AccountGroupAgreement a = new AccountGroupAgreement();
-          a.groupId = new AccountGroup.Id(rs.getInt(1));
-          a.claId = rs.getInt(2);
-          if (!agreements.containsKey(a.claId)) {
-            continue; // Agreement is invalid
-          }
-          a.acceptedOn = rs.getTimestamp(3);
-          a.reviewedBy = new Account.Id(rs.getInt(4));
-          if (rs.wasNull()) {
-            a.reviewedBy = null;
-          }
-
-          a.reviewedOn = rs.getTimestamp(5);
-          if (rs.wasNull()) {
-            a.reviewedOn = null;
-          }
-
-          a.reviewComments = rs.getString(6);
-          if (rs.wasNull()) {
-            a.reviewComments = null;
-          }
-          groupAgreements.add(a);
-        }
-        Collections.sort(groupAgreements, new Comparator<AccountGroupAgreement>() {
-          @Override
-          public int compare(
-              AccountGroupAgreement a1, AccountGroupAgreement a2) {
-            return Long.compare(a1.getTime(), a2.getTime());
-          }
-        });
-        return groupAgreements;
-      } finally {
-        rs.close();
-      }
-    } finally {
-      stmt.close();
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_66.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_66.java
deleted file mode 100644
index 9ee2c6f..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_66.java
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_66 extends SchemaVersion {
-
-  @Inject
-  Schema_66(Provider<Schema_65> prior) {
-    super(prior);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_67.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_67.java
deleted file mode 100644
index bec2f3f..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_67.java
+++ /dev/null
@@ -1,93 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Sets;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-
-public class Schema_67 extends SchemaVersion {
-
-  @Inject
-  Schema_67(Provider<Schema_66> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui)
-      throws OrmException, SQLException {
-    ui.message("Update ownerGroupId to ownerGroupUUID");
-
-    // Scan all AccountGroup, and find the ones that need the owner_group_id
-    // migrated to owner_group_uuid.
-    Map<AccountGroup.Id, AccountGroup.Id> idMap = Maps.newHashMap();
-    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-    try {
-      ResultSet rs = stmt.executeQuery(
-          "SELECT group_id, owner_group_id FROM account_groups"
-          + " WHERE owner_group_uuid is NULL or owner_group_uuid =''");
-      try {
-        while (rs.next()) {
-          AccountGroup.Id groupId = new AccountGroup.Id(rs.getInt(1));
-          AccountGroup.Id ownerId = new AccountGroup.Id(rs.getInt(2));
-          idMap.put(groupId, ownerId);
-        }
-      } finally {
-        rs.close();
-      }
-    } finally {
-      stmt.close();
-    }
-
-    // Lookup up all groups by ID.
-    Set<AccountGroup.Id> all =
-        Sets.newHashSet(Iterables.concat(idMap.keySet(), idMap.values()));
-    Map<AccountGroup.Id, AccountGroup> groups = Maps.newHashMap();
-    com.google.gwtorm.server.ResultSet<AccountGroup> rs =
-        db.accountGroups().get(all);
-    try {
-      for (AccountGroup group : rs) {
-        groups.put(group.getId(), group);
-      }
-    } finally {
-      rs.close();
-    }
-
-    // Update the ownerGroupUUID.
-    List<AccountGroup> toUpdate = Lists.newArrayListWithCapacity(idMap.size());
-    for (Entry<AccountGroup.Id, AccountGroup.Id> entry : idMap.entrySet()) {
-      AccountGroup group = groups.get(entry.getKey());
-      AccountGroup owner = groups.get(entry.getValue());
-      group.setOwnerGroupUUID(owner.getGroupUUID());
-      toUpdate.add(group);
-    }
-
-    db.accountGroups().update(toUpdate);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_68.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_68.java
deleted file mode 100644
index 4dc2b6e..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_68.java
+++ /dev/null
@@ -1,60 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.sql.SQLException;
-import java.sql.Statement;
-
-public class Schema_68 extends SchemaVersion {
-  @Inject
-  Schema_68(Provider<Schema_67> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(final ReviewDb db, final UpdateUI ui)
-      throws SQLException {
-    final Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-    try {
-      stmt.execute("CREATE INDEX submodule_subscription_access_bySubscription"
-          + " ON submodule_subscriptions (submodule_project_name, submodule_branch_name)");
-    } catch (SQLException e) {
-      // the index creation might have failed because the index exists already,
-      // in this case the exception can be safely ignored,
-      // but there are also other possible reasons for an exception here that
-      // should not be ignored,
-      // -> ask the user whether to ignore this exception or not
-      ui.message("warning: Cannot create index for submodule subscriptions");
-      ui.message(e.getMessage());
-
-      if (ui.isBatch()) {
-        ui.message("you may ignore this warning when running in interactive mode");
-        throw e;
-      } else {
-        final boolean answer = ui.yesno(false, "Ignore warning and proceed with schema upgrade");
-        if (!answer) {
-          throw e;
-        }
-      }
-    } finally {
-      stmt.close();
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_69.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_69.java
deleted file mode 100644
index 9b2bff4..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_69.java
+++ /dev/null
@@ -1,225 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import static com.google.common.base.Strings.isNullOrEmpty;
-
-import com.google.common.base.Preconditions;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Sets;
-import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Repository;
-
-import java.io.IOException;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-import javax.naming.NamingException;
-import javax.naming.ldap.LdapName;
-
-public class Schema_69 extends SchemaVersion {
-  private final GitRepositoryManager mgr;
-  private final PersonIdent serverUser;
-
-  @Inject
-  Schema_69(Provider<Schema_68> prior,
-      GitRepositoryManager mgr,
-      @GerritPersonIdent PersonIdent serverUser) {
-    super(prior);
-    this.mgr = mgr;
-    this.serverUser = serverUser;
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui)
-      throws OrmException, SQLException {
-
-    // Find all groups that have an LDAP type.
-    Map<AccountGroup.UUID, GroupReference> ldapUUIDMap = Maps.newHashMap();
-    Set<AccountGroup.UUID> toResolve = Sets.newHashSet();
-    List<AccountGroup.Id> toDelete = Lists.newArrayList();
-    List<AccountGroup.NameKey> namesToDelete = Lists.newArrayList();
-    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-    try {
-      ResultSet rs = stmt.executeQuery(
-          "SELECT group_id, group_uuid, external_name, name FROM account_groups"
-          + " WHERE group_type ='LDAP'");
-      try {
-        while (rs.next()) {
-          AccountGroup.Id groupId = new AccountGroup.Id(rs.getInt(1));
-          AccountGroup.UUID groupUUID = new AccountGroup.UUID(rs.getString(2));
-          AccountGroup.NameKey name = new AccountGroup.NameKey(rs.getString(4));
-          String dn = rs.getString(3);
-
-          if (isNullOrEmpty(dn)) {
-            // The LDAP group does not have a DN. Determine if the UUID is used.
-            toResolve.add(groupUUID);
-          } else {
-            toDelete.add(groupId);
-            namesToDelete.add(name);
-            GroupReference ref = groupReference(dn);
-            ldapUUIDMap.put(groupUUID, ref);
-          }
-        }
-      } catch (NamingException e) {
-        throw new RuntimeException(e);
-      } finally {
-        rs.close();
-      }
-    } finally {
-      stmt.close();
-    }
-    if (toDelete.isEmpty() && toResolve.isEmpty()) {
-      return; // No ldap groups. Nothing to do.
-    }
-
-    ui.message("Update LDAP groups to be GroupReferences.");
-
-    // Update the groupOwnerUUID for LDAP groups to point to the new UUID.
-    List<AccountGroup> toUpdate = Lists.newArrayList();
-    Set<AccountGroup.UUID> resolveToUpdate = Sets.newHashSet();
-    Map<AccountGroup.UUID, AccountGroup> resolveGroups = Maps.newHashMap();
-    for (AccountGroup g : db.accountGroups().all()) {
-      if (ldapUUIDMap.containsKey(g.getGroupUUID())) {
-        continue; // Ignore the LDAP groups with a valid DN.
-      } else if (toResolve.contains(g.getGroupUUID())) {
-        resolveGroups.put(g.getGroupUUID(), g); // Keep the ones to resolve.
-        continue;
-      }
-
-      GroupReference ref = ldapUUIDMap.get(g.getOwnerGroupUUID());
-      if (ref != null) {
-        // Update the owner group UUID to the new ldap UUID scheme.
-        g.setOwnerGroupUUID(ref.getUUID());
-        toUpdate.add(g);
-      } else if (toResolve.contains(g.getOwnerGroupUUID())) {
-        // The unresolved group is used as an owner.
-        // Add to the list of LDAP groups to be made INTERNAL.
-        resolveToUpdate.add(g.getOwnerGroupUUID());
-      }
-    }
-
-    toResolve.removeAll(resolveToUpdate);
-
-    // Update project.config group references to use the new LDAP GroupReference
-    for (Project.NameKey name : mgr.list()) {
-      Repository git;
-      try {
-        git = mgr.openRepository(name);
-      } catch (RepositoryNotFoundException e) {
-        throw new OrmException(e);
-      } catch (IOException e) {
-        throw new OrmException(e);
-      }
-
-      try {
-        MetaDataUpdate md =
-            new MetaDataUpdate(GitReferenceUpdated.DISABLED, name, git);
-        md.getCommitBuilder().setAuthor(serverUser);
-        md.getCommitBuilder().setCommitter(serverUser);
-
-        ProjectConfig config = ProjectConfig.read(md);
-
-        // Update the existing refences to the new reference.
-        boolean updated = false;
-        for (Map.Entry<AccountGroup.UUID, GroupReference> entry: ldapUUIDMap.entrySet()) {
-          GroupReference ref = config.getGroup(entry.getKey());
-          if (ref != null) {
-            updated = true;
-            ref.setName(entry.getValue().getName());
-            ref.setUUID(entry.getValue().getUUID());
-            config.resolve(ref);
-          }
-        }
-
-        // Determine if a toResolve group is used and should be made INTERNAL.
-        Iterator<AccountGroup.UUID> iter = toResolve.iterator();
-        while (iter.hasNext()) {
-          AccountGroup.UUID uuid = iter.next();
-          if (config.getGroup(uuid) != null) {
-            resolveToUpdate.add(uuid);
-            iter.remove();
-          }
-        }
-
-        if (!updated) {
-          continue;
-        }
-
-        md.setMessage("Switch LDAP group UUIDs to DNs\n");
-        config.commit(md);
-      } catch (IOException e) {
-        throw new OrmException(e);
-      } catch (ConfigInvalidException e) {
-        throw new OrmException(e);
-      } finally {
-        git.close();
-      }
-    }
-
-    for (AccountGroup.UUID uuid : resolveToUpdate) {
-      AccountGroup group = resolveGroups.get(uuid);
-      ui.message(String.format(
-          "*** Group has no DN and is in use: %s",
-          group.getName()));
-    }
-
-    for (AccountGroup.UUID uuid : toResolve) {
-      AccountGroup group = resolveGroups.get(uuid);
-      toDelete.add(group.getId());
-      namesToDelete.add(group.getNameKey());
-    }
-
-    // Update group owners
-    db.accountGroups().update(toUpdate);
-    // Delete existing LDAP groups
-    db.accountGroupNames().deleteKeys(namesToDelete);
-    db.accountGroups().deleteKeys(toDelete);
-  }
-
-  private static GroupReference groupReference(String dn)
-      throws NamingException {
-    LdapName name = new LdapName(dn);
-    Preconditions.checkState(!name.isEmpty(), "Invalid LDAP dn: %s", dn);
-    String cn = name.get(name.size() - 1);
-    int index = cn.indexOf('=');
-    if (index >= 0) {
-      cn = cn.substring(index + 1);
-    }
-    return new GroupReference(new AccountGroup.UUID("ldap:" + dn), "ldap/" + cn);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_70.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_70.java
deleted file mode 100644
index 3868e66..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_70.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_70 extends SchemaVersion {
-  @Inject
-  protected Schema_70(Provider<Schema_69> prior) {
-    super(prior);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_72.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_72.java
deleted file mode 100644
index 748837b..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_72.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_72 extends SchemaVersion {
-  @Inject
-  Schema_72(Provider<Schema_71> prior) {
-    super(prior);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_73.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_73.java
deleted file mode 100644
index 2732a3d..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_73.java
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.sql.SQLException;
-import java.sql.Statement;
-
-
-public class Schema_73 extends SchemaVersion {
-  @Inject
-  Schema_73(Provider<Schema_72> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(final ReviewDb db, final UpdateUI ui)
-      throws SQLException {
-    final Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-    try {
-      stmt.executeUpdate("CREATE INDEX change_messages_byPatchset ON change_messages (patchset_change_id, patchset_patch_set_id )");
-    }
-    finally {
-      stmt.close();
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_74.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_74.java
deleted file mode 100644
index 4a2c477..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_74.java
+++ /dev/null
@@ -1,118 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupById;
-import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.sql.Connection;
-import java.sql.PreparedStatement;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.ArrayList;
-import java.util.HashMap;
-
-/* Handles copying all entries from AccountGroupIncludes(Audit) to the new tables */
-public class Schema_74 extends SchemaVersion {
-  @Inject
-  Schema_74(Provider<Schema_73> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(final ReviewDb db, final UpdateUI ui)
-      throws SQLException, OrmException {
-    // Grab all the groups since we don't have the cache available
-    HashMap<AccountGroup.Id, AccountGroup.UUID> allGroups =
-        new HashMap<AccountGroup.Id, AccountGroup.UUID>();
-    for (AccountGroup ag : db.accountGroups().all()) {
-      allGroups.put(ag.getId(), ag.getGroupUUID());
-    }
-
-    // Initialize some variables
-    Connection conn = ((JdbcSchema) db).getConnection();
-    ArrayList<AccountGroupById> newIncludes =
-        new ArrayList<AccountGroupById>();
-    ArrayList<AccountGroupByIdAud> newIncludeAudits =
-        new ArrayList<AccountGroupByIdAud>();
-
-    // Iterate over all entries in account_group_includes
-    Statement oldGroupIncludesStmt = conn.createStatement();
-    try {
-      ResultSet oldGroupIncludes = oldGroupIncludesStmt.
-          executeQuery("SELECT * FROM account_group_includes");
-      while (oldGroupIncludes.next()) {
-        AccountGroup.Id oldGroupId =
-            new AccountGroup.Id(oldGroupIncludes.getInt("group_id"));
-        AccountGroup.Id oldIncludeId =
-            new AccountGroup.Id(oldGroupIncludes.getInt("include_id"));
-        AccountGroup.UUID uuidFromIncludeId = allGroups.get(oldIncludeId);
-
-        // If we've got an include, but the group no longer exists, don't bother converting
-        if (uuidFromIncludeId == null) {
-          ui.message("Skipping group_id = \"" + oldIncludeId.get() +
-              "\", not a current group");
-          continue;
-        }
-
-        // Create the new include entry
-        AccountGroupById destIncludeEntry = new AccountGroupById(
-            new AccountGroupById.Key(oldGroupId, uuidFromIncludeId));
-
-        // Iterate over all the audits (for this group)
-        PreparedStatement oldAuditsQueryStmt = conn.prepareStatement(
-            "SELECT * FROM account_group_includes_audit WHERE group_id=? AND include_id=?");
-        try {
-          oldAuditsQueryStmt.setInt(1, oldGroupId.get());
-          oldAuditsQueryStmt.setInt(2, oldIncludeId.get());
-          ResultSet oldGroupIncludeAudits = oldAuditsQueryStmt.executeQuery();
-          while (oldGroupIncludeAudits.next()) {
-            Account.Id addedBy = new Account.Id(oldGroupIncludeAudits.getInt("added_by"));
-            int removedBy = oldGroupIncludeAudits.getInt("removed_by");
-
-            // Create the new audit entry
-            AccountGroupByIdAud destAuditEntry =
-                new AccountGroupByIdAud(destIncludeEntry, addedBy,
-                    oldGroupIncludeAudits.getTimestamp("added_on"));
-
-            // If this was a "removed on" entry, note that
-            if (removedBy > 0) {
-              destAuditEntry.removed(new Account.Id(removedBy),
-                  oldGroupIncludeAudits.getTimestamp("removed_on"));
-            }
-            newIncludeAudits.add(destAuditEntry);
-          }
-          newIncludes.add(destIncludeEntry);
-        } finally {
-          oldAuditsQueryStmt.close();
-        }
-      }
-    } finally {
-      oldGroupIncludesStmt.close();
-    }
-
-    // Now insert all of the new entries to the database
-    db.accountGroupById().insert(newIncludes);
-    db.accountGroupByIdAud().insert(newIncludeAudits);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_77.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_77.java
deleted file mode 100644
index facfb75..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_77.java
+++ /dev/null
@@ -1,256 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import static com.google.common.base.Preconditions.checkArgument;
-
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.LabelTypes;
-import com.google.gerrit.common.data.LabelValue;
-import com.google.gerrit.reviewdb.client.PatchSetApproval.LabelId;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.config.AllProjectsName;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.schema.sql.DialectH2;
-import com.google.gwtorm.schema.sql.DialectMySQL;
-import com.google.gwtorm.schema.sql.DialectPostgreSQL;
-import com.google.gwtorm.schema.sql.SqlDialect;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Repository;
-
-import java.io.IOException;
-import java.sql.PreparedStatement;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.List;
-import java.util.Map;
-
-public class Schema_77 extends SchemaVersion {
-  private final GitRepositoryManager mgr;
-  private final AllProjectsName allProjects;
-  private final PersonIdent serverUser;
-
-  @Inject
-  Schema_77(Provider<Schema_76> prior, AllProjectsName allProjects,
-      GitRepositoryManager mgr, @GerritPersonIdent PersonIdent serverUser) {
-    super(prior);
-    this.allProjects = allProjects;
-    this.mgr = mgr;
-    this.serverUser = serverUser;
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
-    try {
-      LegacyLabelTypes labelTypes = getLegacyTypes(db);
-      SqlDialect dialect = ((JdbcSchema) db).getDialect();
-      if (dialect instanceof DialectH2) {
-        alterTable(db, "ALTER TABLE %s ALTER COLUMN %s varchar(255)");
-      } else if (dialect instanceof DialectPostgreSQL) {
-        alterTable(db, "ALTER TABLE %s ALTER %s TYPE varchar(255)");
-      } else if (dialect instanceof DialectMySQL) {
-        alterTable(db, "ALTER TABLE %s MODIFY %s varchar(255) BINARY");
-      } else {
-        alterTable(db, "ALTER TABLE %s MODIFY %s varchar(255)");
-      }
-      migratePatchSetApprovals(db, labelTypes);
-      migrateLabelsToAllProjects(db, labelTypes);
-    } catch (RepositoryNotFoundException e) {
-      throw new OrmException(e);
-    } catch (SQLException e) {
-      throw new OrmException(e);
-    } catch (IOException e) {
-      throw new OrmException(e);
-    } catch (ConfigInvalidException e) {
-      throw new OrmException(e);
-    }
-    ui.message(
-        "Migrated label types from database to All-Projects project.config");
-  }
-
-  private void alterTable(ReviewDb db, String sqlFormat) throws SQLException {
-    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-    try {
-      stmt.executeUpdate(
-          String.format(sqlFormat, "patch_set_approvals", "category_id"));
-    } finally {
-      stmt.close();
-    }
-  }
-
-  private void migrateLabelsToAllProjects(ReviewDb db,
-      LegacyLabelTypes labelTypes) throws SQLException,
-      RepositoryNotFoundException, IOException, ConfigInvalidException {
-    Repository git = mgr.openRepository(allProjects);
-
-    try {
-      MetaDataUpdate md =
-          new MetaDataUpdate(GitReferenceUpdated.DISABLED, allProjects, git);
-      md.getCommitBuilder().setAuthor(serverUser);
-      md.getCommitBuilder().setCommitter(serverUser);
-
-      ProjectConfig config = ProjectConfig.read(md);
-      Map<String, LabelType> configTypes = config.getLabelSections();
-      List<LabelType> newTypes = Lists.newArrayList();
-      for (LegacyLabelType type : labelTypes.getLegacyLabelTypes()) {
-        if (!configTypes.containsKey(type.getName())) {
-          newTypes.add(type);
-        }
-      }
-      newTypes.addAll(configTypes.values());
-      configTypes.clear();
-      for (LabelType type : newTypes) {
-        configTypes.put(type.getName(), type);
-      }
-      md.setMessage("Upgrade to Gerrit Code Review schema 77\n");
-      config.commit(md);
-    } finally {
-      git.close();
-    }
-  }
-
-  private void migratePatchSetApprovals(ReviewDb db,
-      LegacyLabelTypes labelTypes) throws SQLException {
-    PreparedStatement stmt = ((JdbcSchema) db).getConnection().prepareStatement(
-        "UPDATE patch_set_approvals SET category_id = ?, granted=granted"
-        + " WHERE category_id = ?");
-    try {
-      for (LegacyLabelType type : labelTypes.getLegacyLabelTypes()) {
-        stmt.setString(1, type.getName());
-        stmt.setString(2, type.getId());
-        stmt.addBatch();
-      }
-      stmt.executeBatch();
-    } finally {
-      stmt.close();
-    }
-  }
-
-  static class LegacyLabelType extends LabelType {
-    private String id;
-
-    private LegacyLabelType(String name, List<LabelValue> values) {
-      super(name, values);
-    }
-
-    String getId() {
-      return id;
-    }
-
-    private void setId(String id) {
-      checkArgument(id.length() <= 4, "Invalid legacy label ID: \"%s\"", id);
-      this.id = id;
-    }
-  }
-
-  static class LegacyLabelTypes extends LabelTypes {
-    private final List<LegacyLabelType> legacyTypes;
-
-    private final Map<String, LegacyLabelType> byId;
-
-    private LegacyLabelTypes(List<LegacyLabelType> types) {
-      super(types);
-      legacyTypes = types;
-      byId = Maps.newHashMap();
-      for (LegacyLabelType type : types) {
-        byId.put(type.getId(), type);
-      }
-    }
-
-    List<LegacyLabelType> getLegacyLabelTypes() {
-      return legacyTypes;
-    }
-
-    @Override
-    public LegacyLabelType byLabel(LabelId labelId) {
-      LegacyLabelType t = byId.get(labelId.get());
-      return t != null ? t : (LegacyLabelType) super.byLabel(labelId);
-    }
-
-    LegacyLabelType byId(LabelId id) {
-      return byId.get(id.get());
-    }
-  }
-
-  static LegacyLabelTypes getLegacyTypes(ReviewDb db) throws SQLException {
-    List<LegacyLabelType> types = Lists.newArrayListWithCapacity(2);
-    Statement catStmt = ((JdbcSchema) db).getConnection().createStatement();
-    try {
-      ResultSet catRs = catStmt.executeQuery(
-          "SELECT category_id, name, abbreviated_name, function_name, "
-          + " copy_min_score"
-          + " FROM approval_categories"
-          + " ORDER BY position, name");
-      PreparedStatement valStmt = ((JdbcSchema) db).getConnection().prepareStatement(
-              "SELECT value, name"
-                      + " FROM approval_category_values"
-                      + " WHERE category_id = ?");
-      try {
-        while (catRs.next()) {
-          String id = catRs.getString("category_id");
-          valStmt.setString(1, id);
-          List<LabelValue> values = Lists.newArrayListWithCapacity(5);
-          ResultSet valRs = valStmt.executeQuery();
-          while (valRs.next()) {
-            values.add(new LabelValue(
-                valRs.getShort("value"), valRs.getString("name")));
-          }
-          LegacyLabelType type =
-              new LegacyLabelType(getLabelName(catRs.getString("name")), values);
-          type.setId(id);
-          type.setAbbreviation(catRs.getString("abbreviated_name"));
-          type.setFunctionName(catRs.getString("function_name"));
-          type.setCopyMinScore("Y".equals(catRs.getString("copy_min_score")));
-          types.add(type);
-        }
-      } finally {
-        valStmt.close();
-      }
-    } finally {
-      catStmt.close();
-    }
-    return new LegacyLabelTypes(types);
-  }
-
-  private static String getLabelName(String name) {
-    StringBuilder r = new StringBuilder();
-    for (int i = 0; i < name.length(); i++) {
-      char c = name.charAt(i);
-      if (('0' <= c && c <= '9') //
-          || ('a' <= c && c <= 'z') //
-          || ('A' <= c && c <= 'Z') //
-          || (c == '-')) {
-        r.append(c);
-      } else if (c == ' ') {
-        r.append('-');
-      }
-    }
-    return r.toString();
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_78.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_78.java
deleted file mode 100644
index 18ae8b4..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_78.java
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_78 extends SchemaVersion {
-
-  @Inject
-  Schema_78(Provider<Schema_77> prior) {
-    super(prior);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_79.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_79.java
deleted file mode 100644
index c5087ea..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_79.java
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_79 extends SchemaVersion {
-
-  @Inject
-  Schema_79(Provider<Schema_78> prior) {
-    super(prior);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_80.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_80.java
deleted file mode 100644
index 2cf8d2a..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_80.java
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_80 extends SchemaVersion {
-
-  @Inject
-  Schema_80(Provider<Schema_79> prior) {
-    super(prior);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_81.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_81.java
deleted file mode 100644
index 1e3b2b7..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_81.java
+++ /dev/null
@@ -1,159 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.config.AllProjectsName;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Repository;
-
-import java.io.File;
-import java.io.FileFilter;
-import java.io.IOException;
-import java.sql.SQLException;
-
-public class Schema_81 extends SchemaVersion {
-
-  private final File pluginsDir;
-  private final GitRepositoryManager mgr;
-  private final AllProjectsName allProjects;
-  private final PersonIdent serverUser;
-
-  @Inject
-  Schema_81(Provider<Schema_80> prior, SitePaths sitePaths,
-      AllProjectsName allProjects, GitRepositoryManager mgr,
-      @GerritPersonIdent PersonIdent serverUser) {
-    super(prior);
-    this.pluginsDir = sitePaths.plugins_dir;
-    this.mgr = mgr;
-    this.allProjects = allProjects;
-    this.serverUser = serverUser;
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException,
-      SQLException {
-    try {
-      migrateStartReplicationCapability(db, scanForReplicationPlugin());
-    } catch (RepositoryNotFoundException e) {
-      throw new OrmException(e);
-    } catch (SQLException e) {
-      throw new OrmException(e);
-    } catch (IOException e) {
-      throw new OrmException(e);
-    } catch (ConfigInvalidException e) {
-      throw new OrmException(e);
-    }
-  }
-
-  private File[] scanForReplicationPlugin() {
-    File[] matches = null;
-    if (pluginsDir != null && pluginsDir.exists()) {
-      matches = pluginsDir.listFiles(new FileFilter() {
-        @Override
-        public boolean accept(File pathname) {
-          String n = pathname.getName();
-          return (n.endsWith(".jar") || n.endsWith(".jar.disabled"))
-              && pathname.isFile() && n.contains("replication");
-        }
-      });
-    }
-    return matches;
-  }
-
-  private void migrateStartReplicationCapability(ReviewDb db, File[] matches)
-      throws SQLException, RepositoryNotFoundException, IOException,
-      ConfigInvalidException {
-    Description d = new Description();
-    if (matches == null || matches.length == 0) {
-      d.what = Description.Action.REMOVE;
-    } else {
-      d.what = Description.Action.RENAME;
-      d.prefix = nameOf(matches[0]);
-    }
-    migrateStartReplicationCapability(db, d);
-  }
-
-  private void migrateStartReplicationCapability(ReviewDb db, Description d)
-      throws SQLException, RepositoryNotFoundException, IOException,
-      ConfigInvalidException {
-    Repository git = mgr.openRepository(allProjects);
-    try {
-      MetaDataUpdate md =
-          new MetaDataUpdate(GitReferenceUpdated.DISABLED, allProjects, git);
-      md.getCommitBuilder().setAuthor(serverUser);
-      md.getCommitBuilder().setCommitter(serverUser);
-      ProjectConfig config = ProjectConfig.read(md);
-      AccessSection capabilities =
-          config.getAccessSection(AccessSection.GLOBAL_CAPABILITIES);
-      Permission startReplication =
-          capabilities.getPermission("startReplication");
-      if (startReplication == null) {
-        return;
-      }
-      String msg = null;
-      switch (d.what) {
-        case REMOVE:
-          capabilities.remove(startReplication);
-          msg = "Remove startReplication capability, plugin not installed\n";
-          break;
-        case RENAME:
-          capabilities.remove(startReplication);
-          Permission pluginStartReplication =
-              capabilities.getPermission(
-                  String.format("%s-startReplication", d.prefix), true);
-          pluginStartReplication.setRules(startReplication.getRules());
-          msg = "Rename startReplication capability to match updated plugin\n";
-          break;
-      }
-      config.replace(capabilities);
-      md.setMessage(msg);
-      config.commit(md);
-    } finally {
-      git.close();
-    }
-  }
-
-  private static String nameOf(File jar) {
-    String name = jar.getName();
-    if (name.endsWith(".disabled")) {
-      name = name.substring(0, name.lastIndexOf('.'));
-    }
-    int ext = name.lastIndexOf('.');
-    return 0 < ext ? name.substring(0, ext) : name;
-  }
-
-  private static class Description {
-    private enum Action {
-      REMOVE, RENAME
-    }
-    Action what;
-    String prefix;
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_82.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_82.java
deleted file mode 100644
index 939afe0..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_82.java
+++ /dev/null
@@ -1,152 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.common.collect.ImmutableMap;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.jdbc.JdbcExecutor;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.schema.sql.DialectMySQL;
-import com.google.gwtorm.schema.sql.SqlDialect;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.Map;
-import java.util.Set;
-
-public class Schema_82 extends SchemaVersion {
-
-  private Map<String,String> tables = ImmutableMap.of(
-      "account_group_includes_by_uuid", "account_group_by_id",
-      "account_group_includes_by_uuid_audit", "account_group_by_id_aud");
-
-  private Map<String,Index> indexes = ImmutableMap.of(
-      "account_project_watches_byProject",
-      new Index("account_project_watches", "account_project_watches_byP"),
-      "patch_set_approvals_closedByUser",
-      new Index("patch_set_approvals", "patch_set_approvals_closedByU"),
-      "submodule_subscription_access_bySubscription",
-      new Index("submodule_subscriptions", "submodule_subscr_acc_byS")
-      );
-
-  @Inject
-  Schema_82(Provider<Schema_81> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void preUpdateSchema(ReviewDb db) throws OrmException, SQLException {
-    final JdbcSchema s = (JdbcSchema) db;
-    final JdbcExecutor e = new JdbcExecutor(s);
-    renameTables(db, s, e);
-    renameColumn(db, s, e);
-    renameIndexes(db);
-  }
-
-  private void renameTables(final ReviewDb db, final JdbcSchema s,
-      final JdbcExecutor e) throws OrmException, SQLException {
-    SqlDialect dialect = ((JdbcSchema) db).getDialect();
-    final Set<String> existingTables = dialect.listTables(s.getConnection());
-    for (Map.Entry<String, String> entry : tables.entrySet()) {
-      // Does source table exist?
-      if (existingTables.contains(entry.getKey())) {
-        // Does target table exist?
-        if (!existingTables.contains(entry.getValue())) {
-          s.renameTable(e, entry.getKey(), entry.getValue());
-        }
-      }
-    }
-  }
-
-  private void renameColumn(final ReviewDb db, final JdbcSchema s,
-      final JdbcExecutor e) throws SQLException, OrmException {
-    SqlDialect dialect = ((JdbcSchema) db).getDialect();
-    final Set<String> existingColumns =
-        dialect.listColumns(s.getConnection(), "accounts");
-    // Does source column exist?
-    if (!existingColumns.contains("show_username_in_review_category")) {
-      return;
-    }
-    // Does target column exist?
-    if (existingColumns.contains("show_user_in_review")) {
-      return;
-    }
-    s.renameColumn(e, "accounts", "show_username_in_review_category",
-        "show_user_in_review");
-    // MySQL loose check constraint during the column renaming.
-    // Well it doesn't implemented anyway,
-    // check constraints are get parsed but do nothing
-    if (dialect instanceof DialectMySQL) {
-      Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-      try {
-        addCheckConstraint(stmt);
-      } finally {
-        stmt.close();
-      }
-    }
-  }
-
-  private void renameIndexes(ReviewDb db) throws SQLException {
-    SqlDialect dialect = ((JdbcSchema) db).getDialect();
-    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-    try {
-      // MySQL doesn't have alter index stmt, drop & create
-      if (dialect instanceof DialectMySQL) {
-        for (Map.Entry<String, Index> entry : indexes.entrySet()) {
-          stmt.executeUpdate("DROP INDEX " + entry.getKey() + " ON "
-              + entry.getValue().table);
-        }
-        stmt.executeUpdate("CREATE INDEX account_project_watches_byP ON " +
-            "account_project_watches (project_name)");
-        stmt.executeUpdate("CREATE INDEX patch_set_approvals_closedByU ON " +
-            "patch_set_approvals (change_open, account_id, change_sort_key)");
-        stmt.executeUpdate("CREATE INDEX submodule_subscr_acc_bys ON " +
-            "submodule_subscriptions (submodule_project_name, " +
-            "submodule_branch_name)");
-      } else {
-        for (Map.Entry<String, Index> entry : indexes.entrySet()) {
-          stmt.executeUpdate("ALTER INDEX " + entry.getKey() + " RENAME TO "
-              + entry.getValue().index);
-        }
-      }
-    } catch (SQLException e) {
-      // we don't care
-      // better we would check if index was already renamed
-      // gwtorm doesn't expose this functionality
-    } finally {
-      stmt.close();
-    }
-  }
-
-  private void addCheckConstraint(Statement stmt) throws SQLException {
-    // add check constraint for the destination column
-    stmt.executeUpdate("ALTER TABLE accounts ADD CONSTRAINT "
-        + "show_user_in_review_check CHECK "
-        + "(show_user_in_review IN('Y', 'N'))");
-  }
-
-  static class Index {
-    String table;
-    String index;
-
-    Index(String tableName, String indexName) {
-      this.table = tableName;
-      this.index = indexName;
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_83.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_83.java
index 160d41c..f703a35 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_83.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_83.java
@@ -16,11 +16,17 @@
 
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.ProvisionException;
 
 public class Schema_83 extends SchemaVersion {
 
   @Inject
-  Schema_83(Provider<Schema_82> prior) {
-    super(prior);
+  Schema_83() {
+    super(new Provider<SchemaVersion>() {
+      @Override
+      public SchemaVersion get() {
+        throw new ProvisionException("Upgrade first to 2.8 or 2.9");
+      }
+    });
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_71.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_94.java
similarity index 65%
rename from gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_71.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_94.java
index 8d5b943..3d45274 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_71.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_94.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 The Android Open Source Project
+// Copyright (C) 2014 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -22,22 +22,17 @@
 import java.sql.SQLException;
 import java.sql.Statement;
 
-
-public class Schema_71 extends SchemaVersion {
+public class Schema_94 extends SchemaVersion {
   @Inject
-  Schema_71(Provider<Schema_70> prior) {
+  Schema_94(Provider<Schema_93> prior) {
     super(prior);
   }
 
   @Override
-  protected void migrateData(final ReviewDb db, final UpdateUI ui)
-      throws SQLException {
-    final Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-    try {
-      stmt.executeUpdate("UPDATE account_diff_preferences SET show_line_endings='Y'");
-    }
-    finally {
-      stmt.close();
+  protected void migrateData(ReviewDb db, UpdateUI ui) throws SQLException {
+    try (Statement stmt = ((JdbcSchema) db).getConnection().createStatement()) {
+      stmt.execute("CREATE INDEX patch_sets_byRevision"
+          + " ON patch_sets (revision)");
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_59.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_95.java
similarity index 66%
rename from gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_59.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_95.java
index d90ecc1..1c839f7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_59.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_95.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2011 The Android Open Source Project
+// Copyright (C) 2014 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -14,29 +14,32 @@
 
 package com.google.gerrit.server.schema;
 
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-import java.sql.SQLException;
-import java.util.List;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 
-public class Schema_59 extends SchemaVersion {
+import java.io.IOException;
+import java.sql.SQLException;
+
+public class Schema_95 extends SchemaVersion {
+  private final AllUsersCreator allUsersCreator;
+
   @Inject
-  Schema_59(Provider<Schema_58> prior) {
+  Schema_95(Provider<Schema_94> prior, AllUsersCreator allUsersCreator) {
     super(prior);
+    this.allUsersCreator = allUsersCreator;
   }
 
   @Override
   protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException,
       SQLException {
-    List<Change> allChanges = db.changes().all().toList();
-    for (Change change : allChanges) {
-      change.setMergeable(true);
-      change.setLastSha1MergeTested(null);
+    try {
+      allUsersCreator.create();
+    } catch (IOException | ConfigInvalidException e) {
+      throw new OrmException(e);
     }
-    db.changes().update(allChanges);
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_76.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_96.java
similarity index 83%
rename from gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_76.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_96.java
index 8b0b557..bf19213 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_76.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_96.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2014 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -17,9 +17,9 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-public class Schema_76 extends SchemaVersion {
+public class Schema_96 extends SchemaVersion {
   @Inject
-  Schema_76(Provider<Schema_75> prior) {
+  Schema_96(Provider<Schema_95> prior) {
     super(prior);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_58.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_97.java
similarity index 82%
rename from gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_58.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_97.java
index 72b0c8b..0670377 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_58.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_97.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2011 The Android Open Source Project
+// Copyright (C) 2014 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -17,9 +17,9 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-public class Schema_58 extends SchemaVersion {
+public class Schema_97 extends SchemaVersion {
   @Inject
-  Schema_58(Provider<Schema_57> prior) {
+  Schema_97(Provider<Schema_96> prior) {
     super(prior);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_63.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_98.java
similarity index 63%
rename from gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_63.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_98.java
index 761c36a..aea5abc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_63.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_98.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 The Android Open Source Project
+// Copyright (C) 2014 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -16,31 +16,27 @@
 
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.schema.sql.DialectPostgreSQL;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
 import java.sql.SQLException;
 import java.sql.Statement;
 
-public class Schema_63 extends SchemaVersion {
+public class Schema_98 extends SchemaVersion {
   @Inject
-  Schema_63(Provider<Schema_62> prior) {
+  Schema_98(Provider<Schema_97> prior) {
     super(prior);
   }
 
   @Override
   protected void migrateData(ReviewDb db, UpdateUI ui) throws SQLException {
+    ui.message("Migrate user preference showUserInReview to "
+        + "reviewCategoryStrategy");
     Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
     try {
-      if (((JdbcSchema) db).getDialect() instanceof DialectPostgreSQL) {
-        stmt.execute("CREATE INDEX changes_byBranchClosed"
-            + " ON changes (status, dest_project_name, dest_branch_name, sort_key)"
-            + " WHERE open = 'N'");
-      } else {
-        stmt.execute("CREATE INDEX changes_byBranchClosed"
-            + " ON changes (status, dest_project_name, dest_branch_name, sort_key)");
-      }
+      stmt.executeUpdate("UPDATE accounts SET "
+          + "REVIEW_CATEGORY_STRATEGY='NAME' "
+          + "WHERE (SHOW_USER_IN_REVIEW='Y')");
     } finally {
       stmt.close();
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/ScriptRunner.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/ScriptRunner.java
index 12c80f4..d03cb3e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/ScriptRunner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/ScriptRunner.java
@@ -78,7 +78,7 @@
     BufferedReader br = new BufferedReader(new InputStreamReader(in, "UTF-8"));
     try {
       String delimiter = ";";
-      List<String> commands = new ArrayList<String>();
+      List<String> commands = new ArrayList<>();
       StringBuilder buffer = new StringBuilder();
       String line;
       while ((line = br.readLine()) != null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/securestore/DefaultSecureStore.java b/gerrit-server/src/main/java/com/google/gerrit/server/securestore/DefaultSecureStore.java
new file mode 100644
index 0000000..e0e8237
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/securestore/DefaultSecureStore.java
@@ -0,0 +1,98 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.securestore;
+
+import com.google.gerrit.common.FileUtil;
+import com.google.gerrit.extensions.annotations.Export;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.internal.storage.file.LockFile;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+
+import java.io.File;
+import java.io.IOException;
+
+@Singleton
+@Export(DefaultSecureStore.NAME)
+public class DefaultSecureStore implements SecureStore {
+  public static final String NAME = "default";
+
+  private final FileBasedConfig sec;
+
+  @Inject
+  DefaultSecureStore(SitePaths site) {
+    File secureConfig = new File(site.etc_dir, "secure.config");
+    sec = new FileBasedConfig(secureConfig, FS.DETECTED);
+    try {
+      sec.load();
+    } catch (Exception e) {
+      throw new RuntimeException("Cannot load secure.config", e);
+    }
+  }
+
+  @Override
+  public String get(String section, String subsection, String name) {
+    return sec.getString(section, subsection, name);
+  }
+
+  @Override
+  public void set(String section, String subsection, String name, String value) {
+    if (value != null) {
+      sec.setString(section, subsection, name, value);
+    } else {
+      sec.unset(section, subsection, name);
+    }
+    save();
+  }
+
+  @Override
+  public void unset(String section, String subsection, String name) {
+    sec.unset(section, subsection, name);
+    save();
+  }
+
+  private void save() {
+    try {
+      saveSecure(sec);
+    } catch (IOException e) {
+      throw new RuntimeException("Cannot save secure.config", e);
+    }
+  }
+
+  private static void saveSecure(final FileBasedConfig sec) throws IOException {
+    if (FileUtil.modified(sec)) {
+      final byte[] out = Constants.encode(sec.toText());
+      final File path = sec.getFile();
+      final LockFile lf = new LockFile(path, FS.DETECTED);
+      if (!lf.lock()) {
+        throw new IOException("Cannot lock " + path);
+      }
+      try {
+        FileUtil.chmod(0600, new File(path.getParentFile(), path.getName()
+            + ".lock"));
+        lf.write(out);
+        if (!lf.commit()) {
+          throw new IOException("Cannot commit write to " + path);
+        }
+      } finally {
+        lf.unlock();
+      }
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/LogUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/securestore/SecureStore.java
similarity index 63%
rename from gerrit-server/src/main/java/com/google/gerrit/server/util/LogUtil.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/securestore/SecureStore.java
index ba39a26..3fe00f4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/LogUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/securestore/SecureStore.java
@@ -12,15 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.util;
+package com.google.gerrit.server.securestore;
 
-import com.google.common.base.Strings;
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
 
-public class LogUtil {
+@ExtensionPoint
+public interface SecureStore {
 
-  private static final String LOG4J_CONFIGURATION = "log4j.configuration";
+  String get(String section, String subsection, String name);
 
-  public static boolean shouldConfigureLogSystem() {
-    return Strings.isNullOrEmpty(System.getProperty(LOG4J_CONFIGURATION));
-  }
+  void set(String section, String subsection, String name, String value);
+
+  void unset(String section, String subsection, String name);
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/securestore/SecureStoreData.java b/gerrit-server/src/main/java/com/google/gerrit/server/securestore/SecureStoreData.java
new file mode 100644
index 0000000..b925105
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/securestore/SecureStoreData.java
@@ -0,0 +1,77 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.securestore;
+
+import com.google.common.base.Objects;
+
+import java.io.File;
+import java.net.URL;
+import java.net.URLClassLoader;
+
+public class SecureStoreData {
+  public final File pluginFile;
+  public final String storeName;
+  public final String className;
+
+  public SecureStoreData(String pluginName, String className, File jarFile,
+      String storeName) {
+    this.className = className;
+    this.pluginFile = jarFile;
+    this.storeName = String.format("%s/%s", pluginName, storeName);
+  }
+
+  public String getStoreName() {
+    return storeName;
+  }
+
+  public Class<? extends SecureStore> load() {
+    return load(pluginFile);
+  }
+
+  @SuppressWarnings("unchecked")
+  public Class<? extends SecureStore> load(File pluginFile) {
+    try {
+      URL[] pluginJarUrls = new URL[] {pluginFile.toURI().toURL()};
+      ClassLoader currentCL = Thread.currentThread().getContextClassLoader();
+      final URLClassLoader newClassLoader =
+          new URLClassLoader(pluginJarUrls, currentCL);
+      Thread.currentThread().setContextClassLoader(newClassLoader);
+      return (Class<? extends SecureStore>) newClassLoader.loadClass(className);
+    } catch (Exception e) {
+      throw new SecureStoreException(String.format(
+          "Cannot load secure store implementation for %s", storeName), e);
+    }
+  }
+
+  @Override
+  public String toString() {
+    return Objects.toStringHelper(this).add("storeName", storeName)
+        .add("className", className).add("file", pluginFile).toString();
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj instanceof SecureStoreData) {
+      SecureStoreData o = (SecureStoreData) obj;
+      return storeName.equals(o.storeName);
+    }
+    return false;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(storeName);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_75.java b/gerrit-server/src/main/java/com/google/gerrit/server/securestore/SecureStoreException.java
similarity index 66%
copy from gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_75.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/securestore/SecureStoreException.java
index a847f25..01450f8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_75.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/securestore/SecureStoreException.java
@@ -12,14 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.schema;
+package com.google.gerrit.server.securestore;
 
-import com.google.inject.Inject;
-import com.google.inject.Provider;
+public class SecureStoreException extends RuntimeException {
+  private static final long serialVersionUID = 5581700510568485065L;
 
-public class Schema_75 extends SchemaVersion {
-  @Inject
-  Schema_75(Provider<Schema_74> prior) {
-    super(prior);
+  SecureStoreException(String msg) {
+    super(msg);
+  }
+
+  SecureStoreException(String msg, Exception e) {
+    super(msg, e);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/tools/ToolsCatalog.java b/gerrit-server/src/main/java/com/google/gerrit/server/tools/ToolsCatalog.java
index a2b0ad1..3e41858 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/tools/ToolsCatalog.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/tools/ToolsCatalog.java
@@ -69,7 +69,7 @@
   }
 
   private static SortedMap<String, Entry> readToc() throws IOException {
-    SortedMap<String, Entry> toc = new TreeMap<String, Entry>();
+    SortedMap<String, Entry> toc = new TreeMap<>();
     final BufferedReader br =
         new BufferedReader(new InputStreamReader(new ByteArrayInputStream(
             read("TOC")), "UTF-8"));
@@ -81,7 +81,7 @@
       }
     }
 
-    final List<Entry> all = new ArrayList<Entry>(toc.values());
+    final List<Entry> all = new ArrayList<>(toc.values());
     for (Entry e : all) {
       String path = dirOf(e.getPath());
       while (path != null) {
@@ -159,7 +159,7 @@
       if (type == Type.FILE) {
         this.children = Collections.emptyList();
       } else {
-        this.children = new ArrayList<Entry>();
+        this.children = new ArrayList<>();
       }
     }
 
@@ -167,7 +167,7 @@
       this.type = type;
       this.mode = mode;
       this.path = path;
-      this.children = new ArrayList<Entry>();
+      this.children = new ArrayList<>();
     }
 
     public Type getType() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/MostSpecificComparator.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/MostSpecificComparator.java
index 804a7ec..1cb180b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/MostSpecificComparator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/MostSpecificComparator.java
@@ -28,7 +28,7 @@
  * name and the regex string shortest example. A shorter distance is a more
  * specific match.
  * <li>2 - Finites first, infinities after.
- * <li>3 - Number of transitions.
+ * <li>3 - Number of transitions.  More transitions is more specific.
  * <li>4 - Length of the expression text.
  * </ul>
  *
@@ -72,7 +72,7 @@
       }
     }
     if (cmp == 0) {
-      cmp = transitions(pattern1) - transitions(pattern2);
+      cmp = transitions(pattern2) - transitions(pattern1);
     }
     if (cmp == 0) {
       cmp = pattern2.length() - pattern1.length();
@@ -86,7 +86,7 @@
       example = RefControl.shortestExample(pattern);
 
     } else if (pattern.endsWith("/*")) {
-      example = pattern.substring(0, pattern.length() - 1) + '1';
+      example = pattern;
 
     } else if (pattern.equals(refName)) {
       return 0;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/RangeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/RangeUtil.java
new file mode 100644
index 0000000..5c5e2f9
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/RangeUtil.java
@@ -0,0 +1,119 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.util;
+
+import com.google.common.primitives.Ints;
+import com.google.gerrit.common.Nullable;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public final class RangeUtil {
+  private static final Pattern RANGE_PATTERN =
+      Pattern.compile("(>|>=|=|<|<=|)([+-]?\\d+)$");
+
+  private RangeUtil() {}
+
+  public static class Range {
+    /** The prefix of the query, before the range component. */
+    public final String prefix;
+
+    /** The minimum value specified in the query, inclusive. */
+    public final int min;
+
+    /** The maximum value specified in the query, inclusive. */
+    public final int max;
+
+    public Range(String prefix, int min, int max) {
+      this.prefix = prefix;
+      this.min = min;
+      this.max = max;
+    }
+  }
+
+  /**
+   * Determine the range of values being requested in the given query.
+   *
+   * @param rangeQuery the raw query, e.g. "added:>12345"
+   * @param minValue the minimum possible value for the field, inclusive
+   * @param maxValue the maximum possible value for the field, inclusive
+   * @return the calculated {@link Range}, or null if the query is invalid
+   */
+  @Nullable
+  public static Range getRange(String rangeQuery, int minValue, int maxValue) {
+    Matcher m = RANGE_PATTERN.matcher(rangeQuery);
+    String prefix;
+    String test;
+    Integer queryInt;
+    if (m.find()) {
+      prefix = rangeQuery.substring(0, m.start());
+      test = m.group(1);
+      queryInt = value(m.group(2));
+      if (queryInt == null) {
+        return null;
+      }
+    } else {
+      return null;
+    }
+
+    return getRange(prefix, test, queryInt, minValue, maxValue);
+  }
+
+  /**
+   * Determine the range of values being requested in the given query.
+   *
+   * @param prefix a prefix string which is copied into the range
+   * @param test the test operator, one of &gt;, &gt;=, =, &lt;, or &lt;=
+   * @param queryInt the integer being queried
+   * @param minValue the minimum possible value for the field, inclusive
+   * @param maxValue the maximum possible value for the field, inclusive
+   * @return the calculated {@link Range}
+   */
+  public static Range getRange(
+      String prefix, String test, int queryInt, int minValue, int maxValue) {
+    int min, max;
+    switch (test) {
+      case "=":
+      default:
+        min = max = queryInt;
+        break;
+      case ">":
+        min = Ints.saturatedCast(queryInt + 1L);
+        max = maxValue;
+        break;
+      case ">=":
+        min = queryInt;
+        max = maxValue;
+        break;
+      case "<":
+        min = minValue;
+        max = Ints.saturatedCast(queryInt - 1L);
+        break;
+      case "<=":
+        min = minValue;
+        max = queryInt;
+        break;
+    }
+
+    return new Range(prefix, min, max);
+  }
+
+  private static Integer value(String value) {
+    if (value.startsWith("+")) {
+      value = value.substring(1);
+    }
+    return Ints.tryParse(value);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/SubmoduleSectionParser.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/SubmoduleSectionParser.java
index fbb3e93..8970425 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/SubmoduleSectionParser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/SubmoduleSectionParser.java
@@ -60,8 +60,7 @@
   }
 
   public List<SubmoduleSubscription> parseAllSections() {
-    List<SubmoduleSubscription> parsedSubscriptions =
-        new ArrayList<SubmoduleSubscription>();
+    List<SubmoduleSubscription> parsedSubscriptions = new ArrayList<>();
     for (final String id : bbc.getSubsections("submodule")) {
       final SubmoduleSubscription subscription = parse(id);
       if (subscription != null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/SystemLog.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/SystemLog.java
new file mode 100644
index 0000000..ba31f56
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/SystemLog.java
@@ -0,0 +1,134 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.util;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.common.Die;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.apache.log4j.Appender;
+import org.apache.log4j.AsyncAppender;
+import org.apache.log4j.DailyRollingFileAppender;
+import org.apache.log4j.Layout;
+import org.apache.log4j.LogManager;
+import org.apache.log4j.Logger;
+import org.apache.log4j.helpers.OnlyOnceErrorHandler;
+import org.apache.log4j.spi.ErrorHandler;
+import org.apache.log4j.spi.LoggingEvent;
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+
+@Singleton
+public class SystemLog {
+
+  private static final org.slf4j.Logger log = LoggerFactory
+      .getLogger(SystemLog.class);
+  private static final String LOG4J_CONFIGURATION = "log4j.configuration";
+  private final SitePaths site;
+  private final Config config;
+
+  @Inject
+  public SystemLog(final SitePaths site, @GerritServerConfig Config config) {
+    this.site = site;
+    this.config = config;
+  }
+
+  public static boolean shouldConfigure() {
+    return Strings.isNullOrEmpty(System.getProperty(LOG4J_CONFIGURATION));
+  }
+
+  public static Appender createAppender(File logdir, String name, Layout layout) {
+    final DailyRollingFileAppender dst = new DailyRollingFileAppender();
+    dst.setName(name);
+    dst.setLayout(layout);
+    dst.setEncoding("UTF-8");
+    dst.setFile(new File(resolve(logdir), name).getPath());
+    dst.setImmediateFlush(true);
+    dst.setAppend(true);
+    dst.setErrorHandler(new DieErrorHandler());
+    dst.activateOptions();
+    dst.setErrorHandler(new OnlyOnceErrorHandler());
+    return dst;
+  }
+
+  public AsyncAppender createAsyncAppender(String name, Layout layout) {
+    AsyncAppender async = new AsyncAppender();
+    async.setBlocking(true);
+    async.setBufferSize(config.getInt("core", "asyncLoggingBufferSize", 64));
+    async.setLocationInfo(false);
+
+    if (shouldConfigure()) {
+      async.addAppender(createAppender(site.logs_dir, name, layout));
+    } else {
+      Appender appender = LogManager.getLogger(name).getAppender(name);
+      if (appender != null) {
+        async.addAppender(appender);
+      } else {
+        log.warn("No appender with the name: " + name + " was found. " + name
+            + " logging is disabled");
+      }
+    }
+    async.activateOptions();
+    return async;
+  }
+
+  private static File resolve(final File logs_dir) {
+    try {
+      return logs_dir.getCanonicalFile();
+    } catch (IOException e) {
+      return logs_dir.getAbsoluteFile();
+    }
+  }
+
+  private static final class DieErrorHandler implements ErrorHandler {
+    @Override
+    public void error(String message, Exception e, int errorCode,
+        LoggingEvent event) {
+      error(e != null ? e.getMessage() : message);
+    }
+
+    @Override
+    public void error(String message, Exception e, int errorCode) {
+      error(e != null ? e.getMessage() : message);
+    }
+
+    @Override
+    public void error(String message) {
+      throw new Die("Cannot open log file: " + message);
+    }
+
+    @Override
+    public void activateOptions() {
+    }
+
+    @Override
+    public void setAppender(Appender appender) {
+    }
+
+    @Override
+    public void setBackupAppender(Appender appender) {
+    }
+
+    @Override
+    public void setLogger(Logger logger) {
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java
index 2a67c90..f8bad77 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java
@@ -72,8 +72,7 @@
     };
   }
 
-  private static final ThreadLocal<RequestContext> local =
-      new ThreadLocal<RequestContext>();
+  private static final ThreadLocal<RequestContext> local = new ThreadLocal<>();
 
   @Inject
   ThreadLocalRequestContext() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/TimeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/TimeUtil.java
index 4c4b96e..6bc261f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/TimeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/TimeUtil.java
@@ -28,6 +28,11 @@
     return new Timestamp(nowMs());
   }
 
+  public static Timestamp roundTimestampToSecond(Timestamp t) {
+    long milliseconds = (t.getTime()/1000) * 1000;
+    return new Timestamp(milliseconds);
+  }
+
   private TimeUtil() {
   }
 }
diff --git a/gerrit-server/src/main/java/gerrit/PRED_commit_edits_2.java b/gerrit-server/src/main/java/gerrit/PRED_commit_edits_2.java
index ab63207..cddbf1f 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_commit_edits_2.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_commit_edits_2.java
@@ -161,4 +161,4 @@
     }
     return new Text(reader.open(tw.getObjectId(0), Constants.OBJ_BLOB));
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-server/src/main/java/gerrit/PRED_project_default_submit_type_1.java b/gerrit-server/src/main/java/gerrit/PRED_project_default_submit_type_1.java
index 0f173c7..a471450 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_project_default_submit_type_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_project_default_submit_type_1.java
@@ -14,7 +14,7 @@
 
 package gerrit;
 
-import com.google.gerrit.reviewdb.client.Project.SubmitType;
+import com.google.gerrit.extensions.common.SubmitType;
 import com.google.gerrit.rules.StoredValues;
 import com.google.gerrit.server.project.ChangeControl;
 
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.vm
index 1eb6842..f1d3e90 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.vm
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.vm
@@ -41,7 +41,7 @@
 To unsubscribe, visit $email.settingsUrl
 #set ($notblank = 1)
 #end
-#if ($notblank == 1)
+#if ($notblank)
 
 #end
 Gerrit-MessageType: $messageClass
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeSubject.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeSubject.vm
index acec1d1..1d5b33a 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeSubject.vm
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeSubject.vm
@@ -34,4 +34,4 @@
 #macro(ellipsis $length $str)
 #if($str.length() > $length)#set($length = $length - 3)${str.substring(0,$length)}...#else$str#end
 #end
-Change in $projectName.replaceAll('/.*/', '...')[$branch.shortName]: #ellipsis(63, $change.subject)
+Change in ${projectName.replaceAll('/.*/', '...')}[$branch.shortName]: #ellipsis(63, $change.subject)
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 d5db854..1ed35be 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
@@ -15,8 +15,8 @@
 package com.google.gerrit.rules;
 
 import static com.google.gerrit.common.data.Permission.LABEL;
+import static com.google.gerrit.server.project.Util.allow;
 import static com.google.gerrit.server.project.Util.category;
-import static com.google.gerrit.server.project.Util.grant;
 import static com.google.gerrit.server.project.Util.value;
 
 import com.google.gerrit.common.data.LabelType;
@@ -74,8 +74,8 @@
     local.getLabelSections().put(V.getName(), V);
     local.getLabelSections().put(Q.getName(), Q);
     util.add(local);
-    grant(local, LABEL + V.getName(), -1, +1, SystemGroupBackend.REGISTERED_USERS, "refs/heads/*");
-    grant(local, LABEL + Q.getName(), -1, +1, SystemGroupBackend.REGISTERED_USERS, "refs/heads/master");
+    allow(local, LABEL + V.getName(), -1, +1, SystemGroupBackend.REGISTERED_USERS, "refs/heads/*");
+    allow(local, LABEL + Q.getName(), -1, +1, SystemGroupBackend.REGISTERED_USERS, "refs/heads/master");
   }
 
   @Override
diff --git a/gerrit-server/src/test/java/com/google/gerrit/rules/PrologTestCase.java b/gerrit-server/src/test/java/com/google/gerrit/rules/PrologTestCase.java
index 19edaf4..c697400 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/rules/PrologTestCase.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/rules/PrologTestCase.java
@@ -57,7 +57,7 @@
 
   protected void load(String pkg, String prologResource, Module... modules)
       throws CompileException, IOException {
-    ArrayList<Module> moduleList = new ArrayList<Module>();
+    ArrayList<Module> moduleList = new ArrayList<>();
     moduleList.add(new PrologModule.EnvironmentModule());
     moduleList.addAll(Arrays.asList(modules));
 
@@ -74,7 +74,7 @@
         SymbolTerm.intern(pkg),
         new StructureTerm(test_1, new VariableTerm()));
 
-    tests = new ArrayList<Term>();
+    tests = new ArrayList<>();
     for (Term[] pair : env.all(Prolog.BUILTIN, "clause", head, new VariableTerm())) {
       tests.add(pair[0]);
     }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/change/CommentsTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/change/CommentsTest.java
index 6c24f00..a30fa92 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/change/CommentsTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/change/CommentsTest.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.change;
 
+import static com.google.inject.Scopes.SINGLETON;
 import static org.easymock.EasyMock.createMock;
 import static org.easymock.EasyMock.expect;
 import static org.easymock.EasyMock.expectLastCall;
@@ -36,37 +37,86 @@
 import com.google.gerrit.reviewdb.client.CommentRange;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.PatchLineCommentAccess;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.PatchLineCommentsUtil;
+import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountInfo;
+import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.server.config.AnonymousCowardNameProvider;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitModule;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.util.TimeUtil;
+import com.google.gerrit.testutil.TestChanges;
+import com.google.gerrit.testutil.ConfigSuite;
+import com.google.gerrit.testutil.FakeAccountCache;
+import com.google.gerrit.testutil.InMemoryRepositoryManager;
 import com.google.gwtorm.server.ListResultSet;
+import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.AbstractModule;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
 import com.google.inject.TypeLiteral;
+import com.google.inject.util.Providers;
 
 import org.easymock.IAnswer;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.PersonIdent;
 import org.junit.Before;
 import org.junit.Test;
+import org.junit.runner.RunWith;
 
 import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.TimeZone;
 
-public class CommentsTest {
+@RunWith(ConfigSuite.class)
+public class CommentsTest  {
+  private static final TimeZone TZ =
+      TimeZone.getTimeZone("America/Los_Angeles");
+
+  @ConfigSuite.Parameter
+  public Config config;
+
+  @ConfigSuite.Config
+  public static @GerritServerConfig Config noteDbEnabled() {
+    @GerritServerConfig Config cfg = new Config();
+    cfg.setBoolean("notedb", null, "write", true);
+    cfg.setBoolean("notedb", "publishedComments", "read", true);
+    return cfg;
+  }
 
   private Injector injector;
+  private Project.NameKey project;
+  private InMemoryRepositoryManager repoManager;
+  private PatchLineCommentsUtil plcUtil;
   private RevisionResource revRes1;
   private RevisionResource revRes2;
   private PatchLineComment plc1;
   private PatchLineComment plc2;
   private PatchLineComment plc3;
+  private IdentifiedUser changeOwner;
 
   @Before
   public void setUp() throws Exception {
@@ -78,59 +128,137 @@
     final AccountInfo.Loader.Factory alf =
         createMock(AccountInfo.Loader.Factory.class);
     final ReviewDb db = createMock(ReviewDb.class);
+    final FakeAccountCache accountCache = new FakeAccountCache();
+    final PersonIdent serverIdent = new PersonIdent(
+        "Gerrit Server", "noreply@gerrit.com", TimeUtil.nowTs(), TZ);
+    project = new Project.NameKey("test-project");
+    repoManager = new InMemoryRepositoryManager();
+
+    @SuppressWarnings("unused")
+    InMemoryRepository repo = repoManager.createRepository(project);
 
     AbstractModule mod = new AbstractModule() {
       @Override
       protected void configure() {
         bind(viewsType).toInstance(views);
         bind(AccountInfo.Loader.Factory.class).toInstance(alf);
-        bind(ReviewDb.class).toInstance(db);
-      }};
+        bind(ReviewDb.class).toProvider(Providers.<ReviewDb>of(db));
+        bind(Config.class).annotatedWith(GerritServerConfig.class).toInstance(config);
+        bind(ProjectCache.class).toProvider(Providers.<ProjectCache> of(null));
+        install(new GitModule());
+        bind(GitRepositoryManager.class).toInstance(repoManager);
+        bind(CapabilityControl.Factory.class)
+            .toProvider(Providers.<CapabilityControl.Factory> of(null));
+        bind(String.class).annotatedWith(AnonymousCowardName.class)
+            .toProvider(AnonymousCowardNameProvider.class);
+        bind(String.class).annotatedWith(CanonicalWebUrl.class)
+            .toInstance("http://localhost:8080/");
+        bind(GroupBackend.class).to(SystemGroupBackend.class).in(SINGLETON);
+        bind(AccountCache.class).toInstance(accountCache);
+        bind(GitReferenceUpdated.class)
+            .toInstance(GitReferenceUpdated.DISABLED);
+        bind(PersonIdent.class).annotatedWith(GerritPersonIdent.class)
+          .toInstance(serverIdent);
+      }
+    };
 
-    Account.Id account1 = new Account.Id(1);
-    Account.Id account2 = new Account.Id(2);
+    injector = Guice.createInjector(mod);
+
+    NotesMigration migration = injector.getInstance(NotesMigration.class);
+    plcUtil = new PatchLineCommentsUtil(migration);
+
+    Account co = new Account(new Account.Id(1), TimeUtil.nowTs());
+    co.setFullName("Change Owner");
+    co.setPreferredEmail("change@owner.com");
+    accountCache.put(co);
+    Account.Id ownerId = co.getId();
+
+    Account ou = new Account(new Account.Id(2), TimeUtil.nowTs());
+    ou.setFullName("Other Account");
+    ou.setPreferredEmail("other@account.com");
+    accountCache.put(ou);
+    Account.Id otherUserId = ou.getId();
+
+    IdentifiedUser.GenericFactory userFactory =
+        injector.getInstance(IdentifiedUser.GenericFactory.class);
+    changeOwner = userFactory.create(ownerId);
+    IdentifiedUser otherUser = userFactory.create(otherUserId);
+
     AccountInfo.Loader accountLoader = createMock(AccountInfo.Loader.class);
     accountLoader.fill();
     expectLastCall().anyTimes();
-    expect(accountLoader.get(account1))
-        .andReturn(new AccountInfo(account1)).anyTimes();
-    expect(accountLoader.get(account2))
-        .andReturn(new AccountInfo(account2)).anyTimes();
+    expect(accountLoader.get(ownerId))
+        .andReturn(new AccountInfo(ownerId)).anyTimes();
+    expect(accountLoader.get(otherUserId))
+        .andReturn(new AccountInfo(otherUserId)).anyTimes();
     expect(alf.create(true)).andReturn(accountLoader).anyTimes();
     replay(accountLoader, alf);
 
-    revRes1 = createMock(RevisionResource.class);
-    revRes2 = createMock(RevisionResource.class);
-
     PatchLineCommentAccess plca = createMock(PatchLineCommentAccess.class);
     expect(db.patchComments()).andReturn(plca).anyTimes();
 
-    Change.Id changeId = new Change.Id(123);
-    PatchSet.Id psId1 = new PatchSet.Id(changeId, 1);
+    Change change = newChange();
+    PatchSet.Id psId1 = new PatchSet.Id(change.getId(), 1);
     PatchSet ps1 = new PatchSet(psId1);
-    expect(revRes1.getPatchSet()).andReturn(ps1).anyTimes();
-    PatchSet.Id psId2 = new PatchSet.Id(changeId, 2);
+    PatchSet.Id psId2 = new PatchSet.Id(change.getId(), 2);
     PatchSet ps2 = new PatchSet(psId2);
-    expect(revRes2.getPatchSet()).andReturn(ps2);
 
     long timeBase = TimeUtil.nowMs();
     plc1 = newPatchLineComment(psId1, "Comment1", null,
-        "FileOne.txt", Side.REVISION, 1, account1, timeBase,
+        "FileOne.txt", Side.REVISION, 3, ownerId, timeBase,
         "First Comment", new CommentRange(1, 2, 3, 4));
+    plc1.setRevId(new RevId("ABCDABCDABCDABCDABCDABCDABCDABCDABCDABCD"));
     plc2 = newPatchLineComment(psId1, "Comment2", "Comment1",
-        "FileOne.txt", Side.REVISION, 1, account2, timeBase + 1000,
+        "FileOne.txt", Side.REVISION, 3, otherUserId, timeBase + 1000,
         "Reply to First Comment",  new CommentRange(1, 2, 3, 4));
+    plc2.setRevId(new RevId("ABCDABCDABCDABCDABCDABCDABCDABCDABCDABCD"));
     plc3 = newPatchLineComment(psId1, "Comment3", "Comment1",
-        "FileOne.txt", Side.PARENT, 1, account1, timeBase + 2000,
+        "FileOne.txt", Side.PARENT, 3, ownerId, timeBase + 2000,
         "First Parent Comment",  new CommentRange(1, 2, 3, 4));
+    plc3.setRevId(new RevId("CDEFCDEFCDEFCDEFCDEFCDEFCDEFCDEFCDEFCDEF"));
+
+    List<PatchLineComment> commentsByOwner = Lists.newArrayList();
+    commentsByOwner.add(plc1);
+    commentsByOwner.add(plc3);
+    List<PatchLineComment> commentsByReviewer = Lists.newArrayList();
+    commentsByReviewer.add(plc2);
+
+    plca.upsert(commentsByOwner);
+    expectLastCall().anyTimes();
+    plca.upsert(commentsByReviewer);
+    expectLastCall().anyTimes();
 
     expect(plca.publishedByPatchSet(psId1))
         .andAnswer(results(plc1, plc2, plc3)).anyTimes();
     expect(plca.publishedByPatchSet(psId2))
         .andAnswer(results()).anyTimes();
+    replay(db, plca);
 
-    replay(db, revRes1, revRes2, plca);
-    injector = Guice.createInjector(mod);
+    ChangeUpdate update = newUpdate(change, changeOwner);
+    update.setPatchSetId(psId1);
+    plcUtil.addPublishedComments(db, update, commentsByOwner);
+    update.commit();
+
+    update = newUpdate(change, otherUser);
+    update.setPatchSetId(psId1);
+    plcUtil.addPublishedComments(db, update, commentsByReviewer);
+    update.commit();
+
+    ChangeControl ctl = stubChangeControl(change);
+    revRes1 = new RevisionResource(new ChangeResource(ctl), ps1);
+    revRes2 = new RevisionResource(new ChangeResource(ctl), ps2);
+  }
+
+  private ChangeControl stubChangeControl(Change c) throws OrmException {
+    return TestChanges.stubChangeControl(repoManager, c, changeOwner);
+  }
+
+  private Change newChange() {
+    return TestChanges.newChange(project, changeOwner);
+  }
+
+  private ChangeUpdate newUpdate(Change c, final IdentifiedUser user) throws Exception {
+    return TestChanges.newUpdate(injector, repoManager, c, user);
   }
 
   @Test
@@ -158,7 +286,7 @@
     return new IAnswer<ResultSet<PatchLineComment>>() {
       @Override
       public ResultSet<PatchLineComment> answer() throws Throwable {
-        return new ListResultSet<PatchLineComment>(Lists.newArrayList(comments));
+        return new ListResultSet<>(Lists.newArrayList(comments));
       }};
   }
 
@@ -205,14 +333,14 @@
   private static void assertComment(PatchLineComment plc, CommentInfo ci) {
     assertEquals(plc.getKey().get(), ci.id);
     assertEquals(plc.getParentUuid(), ci.inReplyTo);
-    assertEquals("gerritcodereview#comment", ci.kind);
     assertEquals(plc.getMessage(), ci.message);
     assertNotNull(ci.author);
     assertEquals(plc.getAuthor(), ci.author._id);
     assertEquals(plc.getLine(), (int) ci.line);
     assertEquals(plc.getSide() == 0 ? Side.PARENT : Side.REVISION,
         Objects.firstNonNull(ci.side, Side.REVISION));
-    assertEquals(plc.getWrittenOn(), ci.updated);
+    assertEquals(TimeUtil.roundTimestampToSecond(plc.getWrittenOn()),
+        TimeUtil.roundTimestampToSecond(ci.updated));
     assertEquals(plc.getRange(), ci.range);
   }
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/change/IncludedInResolverTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/change/IncludedInResolverTest.java
index 9c45267..1d0626c 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/change/IncludedInResolverTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/change/IncludedInResolverTest.java
@@ -58,8 +58,8 @@
   private RevCommit commit_v1_3;
   private RevCommit commit_v2_5;
 
-  private List<String> expTags = new ArrayList<String>();
-  private List<String> expBranches = new ArrayList<String>();
+  private List<String> expTags = new ArrayList<>();
+  private List<String> expBranches = new ArrayList<>();
 
   private RevWalk revWalk;
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/config/ScheduleConfigTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/config/ScheduleConfigTest.java
new file mode 100644
index 0000000..e6e15eb
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/config/ScheduleConfigTest.java
@@ -0,0 +1,104 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import static java.util.concurrent.TimeUnit.DAYS;
+import static java.util.concurrent.TimeUnit.HOURS;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+import org.joda.time.DateTime;
+import org.junit.Test;
+
+import java.text.MessageFormat;
+import java.util.concurrent.TimeUnit;
+
+public class ScheduleConfigTest {
+
+  // Friday June 13, 2014 10:00 UTC
+  private static final DateTime NOW = DateTime.parse("2014-06-13T10:00:00-00:00");
+
+  @Test
+  public void testInitialDelay() throws Exception {
+    assertEquals(ms(1, HOURS), initialDelay("11:00", "1h"));
+    assertEquals(ms(30, MINUTES), initialDelay("05:30", "1h"));
+    assertEquals(ms(30, MINUTES), initialDelay("09:30", "1h"));
+    assertEquals(ms(30, MINUTES), initialDelay("13:30", "1h"));
+    assertEquals(ms(59, MINUTES), initialDelay("13:59", "1h"));
+
+    assertEquals(ms(1, HOURS), initialDelay("11:00", "1d"));
+    assertEquals(ms(19, HOURS) + ms(30, MINUTES), initialDelay("05:30", "1d"));
+
+    assertEquals(ms(1, HOURS), initialDelay("11:00", "1w"));
+    assertEquals(ms(7, DAYS) - ms(4, HOURS) - ms(30, MINUTES),
+        initialDelay("05:30", "1w"));
+
+    assertEquals(ms(3, DAYS) + ms(1, HOURS), initialDelay("Mon 11:00", "1w"));
+    assertEquals(ms(1, HOURS), initialDelay("Fri 11:00", "1w"));
+
+    assertEquals(ms(1, HOURS), initialDelay("Mon 11:00", "1d"));
+    assertEquals(ms(23, HOURS), initialDelay("Mon 09:00", "1d"));
+    assertEquals(ms(1, DAYS), initialDelay("Mon 10:00", "1d"));
+    assertEquals(ms(1, DAYS), initialDelay("Mon 10:00", "1d"));
+  }
+
+  @Test
+  public void testCustomKeys() throws ConfigInvalidException {
+    Config rc = readConfig(MessageFormat.format(
+            "[section \"subsection\"]\n{0} = {1}\n{2} = {3}\n",
+            "myStartTime", "01:00", "myInterval", "1h"));
+
+    ScheduleConfig scheduleConfig;
+
+    scheduleConfig = new ScheduleConfig(rc, "section",
+        "subsection", "myInterval", "myStartTime");
+    assertNotEquals(scheduleConfig.getInterval(), ScheduleConfig.MISSING_CONFIG);
+    assertNotEquals(scheduleConfig.getInitialDelay(), ScheduleConfig.MISSING_CONFIG);
+
+    scheduleConfig = new ScheduleConfig(rc, "section",
+        "subsection", "nonExistent", "myStartTime");
+    assertEquals(scheduleConfig.getInterval(), ScheduleConfig.MISSING_CONFIG);
+    assertEquals(scheduleConfig.getInitialDelay(), ScheduleConfig.MISSING_CONFIG);
+  }
+
+  private static long initialDelay(String startTime, String interval)
+      throws ConfigInvalidException {
+    return config(startTime, interval).getInitialDelay();
+  }
+
+  private static ScheduleConfig config(String startTime, String interval)
+      throws ConfigInvalidException {
+    Config rc =
+        readConfig(MessageFormat.format(
+            "[section \"subsection\"]\nstartTime = {0}\ninterval = {1}\n",
+            startTime, interval));
+    return new ScheduleConfig(rc, "section", "subsection", NOW);
+  }
+
+  private static Config readConfig(String dat)
+      throws ConfigInvalidException {
+    Config config = new Config();
+    config.fromText(dat);
+    return config;
+  }
+
+  private static long ms(int cnt, TimeUnit unit) {
+    return MILLISECONDS.convert(cnt, unit);
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/LabelNormalizerTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/LabelNormalizerTest.java
index 5412c67..40088e9 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/git/LabelNormalizerTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/LabelNormalizerTest.java
@@ -16,8 +16,8 @@
 
 import static com.google.gerrit.common.data.Permission.forLabel;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.server.project.Util.allow;
 import static com.google.gerrit.server.project.Util.category;
-import static com.google.gerrit.server.project.Util.grant;
 import static com.google.gerrit.server.project.Util.value;
 import static org.junit.Assert.assertEquals;
 
@@ -133,8 +133,8 @@
   @Test
   public void normalizeByPermission() throws Exception {
     ProjectConfig pc = loadAllProjects();
-    grant(pc, forLabel("Code-Review"), -1, 1, REGISTERED_USERS, "refs/heads/*");
-    grant(pc, forLabel("Verified"), -1, 1, REGISTERED_USERS, "refs/heads/*");
+    allow(pc, forLabel("Code-Review"), -1, 1, REGISTERED_USERS, "refs/heads/*");
+    allow(pc, forLabel("Verified"), -1, 1, REGISTERED_USERS, "refs/heads/*");
     save(pc);
 
     PatchSetApproval cr = psa(userId, "Code-Review", 2);
@@ -149,8 +149,8 @@
   @Test
   public void normalizeByType() throws Exception {
     ProjectConfig pc = loadAllProjects();
-    grant(pc, forLabel("Code-Review"), -5, 5, REGISTERED_USERS, "refs/heads/*");
-    grant(pc, forLabel("Verified"), -5, 5, REGISTERED_USERS, "refs/heads/*");
+    allow(pc, forLabel("Code-Review"), -5, 5, REGISTERED_USERS, "refs/heads/*");
+    allow(pc, forLabel("Verified"), -5, 5, REGISTERED_USERS, "refs/heads/*");
     save(pc);
 
     PatchSetApproval cr = psa(userId, "Code-Review", 5);
@@ -176,7 +176,7 @@
   @Test
   public void explicitZeroVoteOnNonEmptyRangeIsPresent() throws Exception {
     ProjectConfig pc = loadAllProjects();
-    grant(pc, forLabel("Code-Review"), -1, 1, REGISTERED_USERS, "refs/heads/*");
+    allow(pc, forLabel("Code-Review"), -1, 1, REGISTERED_USERS, "refs/heads/*");
     save(pc);
 
     PatchSetApproval cr = psa(userId, "Code-Review", 0);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java
index 1a9f74b..cc4a9e3 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java
@@ -21,9 +21,11 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import com.google.common.collect.Iterables;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -47,6 +49,7 @@
 
 import java.io.IOException;
 import java.util.Collections;
+import java.util.Map;
 
 public class ProjectConfigTest extends LocalDiskRepositoryTestCase {
   private final GroupReference developers = new GroupReference(
@@ -62,7 +65,7 @@
   public void setUp() throws Exception {
     super.setUp();
     db = createBareRepository();
-    util = new TestRepository<Repository>(db);
+    util = new TestRepository<>(db);
   }
 
   @Test
@@ -115,6 +118,60 @@
   }
 
   @Test
+  public void testReadConfigLabelDefaultValue() throws Exception {
+    RevCommit rev = util.commit(util.tree( //
+        util.file("groups", util.blob(group(developers))), //
+        util.file("project.config", util.blob(""//
+            + "[label \"CustomLabel\"]\n" //
+            + "  value = -1 Negative\n" //
+            + "  value =  0 No Score\n" //
+            + "  value =  1 Positive\n")) //
+        ));
+
+    ProjectConfig cfg = read(rev);
+    Map<String, LabelType> labels = cfg.getLabelSections();
+    Short dv = labels.entrySet().iterator().next().getValue().getDefaultValue();
+    assertEquals(0, (int) dv);
+  }
+
+  @Test
+  public void testReadConfigLabelDefaultValueInRange() throws Exception {
+    RevCommit rev = util.commit(util.tree( //
+        util.file("groups", util.blob(group(developers))), //
+        util.file("project.config", util.blob(""//
+            + "[label \"CustomLabel\"]\n" //
+            + "  value = -1 Negative\n" //
+            + "  value =  0 No Score\n" //
+            + "  value =  1 Positive\n" //
+            + "  defaultValue = -1\n")) //
+        ));
+
+    ProjectConfig cfg = read(rev);
+    Map<String, LabelType> labels = cfg.getLabelSections();
+    Short dv = labels.entrySet().iterator().next().getValue().getDefaultValue();
+    assertEquals(-1, (int) dv);
+  }
+
+  @Test
+  public void testReadConfigLabelDefaultValueNotInRange() throws Exception {
+    RevCommit rev = util.commit(util.tree( //
+        util.file("groups", util.blob(group(developers))), //
+        util.file("project.config", util.blob(""//
+            + "[label \"CustomLabel\"]\n" //
+            + "  value = -1 Negative\n" //
+            + "  value =  0 No Score\n" //
+            + "  value =  1 Positive\n" //
+            + "  defaultValue = -2\n")) //
+        ));
+
+    ProjectConfig cfg = read(rev);
+    assertEquals(1, cfg.getValidationErrors().size());
+    assertEquals("project.config: Invalid defaultValue \"-2\" "
+        + "for label \"CustomLabel\"",
+        Iterables.getOnlyElement(cfg.getValidationErrors()).getMessage());
+  }
+
+  @Test
   public void testEditConfig() throws Exception {
     RevCommit rev = util.commit(util.tree( //
         util.file("groups", util.blob(group(developers))), //
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java
index e7a219e..bb109f8 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java
@@ -130,7 +130,7 @@
 
     expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
     final ResultSet<SubmoduleSubscription> emptySubscriptions =
-        new ListResultSet<SubmoduleSubscription>(new ArrayList<SubmoduleSubscription>());
+        new ListResultSet<>(new ArrayList<SubmoduleSubscription>());
     expect(subscriptions.bySubmodule(branchNameKey)).andReturn(
         emptySubscriptions);
 
@@ -296,8 +296,7 @@
         new Branch.NameKey(new Project.NameKey("dest-project"),
             "refs/heads/master");
 
-    final List<SubmoduleSubscription> subscriptionsToInsert =
-        new ArrayList<SubmoduleSubscription>();
+    List<SubmoduleSubscription> subscriptionsToInsert = new ArrayList<>();
     subscriptionsToInsert
         .add(new SubmoduleSubscription(mergedBranch, new Branch.NameKey(
             new Project.NameKey("source-a"), "refs/heads/master"), "source-a"));
@@ -354,8 +353,7 @@
         new Branch.NameKey(new Project.NameKey("dest-project"),
             "refs/heads/master");
 
-    final List<SubmoduleSubscription> subscriptionsToInsert =
-        new ArrayList<SubmoduleSubscription>();
+    List<SubmoduleSubscription> subscriptionsToInsert = new ArrayList<>();
     subscriptionsToInsert
         .add(new SubmoduleSubscription(mergedBranch, new Branch.NameKey(
             new Project.NameKey("source-a"), "refs/heads/master"), "source-a"));
@@ -412,8 +410,7 @@
         new Branch.NameKey(new Project.NameKey("dest-project"),
             "refs/heads/master");
 
-    final List<SubmoduleSubscription> subscriptionsToInsert =
-        new ArrayList<SubmoduleSubscription>();
+    List<SubmoduleSubscription> subscriptionsToInsert = new ArrayList<>();
     subscriptionsToInsert
         .add(new SubmoduleSubscription(mergedBranch, new Branch.NameKey(
             new Project.NameKey("source-a"), "refs/heads/test-a"), "source-a"));
@@ -467,14 +464,12 @@
         new Branch.NameKey(new Project.NameKey("dest-project"),
             "refs/heads/master");
 
-    final List<SubmoduleSubscription> subscriptionsToInsert =
-        new ArrayList<SubmoduleSubscription>();
+    List<SubmoduleSubscription> subscriptionsToInsert = new ArrayList<>();
     subscriptionsToInsert.add(new SubmoduleSubscription(mergedBranch,
         new Branch.NameKey(new Project.NameKey("source"), "refs/heads/master"),
         "source"));
 
-    final List<SubmoduleSubscription> oldOnesToMergedBranch =
-        new ArrayList<SubmoduleSubscription>();
+    List<SubmoduleSubscription> oldOnesToMergedBranch = new ArrayList<>();
     oldOnesToMergedBranch.add(new SubmoduleSubscription(mergedBranch,
         new Branch.NameKey(new Project.NameKey("old-source"),
             "refs/heads/master"), "old-source"));
@@ -534,15 +529,13 @@
         new SubmoduleSubscription(mergedBranch, new Branch.NameKey(new Project.NameKey(
             "old"), "refs/heads/master"), "old");
 
-    final List<SubmoduleSubscription> extractedsubscriptions =
-        new ArrayList<SubmoduleSubscription>();
+    List<SubmoduleSubscription> extractedsubscriptions = new ArrayList<>();
     extractedsubscriptions.add(new SubmoduleSubscription(mergedBranch,
         new Branch.NameKey(new Project.NameKey("new"), "refs/heads/master"),
         "new"));
     extractedsubscriptions.add(old);
 
-    final List<SubmoduleSubscription> oldOnesToMergedBranch =
-        new ArrayList<SubmoduleSubscription>();
+    List<SubmoduleSubscription> oldOnesToMergedBranch = new ArrayList<>();
     oldOnesToMergedBranch.add(old);
 
     doOnlySubscriptionTableOperations(sb.toString(), mergedBranch,
@@ -567,11 +560,8 @@
         new Branch.NameKey(new Project.NameKey("dest-project"),
             "refs/heads/master");
 
-    final List<SubmoduleSubscription> extractedsubscriptions =
-        new ArrayList<SubmoduleSubscription>();
-
-    final List<SubmoduleSubscription> oldOnesToMergedBranch =
-        new ArrayList<SubmoduleSubscription>();
+    List<SubmoduleSubscription> extractedsubscriptions = new ArrayList<>();
+    List<SubmoduleSubscription> oldOnesToMergedBranch = new ArrayList<>();
     oldOnesToMergedBranch
         .add(new SubmoduleSubscription(mergedBranch, new Branch.NameKey(
             new Project.NameKey("source-a"), "refs/heads/master"), "source-a"));
@@ -616,11 +606,10 @@
         new Change.Key(sourceMergeTip.toObjectId().getName()), new Change.Id(1),
         new Account.Id(1), sourceBranchNameKey, TimeUtil.nowTs());
 
-    final Map<Change.Id, CodeReviewCommit> mergedCommits =
-        new HashMap<Change.Id, CodeReviewCommit>();
+    final Map<Change.Id, CodeReviewCommit> mergedCommits = new HashMap<>();
     mergedCommits.put(submittedChange.getId(), codeReviewCommit);
 
-    final List<Change> submitted = new ArrayList<Change>();
+    final List<Change> submitted = new ArrayList<>();
     submitted.add(submittedChange);
 
     final Repository targetRepository = createWorkRepository();
@@ -638,7 +627,7 @@
 
     expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
     final ResultSet<SubmoduleSubscription> subscribers =
-        new ListResultSet<SubmoduleSubscription>(Collections
+        new ListResultSet<>(Collections
             .singletonList(new SubmoduleSubscription(targetBranchNameKey,
                 sourceBranchNameKey, "source-project")));
     expect(subscriptions.bySubmodule(sourceBranchNameKey)).andReturn(
@@ -647,7 +636,7 @@
     expect(repoManager.openRepository(targetBranchNameKey.getParentKey()))
         .andReturn(targetRepository).anyTimes();
 
-    Capture<RefUpdate> ruCapture = new Capture<RefUpdate>();
+    Capture<RefUpdate> ruCapture = new Capture<>();
     gitRefUpdated.fire(eq(targetBranchNameKey.getParentKey()),
         capture(ruCapture));
     changeHooks.doRefUpdatedHook(eq(targetBranchNameKey),
@@ -655,7 +644,7 @@
 
     expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
     final ResultSet<SubmoduleSubscription> emptySubscriptions =
-      new ListResultSet<SubmoduleSubscription>(new ArrayList<SubmoduleSubscription>());
+        new ListResultSet<>(new ArrayList<SubmoduleSubscription>());
     expect(subscriptions.bySubmodule(targetBranchNameKey)).andReturn(
         emptySubscriptions);
 
@@ -722,11 +711,10 @@
         new Change.Key(sourceMergeTip.toObjectId().getName()), new Change.Id(1),
         new Account.Id(1), sourceBranchNameKey, TimeUtil.nowTs());
 
-    final Map<Change.Id, CodeReviewCommit> mergedCommits =
-        new HashMap<Change.Id, CodeReviewCommit>();
+    final Map<Change.Id, CodeReviewCommit> mergedCommits = new HashMap<>();
     mergedCommits.put(submittedChange.getId(), codeReviewCommit);
 
-    final List<Change> submitted = new ArrayList<Change>();
+    final List<Change> submitted = new ArrayList<>();
     submitted.add(submittedChange);
 
     final Repository targetRepository = createWorkRepository();
@@ -744,7 +732,7 @@
 
     expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
     final ResultSet<SubmoduleSubscription> subscribers =
-        new ListResultSet<SubmoduleSubscription>(Collections
+        new ListResultSet<>(Collections
             .singletonList(new SubmoduleSubscription(targetBranchNameKey,
                 sourceBranchNameKey, "source-project")));
     expect(subscriptions.bySubmodule(sourceBranchNameKey)).andReturn(
@@ -753,7 +741,7 @@
     expect(repoManager.openRepository(targetBranchNameKey.getParentKey()))
         .andReturn(targetRepository).anyTimes();
 
-    Capture<RefUpdate> ruCapture = new Capture<RefUpdate>();
+    Capture<RefUpdate> ruCapture = new Capture<>();
     gitRefUpdated.fire(eq(targetBranchNameKey.getParentKey()),
         capture(ruCapture));
     changeHooks.doRefUpdatedHook(eq(targetBranchNameKey),
@@ -811,8 +799,7 @@
         new Branch.NameKey(new Project.NameKey("dest-project"),
             "refs/heads/master");
 
-    final List<SubmoduleSubscription> subscriptionsToInsert =
-        new ArrayList<SubmoduleSubscription>();
+    List<SubmoduleSubscription> subscriptionsToInsert = new ArrayList<>();
     subscriptionsToInsert.add(new SubmoduleSubscription(mergedBranch,
         new Branch.NameKey(new Project.NameKey("source"), sourceBranchName),
         "source"));
@@ -886,10 +873,9 @@
 
     expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
     expect(subscriptions.bySuperProject(mergedBranch)).andReturn(
-        new ListResultSet<SubmoduleSubscription>(previousSubscriptions));
+        new ListResultSet<>(previousSubscriptions));
 
-    SortedSet<Project.NameKey> existingProjects =
-        new TreeSet<Project.NameKey>();
+    SortedSet<Project.NameKey> existingProjects = new TreeSet<>();
 
     for (SubmoduleSubscription extracted : extractedSubscriptions) {
       existingProjects.add(extracted.getSubmodule().getParentKey());
@@ -899,8 +885,7 @@
       expect(repoManager.list()).andReturn(existingProjects);
     }
 
-    final Set<SubmoduleSubscription> alreadySubscribeds =
-        new HashSet<SubmoduleSubscription>();
+    final Set<SubmoduleSubscription> alreadySubscribeds = new HashSet<>();
     for (SubmoduleSubscription s : extractedSubscriptions) {
       if (previousSubscriptions.contains(s)) {
         alreadySubscribeds.add(s);
@@ -908,9 +893,9 @@
     }
 
     final Set<SubmoduleSubscription> subscriptionsToRemove =
-        new HashSet<SubmoduleSubscription>(previousSubscriptions);
+        new HashSet<>(previousSubscriptions);
     final List<SubmoduleSubscription> subscriptionsToInsert =
-        new ArrayList<SubmoduleSubscription>(extractedSubscriptions);
+        new ArrayList<>(extractedSubscriptions);
 
     subscriptionsToRemove.removeAll(subscriptionsToInsert);
     subscriptionsToInsert.removeAll(alreadySubscribeds);
@@ -925,7 +910,7 @@
 
     expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
     expect(subscriptions.bySubmodule(mergedBranch)).andReturn(
-        new ListResultSet<SubmoduleSubscription>(new ArrayList<SubmoduleSubscription>()));
+        new ListResultSet<>(new ArrayList<SubmoduleSubscription>()));
 
     schema.close();
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeIndex.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeIndex.java
index a14bbf1..fc0fb32 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeIndex.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeIndex.java
@@ -25,11 +25,11 @@
 import java.io.IOException;
 
 class FakeIndex implements ChangeIndex {
-  static Schema<ChangeData> V1 = new Schema<ChangeData>(1, false,
+  static Schema<ChangeData> V1 = new Schema<>(1, false,
     ImmutableList.<FieldDef<ChangeData, ?>> of(
       ChangeField.STATUS));
 
-  static Schema<ChangeData> V2 = new Schema<ChangeData>(2, false,
+  static Schema<ChangeData> V2 = new Schema<>(2, false,
     ImmutableList.of(
       ChangeField.STATUS,
       ChangeField.PATH,
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeQueryBuilder.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeQueryBuilder.java
index 2079d3d..50e5764 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeQueryBuilder.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeQueryBuilder.java
@@ -23,7 +23,7 @@
 public class FakeQueryBuilder extends ChangeQueryBuilder {
   FakeQueryBuilder(IndexCollection indexes) {
     super(
-        new FakeQueryBuilder.Definition<ChangeData, FakeQueryBuilder>(
+        new FakeQueryBuilder.Definition<>(
           FakeQueryBuilder.class),
         new ChangeQueryBuilder.Arguments(null, null, null, null, null, null,
           null, null, null, null, null, null, null, null, indexes, null, null,
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
index fae6fad..4b3b206 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -16,26 +16,33 @@
 
 import static com.google.gerrit.server.notedb.ReviewerState.CC;
 import static com.google.gerrit.server.notedb.ReviewerState.REVIEWER;
+import static com.google.gerrit.testutil.TestChanges.incrementPatchSet;
 import static com.google.inject.Scopes.SINGLETON;
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.SECONDS;
-import static org.easymock.EasyMock.expect;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSetMultimap;
 import com.google.common.collect.Iterables;
+import com.google.common.collect.LinkedListMultimap;
 import com.google.common.collect.ListMultimap;
-import com.google.common.collect.Ordering;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Multimap;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.CommentRange;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.PatchSetInfo;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
@@ -52,9 +59,10 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.VersionedMetaData.BatchMetaDataUpdate;
 import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.notedb.CommentsInNotesUtil;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.util.TimeUtil;
+import com.google.gerrit.testutil.TestChanges;
 import com.google.gerrit.testutil.FakeAccountCache;
 import com.google.gerrit.testutil.FakeRealm;
 import com.google.gerrit.testutil.InMemoryRepositoryManager;
@@ -65,11 +73,12 @@
 import com.google.inject.Injector;
 import com.google.inject.util.Providers;
 
-import org.easymock.EasyMock;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.notes.Note;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.joda.time.DateTime;
@@ -80,6 +89,7 @@
 import org.junit.Test;
 
 import java.sql.Timestamp;
+import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
 import java.util.TimeZone;
@@ -213,6 +223,31 @@
   }
 
   @Test
+  public void changeMessageCommitFormatSimple() throws Exception {
+    Change c = newChange();
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    update.setChangeMessage("Just a little code change.\n"
+        + "How about a new line");
+    update.commit();
+    assertEquals("refs/changes/01/1/meta", update.getRefName());
+
+    RevWalk walk = new RevWalk(repo);
+    try {
+      RevCommit commit = walk.parseCommit(update.getRevision());
+      walk.parseBody(commit);
+      assertEquals("Update patch set 1\n"
+          + "\n"
+          + "Just a little code change.\n"
+          + "How about a new line\n"
+          + "\n"
+          + "Patch-set: 1\n",
+          commit.getFullMessage());
+    } finally {
+      walk.close();
+    }
+  }
+
+  @Test
   public void approvalTombstoneCommitFormat() throws Exception {
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, changeOwner);
@@ -608,43 +643,565 @@
     assertEquals((short) 2, psas.get(1).getValue());
   }
 
-  private Change newChange() {
-    Change.Id changeId = new Change.Id(1);
-    Change c = new Change(
-        new Change.Key("Iabcd1234abcd1234abcd1234abcd1234abcd1234"),
-        changeId,
-        changeOwner.getAccount().getId(),
-        new Branch.NameKey(project, "master"),
-        TimeUtil.nowTs());
-    incrementPatchSet(c);
-    return c;
+  @Test
+  public void changeMessageOnePatchSet() throws Exception {
+    Change c = newChange();
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    update.putReviewer(changeOwner.getAccount().getId(), REVIEWER);
+    update.setChangeMessage("Just a little code change.\n");
+    update.commit();
+    PatchSet.Id ps1 = c.currentPatchSetId();
+
+    ChangeNotes notes = newNotes(c);
+    ListMultimap<PatchSet.Id, ChangeMessage> changeMessages =
+        notes.getChangeMessages();
+    assertEquals(1, changeMessages.keySet().size());
+
+    ChangeMessage cm = Iterables.getOnlyElement(changeMessages.get(ps1));
+    assertEquals("Just a little code change.\n",
+        cm.getMessage());
+    assertEquals(changeOwner.getAccount().getId(),
+        cm.getAuthor());
+    assertEquals(ps1, cm.getPatchSetId());
   }
 
-  private ChangeUpdate newUpdate(Change c, final IdentifiedUser user)
+  @Test
+  public void changeMessagesMultiplePatchSets() throws Exception {
+    Change c = newChange();
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    update.putReviewer(changeOwner.getAccount().getId(), REVIEWER);
+    update.setChangeMessage("This is the change message for the first PS.");
+    update.commit();
+    PatchSet.Id ps1 = c.currentPatchSetId();
+
+    incrementPatchSet(c);
+    update = newUpdate(c, changeOwner);
+
+    update.setChangeMessage("This is the change message for the second PS.");
+    update.commit();
+    PatchSet.Id ps2 = c.currentPatchSetId();
+
+    ChangeNotes notes = newNotes(c);
+    ListMultimap<PatchSet.Id, ChangeMessage> changeMessages =
+        notes.getChangeMessages();
+    assertEquals(2, changeMessages.keySet().size());
+
+    ChangeMessage cm1 = Iterables.getOnlyElement(changeMessages.get(ps1));
+    assertEquals("This is the change message for the first PS.",
+        cm1.getMessage());
+    assertEquals(changeOwner.getAccount().getId(),
+        cm1.getAuthor());
+
+    ChangeMessage cm2 = Iterables.getOnlyElement(changeMessages.get(ps2));
+    assertEquals(ps1, cm1.getPatchSetId());
+    assertEquals("This is the change message for the second PS.",
+        cm2.getMessage());
+    assertEquals(changeOwner.getAccount().getId(), cm2.getAuthor());
+    assertEquals(ps2, cm2.getPatchSetId());
+  }
+
+  @Test
+  public void noChangeMessage() throws Exception {
+    Change c = newChange();
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    update.putReviewer(changeOwner.getAccount().getId(), REVIEWER);
+    update.commit();
+
+    RevWalk walk = new RevWalk(repo);
+    try {
+      RevCommit commit = walk.parseCommit(update.getRevision());
+      walk.parseBody(commit);
+      assertEquals("Update patch set 1\n"
+          + "\n"
+          + "Patch-set: 1\n"
+          + "Reviewer: Change Owner <1@gerrit>\n",
+          commit.getFullMessage());
+    } finally {
+      walk.close();
+    }
+
+    ChangeNotes notes = newNotes(c);
+    ListMultimap<PatchSet.Id, ChangeMessage> changeMessages =
+        notes.getChangeMessages();
+    assertEquals(0, changeMessages.keySet().size());
+  }
+
+  @Test
+  public void changeMessageWithTrailingDoubleNewline() throws Exception {
+    Change c = newChange();
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    update.setChangeMessage("Testing trailing double newline\n"
+        + "\n");
+    update.commit();
+    PatchSet.Id ps1 = c.currentPatchSetId();
+
+    RevWalk walk = new RevWalk(repo);
+    try {
+      RevCommit commit = walk.parseCommit(update.getRevision());
+      walk.parseBody(commit);
+      assertEquals("Update patch set 1\n"
+          + "\n"
+          + "Testing trailing double newline\n"
+          + "\n"
+          + "\n"
+          + "\n"
+          + "Patch-set: 1\n",
+          commit.getFullMessage());
+    } finally {
+      walk.close();
+    }
+
+    ChangeNotes notes = newNotes(c);
+    ListMultimap<PatchSet.Id, ChangeMessage> changeMessages =
+        notes.getChangeMessages();
+    assertEquals(1, changeMessages.keySet().size());
+
+    ChangeMessage cm1 = Iterables.getOnlyElement(changeMessages.get(ps1));
+    assertEquals("Testing trailing double newline\n" + "\n", cm1.getMessage());
+    assertEquals(changeOwner.getAccount().getId(), cm1.getAuthor());
+
+  }
+
+  @Test
+  public void changeMessageWithMultipleParagraphs() throws Exception {
+    Change c = newChange();
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    update.setChangeMessage("Testing paragraph 1\n"
+        + "\n"
+        + "Testing paragraph 2\n"
+        + "\n"
+        + "Testing paragraph 3");
+    update.commit();
+    PatchSet.Id ps1 = c.currentPatchSetId();
+
+    RevWalk walk = new RevWalk(repo);
+    try {
+      RevCommit commit = walk.parseCommit(update.getRevision());
+      walk.parseBody(commit);
+      assertEquals("Update patch set 1\n"
+          + "\n"
+          + "Testing paragraph 1\n"
+          + "\n"
+          + "Testing paragraph 2\n"
+          + "\n"
+          + "Testing paragraph 3\n"
+          + "\n"
+          + "Patch-set: 1\n",
+          commit.getFullMessage());
+    } finally {
+      walk.close();
+    }
+
+    ChangeNotes notes = newNotes(c);
+    ListMultimap<PatchSet.Id, ChangeMessage> changeMessages =
+        notes.getChangeMessages();
+    assertEquals(1, changeMessages.keySet().size());
+
+    ChangeMessage cm1 = Iterables.getOnlyElement(changeMessages.get(ps1));
+    assertEquals("Testing paragraph 1\n"
+        + "\n"
+        + "Testing paragraph 2\n"
+        + "\n"
+        + "Testing paragraph 3", cm1.getMessage());
+    assertEquals(changeOwner.getAccount().getId(), cm1.getAuthor());
+  }
+
+  @Test
+  public void changeMessageMultipleInOnePatchSet() throws Exception {
+    Change c = newChange();
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    update.putReviewer(changeOwner.getAccount().getId(), REVIEWER);
+    update.setChangeMessage("First change message.\n");
+    update.commit();
+
+    PatchSet.Id ps1 = c.currentPatchSetId();
+
+    update = newUpdate(c, changeOwner);
+    update.putReviewer(changeOwner.getAccount().getId(), REVIEWER);
+    update.setChangeMessage("Second change message.\n");
+    update.commit();
+
+    ChangeNotes notes = newNotes(c);
+    ListMultimap<PatchSet.Id, ChangeMessage> changeMessages =
+        notes.getChangeMessages();
+    assertEquals(1, changeMessages.keySet().size());
+
+    List<ChangeMessage> cm = changeMessages.get(ps1);
+    assertEquals(2, cm.size());
+    assertEquals("First change message.\n",
+        cm.get(0).getMessage());
+    assertEquals(changeOwner.getAccount().getId(),
+        cm.get(0).getAuthor());
+    assertEquals(ps1, cm.get(0).getPatchSetId());
+    assertEquals("Second change message.\n",
+        cm.get(1).getMessage());
+    assertEquals(changeOwner.getAccount().getId(),
+        cm.get(1).getAuthor());
+    assertEquals(ps1, cm.get(1).getPatchSetId());
+  }
+
+  @Test
+  public void patchLineCommentNotesFormatSide1() throws Exception {
+    Change c = newChange();
+    ChangeUpdate update = newUpdate(c, otherUser);
+    String uuid = "uuid";
+    String message1 = "comment 1";
+    String message2 = "comment 2";
+    String message3 = "comment 3";
+    CommentRange range1 = new CommentRange(1, 1, 2, 1);
+    Timestamp time1 = TimeUtil.nowTs();
+    Timestamp time2 = TimeUtil.nowTs();
+    Timestamp time3 = TimeUtil.nowTs();
+    PatchSet.Id psId = c.currentPatchSetId();
+
+    PatchLineComment comment1 = newPublishedPatchLineComment(psId, "file1",
+        uuid, range1, range1.getEndLine(), otherUser, null, time1, message1,
+        (short) 1, "abcd1234abcd1234abcd1234abcd1234abcd1234");
+    update.setPatchSetId(psId);
+    update.putComment(comment1);
+    update.commit();
+
+    update = newUpdate(c, otherUser);
+    CommentRange range2 = new CommentRange(2, 1, 3, 1);
+    PatchLineComment comment2 = newPublishedPatchLineComment(psId, "file1",
+        uuid, range2, range2.getEndLine(), otherUser, null, time2, message2,
+        (short) 1, "abcd1234abcd1234abcd1234abcd1234abcd1234");
+    update.setPatchSetId(psId);
+    update.putComment(comment2);
+    update.commit();
+
+    update = newUpdate(c, otherUser);
+    CommentRange range3 = new CommentRange(3, 1, 4, 1);
+    PatchLineComment comment3 = newPublishedPatchLineComment(psId, "file2",
+        uuid, range3, range3.getEndLine(), otherUser, null, time3, message3,
+        (short) 1, "abcd1234abcd1234abcd1234abcd1234abcd1234");
+    update.setPatchSetId(psId);
+    update.putComment(comment3);
+    update.commit();
+
+    ChangeNotes notes = newNotes(c);
+
+    RevWalk walk = new RevWalk(repo);
+    ArrayList<Note> notesInTree =
+        Lists.newArrayList(notes.getNoteMap().iterator());
+    Note note = Iterables.getOnlyElement(notesInTree);
+
+    byte[] bytes =
+        walk.getObjectReader().open(
+            note.getData(), Constants.OBJ_BLOB).getBytes();
+    String noteString = new String(bytes, UTF_8);
+    assertEquals("Patch-set: 1\n"
+        + "Revision: abcd1234abcd1234abcd1234abcd1234abcd1234\n"
+        + "File: file1\n"
+        + "\n"
+        + "1:1-2:1\n"
+        + CommentsInNotesUtil.formatTime(serverIdent, time1) + "\n"
+        + "Author: Other Account <2@gerrit>\n"
+        + "UUID: uuid\n"
+        + "Bytes: 9\n"
+        + "comment 1\n"
+        + "\n"
+        + "2:1-3:1\n"
+        + CommentsInNotesUtil.formatTime(serverIdent, time2) + "\n"
+        + "Author: Other Account <2@gerrit>\n"
+        + "UUID: uuid\n"
+        + "Bytes: 9\n"
+        + "comment 2\n"
+        + "\n"
+        + "File: file2\n"
+        + "\n"
+        + "3:1-4:1\n"
+        + CommentsInNotesUtil.formatTime(serverIdent, time3) + "\n"
+        + "Author: Other Account <2@gerrit>\n"
+        + "UUID: uuid\n"
+        + "Bytes: 9\n"
+        + "comment 3\n"
+        + "\n",
+        noteString);
+  }
+
+  @Test
+  public void patchLineCommentNotesFormatSide0() throws Exception {
+    Change c = newChange();
+    ChangeUpdate update = newUpdate(c, otherUser);
+    String uuid = "uuid";
+    String message1 = "comment 1";
+    String message2 = "comment 2";
+    CommentRange range1 = new CommentRange(1, 1, 2, 1);
+    Timestamp time1 = TimeUtil.nowTs();
+    Timestamp time2 = TimeUtil.nowTs();
+    PatchSet.Id psId = c.currentPatchSetId();
+
+    PatchLineComment comment1 = newPublishedPatchLineComment(psId, "file1",
+        uuid, range1, range1.getEndLine(), otherUser, null, time1, message1,
+        (short) 0, "abcd1234abcd1234abcd1234abcd1234abcd1234");
+    update.setPatchSetId(psId);
+    update.putComment(comment1);
+    update.commit();
+
+    update = newUpdate(c, otherUser);
+    CommentRange range2 = new CommentRange(2, 1, 3, 1);
+    PatchLineComment comment2 = newPublishedPatchLineComment(psId, "file1",
+        uuid, range2, range2.getEndLine(), otherUser, null, time2, message2,
+        (short) 0, "abcd1234abcd1234abcd1234abcd1234abcd1234");
+    update.setPatchSetId(psId);
+    update.putComment(comment2);
+    update.commit();
+
+    ChangeNotes notes = newNotes(c);
+
+    RevWalk walk = new RevWalk(repo);
+    ArrayList<Note> notesInTree =
+        Lists.newArrayList(notes.getNoteMap().iterator());
+    Note note = Iterables.getOnlyElement(notesInTree);
+
+    byte[] bytes =
+        walk.getObjectReader().open(
+            note.getData(), Constants.OBJ_BLOB).getBytes();
+    String noteString = new String(bytes, UTF_8);
+    assertEquals("Base-for-patch-set: 1\n"
+        + "Revision: abcd1234abcd1234abcd1234abcd1234abcd1234\n"
+        + "File: file1\n"
+        + "\n"
+        + "1:1-2:1\n"
+        + CommentsInNotesUtil.formatTime(serverIdent, time1) + "\n"
+        + "Author: Other Account <2@gerrit>\n"
+        + "UUID: uuid\n"
+        + "Bytes: 9\n"
+        + "comment 1\n"
+        + "\n"
+        + "2:1-3:1\n"
+        + CommentsInNotesUtil.formatTime(serverIdent, time2) + "\n"
+        + "Author: Other Account <2@gerrit>\n"
+        + "UUID: uuid\n"
+        + "Bytes: 9\n"
+        + "comment 2\n"
+        + "\n",
+        noteString);
+  }
+
+
+  @Test
+  public void patchLineCommentMultipleOnePatchsetOneFileBothSides()
       throws Exception {
-    return injector.createChildInjector(new FactoryModule() {
-      @Override
-      public void configure() {
-        factory(ChangeUpdate.Factory.class);
-        bind(IdentifiedUser.class).toInstance(user);
-      }
-    }).getInstance(ChangeUpdate.Factory.class).create(
-        stubChangeControl(c, user), TimeUtil.nowTs(),
-        Ordering.<String> natural());
+    Change c = newChange();
+    ChangeUpdate update = newUpdate(c, otherUser);
+    String uuid = "uuid";
+    String messageForBase = "comment for base";
+    String messageForPS = "comment for ps";
+    CommentRange range = new CommentRange(1, 1, 2, 1);
+    Timestamp now = TimeUtil.nowTs();
+    PatchSet.Id psId = c.currentPatchSetId();
+
+    PatchLineComment commentForBase =
+        newPublishedPatchLineComment(psId, "filename", uuid,
+        range, range.getEndLine(), otherUser, null, now, messageForBase,
+        (short) 0, "abcd1234abcd1234abcd1234abcd1234abcd1234");
+    update.setPatchSetId(psId);
+    update.putComment(commentForBase);
+    update.commit();
+
+    update = newUpdate(c, otherUser);
+    PatchLineComment commentForPS =
+        newPublishedPatchLineComment(psId, "filename", uuid,
+        range, range.getEndLine(), otherUser, null, now, messageForPS,
+        (short) 1, "abcd4567abcd4567abcd4567abcd4567abcd4567");
+    update.setPatchSetId(psId);
+    update.putComment(commentForPS);
+    update.commit();
+
+    ChangeNotes notes = newNotes(c);
+    Multimap<PatchSet.Id, PatchLineComment> commentsForBase =
+        notes.getBaseComments();
+    Multimap<PatchSet.Id, PatchLineComment> commentsForPS =
+        notes.getPatchSetComments();
+    assertEquals(commentsForBase.size(), 1);
+    assertEquals(commentsForPS.size(), 1);
+
+    assertEquals(commentForBase,
+        Iterables.getOnlyElement(commentsForBase.get(psId)));
+    assertEquals(commentForPS,
+        Iterables.getOnlyElement(commentsForPS.get(psId)));
+  }
+
+  @Test
+  public void patchLineCommentMultipleOnePatchsetOneFile() throws Exception {
+    Change c = newChange();
+    String uuid = "uuid";
+    CommentRange range = new CommentRange(1, 1, 2, 1);
+    PatchSet.Id psId = c.currentPatchSetId();
+    String filename = "filename";
+    short side = (short) 1;
+
+    ChangeUpdate update = newUpdate(c, otherUser);
+    Timestamp timeForComment1 = TimeUtil.nowTs();
+    Timestamp timeForComment2 = TimeUtil.nowTs();
+    PatchLineComment comment1 = newPublishedPatchLineComment(psId, filename,
+        uuid, range, range.getEndLine(), otherUser, null, timeForComment1,
+        "comment 1", side, "abcd1234abcd1234abcd1234abcd1234abcd1234");
+    update.setPatchSetId(psId);
+    update.putComment(comment1);
+    update.commit();
+
+    update = newUpdate(c, otherUser);
+    PatchLineComment comment2 = newPublishedPatchLineComment(psId, filename,
+        uuid, range, range.getEndLine(), otherUser, null, timeForComment2,
+        "comment 2", side, "abcd1234abcd1234abcd1234abcd1234abcd1234");
+    update.setPatchSetId(psId);
+    update.putComment(comment2);
+    update.commit();
+
+    ChangeNotes notes = newNotes(c);
+    Multimap<PatchSet.Id, PatchLineComment> commentsForBase =
+        notes.getBaseComments();
+    Multimap<PatchSet.Id, PatchLineComment> commentsForPS =
+        notes.getPatchSetComments();
+    assertEquals(commentsForBase.size(), 0);
+    assertEquals(commentsForPS.size(), 2);
+
+    ImmutableList<PatchLineComment> commentsForThisPS =
+        (ImmutableList<PatchLineComment>) commentsForPS.get(psId);
+    assertEquals(commentsForThisPS.size(), 2);
+    PatchLineComment commentFromNotes1 = commentsForThisPS.get(0);
+    PatchLineComment commentFromNotes2 = commentsForThisPS.get(1);
+
+    assertEquals(comment1, commentFromNotes1);
+    assertEquals(comment2, commentFromNotes2);
+  }
+
+  @Test
+  public void patchLineCommentMultipleOnePatchsetMultipleFiles()
+      throws Exception {
+    Change c = newChange();
+    String uuid = "uuid";
+    CommentRange range = new CommentRange(1, 1, 2, 1);
+    PatchSet.Id psId = c.currentPatchSetId();
+    String filename1 = "filename1";
+    String filename2 = "filename2";
+    short side = (short) 1;
+
+    ChangeUpdate update = newUpdate(c, otherUser);
+    Timestamp now = TimeUtil.nowTs();
+    PatchLineComment comment1 = newPublishedPatchLineComment(psId, filename1,
+        uuid, range, range.getEndLine(), otherUser, null, now, "comment 1",
+        side, "abcd1234abcd1234abcd1234abcd1234abcd1234");
+    update.setPatchSetId(psId);
+    update.putComment(comment1);
+    update.commit();
+
+    update = newUpdate(c, otherUser);
+    PatchLineComment comment2 = newPublishedPatchLineComment(psId, filename2,
+        uuid, range, range.getEndLine(), otherUser, null, now, "comment 2",
+        side, "abcd1234abcd1234abcd1234abcd1234abcd1234");
+    update.setPatchSetId(psId);
+    update.putComment(comment2);
+    update.commit();
+
+    ChangeNotes notes = newNotes(c);
+    Multimap<PatchSet.Id, PatchLineComment> commentsForBase =
+        notes.getBaseComments();
+    Multimap<PatchSet.Id, PatchLineComment> commentsForPS =
+        notes.getPatchSetComments();
+    assertEquals(commentsForBase.size(), 0);
+    assertEquals(commentsForPS.size(), 2);
+
+    ImmutableList<PatchLineComment> commentsForThisPS =
+        (ImmutableList<PatchLineComment>) commentsForPS.get(psId);
+    assertEquals(commentsForThisPS.size(), 2);
+    PatchLineComment commentFromNotes1 = commentsForThisPS.get(0);
+    PatchLineComment commentFromNotes2 = commentsForThisPS.get(1);
+
+    assertEquals(comment1, commentFromNotes1);
+    assertEquals(comment2, commentFromNotes2);
+  }
+
+  @Test
+  public void patchLineCommentMultiplePatchsets() throws Exception {
+    Change c = newChange();
+    String uuid = "uuid";
+    CommentRange range = new CommentRange(1, 1, 2, 1);
+    PatchSet.Id ps1 = c.currentPatchSetId();
+    String filename = "filename1";
+    short side = (short) 1;
+
+    ChangeUpdate update = newUpdate(c, otherUser);
+    Timestamp now = TimeUtil.nowTs();
+    PatchLineComment comment1 = newPublishedPatchLineComment(ps1, filename,
+        uuid, range, range.getEndLine(), otherUser, null, now, "comment on ps1",
+        side, "abcd1234abcd1234abcd1234abcd1234abcd1234");
+    update.setPatchSetId(ps1);
+    update.putComment(comment1);
+    update.commit();
+
+    incrementPatchSet(c);
+    PatchSet.Id ps2 = c.currentPatchSetId();
+
+    update = newUpdate(c, otherUser);
+    now = TimeUtil.nowTs();
+    PatchLineComment comment2 = newPublishedPatchLineComment(ps2, filename,
+        uuid, range, range.getEndLine(), otherUser, null, now, "comment on ps2",
+        side, "abcd4567abcd4567abcd4567abcd4567abcd4567");
+    update.setPatchSetId(ps2);
+    update.putComment(comment2);
+    update.commit();
+
+    ChangeNotes notes = newNotes(c);
+    LinkedListMultimap<PatchSet.Id, PatchLineComment> commentsForBase =
+        LinkedListMultimap.create(notes.getBaseComments());
+    LinkedListMultimap<PatchSet.Id, PatchLineComment> commentsForPS =
+        LinkedListMultimap.create(notes.getPatchSetComments());
+    assertEquals(commentsForBase.keys().size(), 0);
+    assertEquals(commentsForPS.values().size(), 2);
+
+    List<PatchLineComment> commentsForPS1 = commentsForPS.get(ps1);
+    assertEquals(commentsForPS1.size(), 1);
+    PatchLineComment commentFromPs1 = commentsForPS1.get(0);
+
+    List<PatchLineComment> commentsForPS2 = commentsForPS.get(ps2);
+    assertEquals(commentsForPS2.size(), 1);
+    PatchLineComment commentFromPs2 = commentsForPS2.get(0);
+
+    assertEquals(comment1, commentFromPs1);
+    assertEquals(comment2, commentFromPs2);
+  }
+
+  private Change newChange() {
+    return TestChanges.newChange(project, changeOwner);
+  }
+
+  private PatchLineComment newPublishedPatchLineComment(PatchSet.Id psId,
+      String filename, String UUID, CommentRange range, int line,
+      IdentifiedUser commenter, String parentUUID, Timestamp t,
+      String message, short side, String commitSHA1) {
+    return newPatchLineComment(psId, filename, UUID, range, line, commenter,
+        parentUUID, t, message, side, commitSHA1, Status.PUBLISHED);
+  }
+
+  private PatchLineComment newPatchLineComment(PatchSet.Id psId,
+      String filename, String UUID, CommentRange range, int line,
+      IdentifiedUser commenter, String parentUUID, Timestamp t,
+      String message, short side, String commitSHA1, Status status) {
+    PatchLineComment comment = new PatchLineComment(
+        new PatchLineComment.Key(
+            new Patch.Key(psId, filename), UUID),
+        line, commenter.getAccountId(), parentUUID, t);
+    comment.setSide(side);
+    comment.setMessage(message);
+    comment.setRange(range);
+    comment.setRevId(new RevId(commitSHA1));
+    comment.setStatus(status);
+    return comment;
+  }
+
+  private ChangeUpdate newUpdate(Change c, IdentifiedUser user)
+      throws Exception {
+    return TestChanges.newUpdate(injector, repoManager, c, user);
   }
 
   private ChangeNotes newNotes(Change c) throws OrmException {
     return new ChangeNotes(repoManager, c).load();
   }
 
-  private static void incrementPatchSet(Change change) {
-    PatchSet.Id curr = change.currentPatchSetId();
-    PatchSetInfo ps = new PatchSetInfo(new PatchSet.Id(
-        change.getId(), curr != null ? curr.get() + 1 : 1));
-    ps.setSubject("Change subject");
-    change.setCurrentPatchSet(ps);
-  }
-
   private static Timestamp truncate(Timestamp ts) {
     return new Timestamp((ts.getTime() / 1000) * 1000);
   }
@@ -653,14 +1210,6 @@
     return new Timestamp(c.getCreatedOn().getTime() + millis);
   }
 
-  private ChangeControl stubChangeControl(Change c, IdentifiedUser user) {
-    ChangeControl ctl = EasyMock.createNiceMock(ChangeControl.class);
-    expect(ctl.getChange()).andStubReturn(c);
-    expect(ctl.getCurrentUser()).andStubReturn(user);
-    EasyMock.replay(ctl);
-    return ctl;
-  }
-
   private static SubmitRecord submitRecord(String status,
       String errorMessage, SubmitRecord.Label... labels) {
     SubmitRecord rec = new SubmitRecord();
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 059974f..5478a6c 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
@@ -29,7 +29,6 @@
 import static com.google.gerrit.server.project.Util.block;
 import static com.google.gerrit.server.project.Util.deny;
 import static com.google.gerrit.server.project.Util.doNotInherit;
-import static com.google.gerrit.server.project.Util.grant;
 import static com.google.gerrit.testutil.InMemoryRepositoryManager.newRepository;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -56,6 +55,8 @@
   private final AccountGroup.UUID fixers = new AccountGroup.UUID("test.fixers");
   private Project.NameKey localKey = new Project.NameKey("local");
   private ProjectConfig local;
+  private Project.NameKey parentKey = new Project.NameKey("parent");
+  private ProjectConfig parent;
   private final Util util;
 
   public RefControlTest() {
@@ -64,14 +65,19 @@
 
   @Before
   public void setUp() throws Exception {
+    parent = new ProjectConfig(parentKey);
+    parent.load(newRepository(parentKey));
+    util.add(parent);
+
     local = new ProjectConfig(localKey);
     local.load(newRepository(localKey));
     util.add(local);
+    local.getProject().setParentName(parentKey);
   }
 
   @Test
   public void testOwnerProject() {
-    grant(local, OWNER, ADMIN, "refs/*");
+    allow(local, OWNER, ADMIN, "refs/*");
 
     assertAdminsAreOwnersAndDevsAreNot();
   }
@@ -94,8 +100,8 @@
 
   @Test
   public void testBranchDelegation1() {
-    grant(local, OWNER, ADMIN, "refs/*");
-    grant(local, OWNER, DEVS, "refs/heads/x/*");
+    allow(local, OWNER, ADMIN, "refs/*");
+    allow(local, OWNER, DEVS, "refs/heads/x/*");
 
     ProjectControl uDev = util.user(local, DEVS);
     assertFalse("not owner", uDev.isOwner());
@@ -111,9 +117,9 @@
 
   @Test
   public void testBranchDelegation2() {
-    grant(local, OWNER, ADMIN, "refs/*");
-    grant(local, OWNER, DEVS, "refs/heads/x/*");
-    grant(local, OWNER, fixers, "refs/heads/x/y/*");
+    allow(local, OWNER, ADMIN, "refs/*");
+    allow(local, OWNER, DEVS, "refs/heads/x/*");
+    allow(local, OWNER, fixers, "refs/heads/x/y/*");
     doNotInherit(local, OWNER, "refs/heads/x/y/*");
 
     ProjectControl uDev = util.user(local, DEVS);
@@ -140,9 +146,9 @@
 
   @Test
   public void testInheritRead_SingleBranchDeniesUpload() {
-    grant(util.getParentConfig(), READ, REGISTERED_USERS, "refs/*");
-    grant(util.getParentConfig(), PUSH, REGISTERED_USERS, "refs/for/refs/*");
-    grant(local, READ, REGISTERED_USERS, "refs/heads/foobar");
+    allow(parent, READ, REGISTERED_USERS, "refs/*");
+    allow(parent, PUSH, REGISTERED_USERS, "refs/for/refs/*");
+    allow(local, READ, REGISTERED_USERS, "refs/heads/foobar");
     doNotInherit(local, READ, "refs/heads/foobar");
     doNotInherit(local, PUSH, "refs/for/refs/heads/foobar");
 
@@ -158,9 +164,8 @@
 
   @Test
   public void testBlockPushDrafts() {
-    grant(util.getParentConfig(), PUSH, REGISTERED_USERS, "refs/for/refs/*");
-    grant(util.getParentConfig(), PUSH, ANONYMOUS_USERS, "refs/drafts/*")
-        .setBlock();
+    allow(parent, PUSH, REGISTERED_USERS, "refs/for/refs/*");
+    block(parent, PUSH, ANONYMOUS_USERS, "refs/drafts/*");
 
     ProjectControl u = util.user(local);
     assertTrue("can upload refs/heads/master",
@@ -171,9 +176,8 @@
 
   @Test
   public void testBlockPushDraftsUnblockAdmin() {
-    grant(util.getParentConfig(), PUSH, ANONYMOUS_USERS, "refs/drafts/*")
-        .setBlock();
-    grant(util.getParentConfig(), PUSH, ADMIN, "refs/drafts/*");
+    block(parent, PUSH, ANONYMOUS_USERS, "refs/drafts/*");
+    allow(parent, PUSH, ADMIN, "refs/drafts/*");
 
     assertTrue("push is blocked for anonymous to refs/drafts/master",
         util.user(local).controlForRef("refs/drafts/refs/heads/master")
@@ -185,9 +189,9 @@
 
   @Test
   public void testInheritRead_SingleBranchDoesNotOverrideInherited() {
-    grant(util.getParentConfig(), READ, REGISTERED_USERS, "refs/*");
-    grant(util.getParentConfig(), PUSH, REGISTERED_USERS, "refs/for/refs/*");
-    grant(local, READ, REGISTERED_USERS, "refs/heads/foobar");
+    allow(parent, READ, REGISTERED_USERS, "refs/*");
+    allow(parent, PUSH, REGISTERED_USERS, "refs/for/refs/*");
+    allow(local, READ, REGISTERED_USERS, "refs/heads/foobar");
 
     ProjectControl u = util.user(local);
     assertTrue("can upload", u.canPushToAtLeastOneRef() == Capable.OK);
@@ -201,21 +205,21 @@
 
   @Test
   public void testInheritDuplicateSections() throws Exception {
-    grant(util.getParentConfig(), READ, ADMIN, "refs/*");
-    grant(local, READ, DEVS, "refs/heads/*");
-    local.getProject().setParentName(util.getParentConfig().getProject().getName());
+    allow(parent, READ, ADMIN, "refs/*");
+    allow(local, READ, DEVS, "refs/heads/*");
     assertTrue("a can read", util.user(local, "a", ADMIN).isVisible());
 
-    local = new ProjectConfig(new Project.NameKey("local"));
+    local = new ProjectConfig(localKey);
     local.load(newRepository(localKey));
-    grant(local, READ, DEVS, "refs/*");
+    local.getProject().setParentName(parentKey);
+    allow(local, READ, DEVS, "refs/*");
     assertTrue("d can read", util.user(local, "d", DEVS).isVisible());
   }
 
   @Test
   public void testInheritRead_OverrideWithDeny() {
-    grant(util.getParentConfig(), READ, REGISTERED_USERS, "refs/*");
-    grant(local, READ, REGISTERED_USERS, "refs/*").setDeny();
+    allow(parent, READ, REGISTERED_USERS, "refs/*");
+    deny(local, READ, REGISTERED_USERS, "refs/*");
 
     ProjectControl u = util.user(local);
     assertFalse("can't read", u.isVisible());
@@ -223,8 +227,8 @@
 
   @Test
   public void testInheritRead_AppendWithDenyOfRef() {
-    grant(util.getParentConfig(), READ, REGISTERED_USERS, "refs/*");
-    grant(local, READ, REGISTERED_USERS, "refs/heads/*").setDeny();
+    allow(parent, READ, REGISTERED_USERS, "refs/*");
+    deny(local, READ, REGISTERED_USERS, "refs/heads/*");
 
     ProjectControl u = util.user(local);
     assertTrue("can read", u.isVisible());
@@ -235,9 +239,9 @@
 
   @Test
   public void testInheritRead_OverridesAndDeniesOfRef() {
-    grant(util.getParentConfig(), READ, REGISTERED_USERS, "refs/*");
-    grant(local, READ, REGISTERED_USERS, "refs/*").setDeny();
-    grant(local, READ, REGISTERED_USERS, "refs/heads/*");
+    allow(parent, READ, REGISTERED_USERS, "refs/*");
+    deny(local, READ, REGISTERED_USERS, "refs/*");
+    allow(local, READ, REGISTERED_USERS, "refs/heads/*");
 
     ProjectControl u = util.user(local);
     assertTrue("can read", u.isVisible());
@@ -248,9 +252,9 @@
 
   @Test
   public void testInheritSubmit_OverridesAndDeniesOfRef() {
-    grant(util.getParentConfig(), SUBMIT, REGISTERED_USERS, "refs/*");
-    grant(local, SUBMIT, REGISTERED_USERS, "refs/*").setDeny();
-    grant(local, SUBMIT, REGISTERED_USERS, "refs/heads/*");
+    allow(parent, SUBMIT, REGISTERED_USERS, "refs/*");
+    deny(local, SUBMIT, REGISTERED_USERS, "refs/*");
+    allow(local, SUBMIT, REGISTERED_USERS, "refs/heads/*");
 
     ProjectControl u = util.user(local);
     assertFalse("can't submit", u.controlForRef("refs/foobar").canSubmit());
@@ -260,9 +264,9 @@
 
   @Test
   public void testCannotUploadToAnyRef() {
-    grant(util.getParentConfig(), READ, REGISTERED_USERS, "refs/*");
-    grant(local, READ, DEVS, "refs/heads/*");
-    grant(local, PUSH, DEVS, "refs/for/refs/heads/*");
+    allow(parent, READ, REGISTERED_USERS, "refs/*");
+    allow(local, READ, DEVS, "refs/heads/*");
+    allow(local, PUSH, DEVS, "refs/for/refs/heads/*");
 
     ProjectControl u = util.user(local);
     assertFalse("cannot upload", u.canPushToAtLeastOneRef() == Capable.OK);
@@ -272,14 +276,14 @@
 
   @Test
   public void testUsernamePatternCanUploadToAnyRef() {
-    grant(local, PUSH, REGISTERED_USERS, "refs/heads/users/${username}/*");
+    allow(local, PUSH, REGISTERED_USERS, "refs/heads/users/${username}/*");
     ProjectControl u = util.user(local, "a-registered-user");
     assertTrue("can upload", u.canPushToAtLeastOneRef() == Capable.OK);
   }
 
   @Test
   public void testUsernamePatternNonRegex() {
-    grant(local, READ, DEVS, "refs/sb/${username}/heads/*");
+    allow(local, READ, DEVS, "refs/sb/${username}/heads/*");
 
     ProjectControl u = util.user(local, "u", DEVS), d = util.user(local, "d", DEVS);
     assertFalse("u can't read", u.controlForRef("refs/sb/d/heads/foobar").isVisible());
@@ -288,7 +292,7 @@
 
   @Test
   public void testUsernamePatternWithRegex() {
-    grant(local, READ, DEVS, "^refs/sb/${username}/heads/.*");
+    allow(local, READ, DEVS, "^refs/sb/${username}/heads/.*");
 
     ProjectControl u = util.user(local, "d.v", DEVS), d = util.user(local, "dev", DEVS);
     assertFalse("u can't read", u.controlForRef("refs/sb/dev/heads/foobar").isVisible());
@@ -297,7 +301,7 @@
 
   @Test
   public void testUsernameEmailPatternWithRegex() {
-    grant(local, READ, DEVS, "^refs/sb/${username}/heads/.*");
+    allow(local, READ, DEVS, "^refs/sb/${username}/heads/.*");
 
     ProjectControl u = util.user(local, "d.v@ger-rit.org", DEVS);
     ProjectControl d = util.user(local, "dev@ger-rit.org", DEVS);
@@ -309,8 +313,8 @@
 
   @Test
   public void testSortWithRegex() {
-    grant(local, READ, DEVS, "^refs/heads/.*");
-    grant(util.getParentConfig(), READ, ANONYMOUS_USERS, "^refs/heads/.*-QA-.*");
+    allow(local, READ, DEVS, "^refs/heads/.*");
+    allow(parent, READ, ANONYMOUS_USERS, "^refs/heads/.*-QA-.*");
 
     ProjectControl u = util.user(local, DEVS), d = util.user(local, DEVS);
     assertTrue("u can read", u.controlForRef("refs/heads/foo-QA-bar").isVisible());
@@ -319,17 +323,17 @@
 
   @Test
   public void testBlockRule_ParentBlocksChild() {
-    grant(local, PUSH, DEVS, "refs/tags/*");
-    grant(util.getParentConfig(), PUSH, ANONYMOUS_USERS, "refs/tags/*").setBlock();
+    allow(local, PUSH, DEVS, "refs/tags/*");
+    block(parent, PUSH, ANONYMOUS_USERS, "refs/tags/*");
     ProjectControl u = util.user(local, DEVS);
     assertFalse("u can't update tag", u.controlForRef("refs/tags/V10").canUpdate());
   }
 
   @Test
   public void testBlockRule_ParentBlocksChildEvenIfAlreadyBlockedInChild() {
-    grant(local, PUSH, DEVS, "refs/tags/*");
-    grant(local, PUSH, ANONYMOUS_USERS, "refs/tags/*").setBlock();
-    grant(util.getParentConfig(), PUSH, ANONYMOUS_USERS, "refs/tags/*").setBlock();
+    allow(local, PUSH, DEVS, "refs/tags/*");
+    block(local, PUSH, ANONYMOUS_USERS, "refs/tags/*");
+    block(parent, PUSH, ANONYMOUS_USERS, "refs/tags/*");
 
     ProjectControl u = util.user(local, DEVS);
     assertFalse("u can't update tag", u.controlForRef("refs/tags/V10").canUpdate());
@@ -337,8 +341,8 @@
 
   @Test
   public void testBlockLabelRange_ParentBlocksChild() {
-    grant(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
-    grant(util.getParentConfig(), LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*").setBlock();
+    allow(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
+    block(parent, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
 
     ProjectControl u = util.user(local, DEVS);
 
@@ -351,10 +355,10 @@
 
   @Test
   public void testBlockLabelRange_ParentBlocksChildEvenIfAlreadyBlockedInChild() {
-    grant(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
-    grant(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*").setBlock();
-    grant(util.getParentConfig(), LABEL + "Code-Review", -2, +2, DEVS,
-        "refs/heads/*").setBlock();
+    allow(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
+    block(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
+    block(parent, LABEL + "Code-Review", -2, +2, DEVS,
+        "refs/heads/*");
 
     ProjectControl u = util.user(local, DEVS);
 
@@ -367,9 +371,20 @@
   }
 
   @Test
+  public void testInheritSubmit_AllowInChildDoesntAffectUnblockInParent() {
+    block(parent, SUBMIT, ANONYMOUS_USERS, "refs/heads/*");
+    allow(parent, SUBMIT, REGISTERED_USERS, "refs/heads/*");
+    allow(local, SUBMIT, REGISTERED_USERS, "refs/heads/*");
+
+    ProjectControl u = util.user(local);
+    assertFalse("not blocked from submitting", u.controlForRef(
+        "refs/heads/master").isBlocked(SUBMIT));
+  }
+
+  @Test
   public void testUnblockNoForce() {
-    grant(local, PUSH, ANONYMOUS_USERS, "refs/heads/*").setBlock();
-    grant(local, PUSH, DEVS, "refs/heads/*");
+    block(local, PUSH, ANONYMOUS_USERS, "refs/heads/*");
+    allow(local, PUSH, DEVS, "refs/heads/*");
 
     ProjectControl u = util.user(local, DEVS);
     assertTrue("u can push", u.controlForRef("refs/heads/master").canUpdate());
@@ -377,10 +392,9 @@
 
   @Test
   public void testUnblockForce() {
-    PermissionRule r = grant(local, PUSH, ANONYMOUS_USERS, "refs/heads/*");
-    r.setBlock();
+    PermissionRule r = block(local, PUSH, ANONYMOUS_USERS, "refs/heads/*");
     r.setForce(true);
-    grant(local, PUSH, DEVS, "refs/heads/*").setForce(true);
+    allow(local, PUSH, DEVS, "refs/heads/*").setForce(true);
 
     ProjectControl u = util.user(local, DEVS);
     assertTrue("u can force push", u.controlForRef("refs/heads/master").canForceUpdate());
@@ -388,10 +402,9 @@
 
   @Test
   public void testUnblockForceWithAllowNoForce_NotPossible() {
-    PermissionRule r = grant(local, PUSH, ANONYMOUS_USERS, "refs/heads/*");
-    r.setBlock();
+    PermissionRule r = block(local, PUSH, ANONYMOUS_USERS, "refs/heads/*");
     r.setForce(true);
-    grant(local, PUSH, DEVS, "refs/heads/*");
+    allow(local, PUSH, DEVS, "refs/heads/*");
 
     ProjectControl u = util.user(local, DEVS);
     assertFalse("u can't force push", u.controlForRef("refs/heads/master").canForceUpdate());
@@ -399,8 +412,8 @@
 
   @Test
   public void testUnblockMoreSpecificRef_Fails() {
-    grant(local, PUSH, ANONYMOUS_USERS, "refs/heads/*").setBlock();
-    grant(local, PUSH, DEVS, "refs/heads/master");
+    block(local, PUSH, ANONYMOUS_USERS, "refs/heads/*");
+    allow(local, PUSH, DEVS, "refs/heads/master");
 
     ProjectControl u = util.user(local, DEVS);
     assertFalse("u can't push", u.controlForRef("refs/heads/master").canUpdate());
@@ -408,8 +421,8 @@
 
   @Test
   public void testUnblockLargerScope_Fails() {
-    grant(local, PUSH, ANONYMOUS_USERS, "refs/heads/master").setBlock();
-    grant(local, PUSH, DEVS, "refs/heads/*");
+    block(local, PUSH, ANONYMOUS_USERS, "refs/heads/master");
+    allow(local, PUSH, DEVS, "refs/heads/*");
 
     ProjectControl u = util.user(local, DEVS);
     assertFalse("u can't push", u.controlForRef("refs/heads/master").canUpdate());
@@ -417,8 +430,8 @@
 
   @Test
   public void testUnblockInLocal_Fails() {
-    grant(util.getParentConfig(), PUSH, ANONYMOUS_USERS, "refs/heads/*").setBlock();
-    grant(local, PUSH, fixers, "refs/heads/*");
+    block(parent, PUSH, ANONYMOUS_USERS, "refs/heads/*");
+    allow(local, PUSH, fixers, "refs/heads/*");
 
     ProjectControl f = util.user(local, fixers);
     assertFalse("u can't push", f.controlForRef("refs/heads/master").canUpdate());
@@ -426,9 +439,9 @@
 
   @Test
   public void testUnblockInParentBlockInLocal() {
-    grant(util.getParentConfig(), PUSH, ANONYMOUS_USERS, "refs/heads/*").setBlock();
-    grant(util.getParentConfig(), PUSH, DEVS, "refs/heads/*");
-    grant(local, PUSH, DEVS, "refs/heads/*").setBlock();
+    block(parent, PUSH, ANONYMOUS_USERS, "refs/heads/*");
+    allow(parent, PUSH, DEVS, "refs/heads/*");
+    block(local, PUSH, DEVS, "refs/heads/*");
 
     ProjectControl d = util.user(local, DEVS);
     assertFalse("u can't push", d.controlForRef("refs/heads/master").canUpdate());
@@ -436,8 +449,8 @@
 
   @Test
   public void testUnblockVisibilityByREGISTEREDUsers() {
-    grant(local, READ, ANONYMOUS_USERS, "refs/heads/*").setBlock();
-    grant(local, READ, REGISTERED_USERS, "refs/heads/*");
+    block(local, READ, ANONYMOUS_USERS, "refs/heads/*");
+    allow(local, READ, REGISTERED_USERS, "refs/heads/*");
 
     ProjectControl u = util.user(local, REGISTERED_USERS);
     assertTrue("u can read", u.controlForRef("refs/heads/master").isVisibleByRegisteredUsers());
@@ -445,8 +458,8 @@
 
   @Test
   public void testUnblockInLocalVisibilityByRegisteredUsers_Fails() {
-    grant(util.getParentConfig(), READ, ANONYMOUS_USERS, "refs/heads/*").setBlock();
-    grant(local, READ, REGISTERED_USERS, "refs/heads/*");
+    block(parent, READ, ANONYMOUS_USERS, "refs/heads/*");
+    allow(local, READ, REGISTERED_USERS, "refs/heads/*");
 
     ProjectControl u = util.user(local, REGISTERED_USERS);
     assertFalse("u can't read", u.controlForRef("refs/heads/master").isVisibleByRegisteredUsers());
@@ -454,8 +467,8 @@
 
   @Test
   public void testUnblockForceEditTopicName() {
-    grant(local, EDIT_TOPIC_NAME, ANONYMOUS_USERS, "refs/heads/*").setBlock();
-    grant(local, EDIT_TOPIC_NAME, DEVS, "refs/heads/*").setForce(true);
+    block(local, EDIT_TOPIC_NAME, ANONYMOUS_USERS, "refs/heads/*");
+    allow(local, EDIT_TOPIC_NAME, DEVS, "refs/heads/*").setForce(true);
 
     ProjectControl u = util.user(local, DEVS);
     assertTrue("u can edit topic name", u.controlForRef("refs/heads/master")
@@ -464,9 +477,8 @@
 
   @Test
   public void testUnblockInLocalForceEditTopicName_Fails() {
-    grant(util.getParentConfig(), EDIT_TOPIC_NAME, ANONYMOUS_USERS, "refs/heads/*")
-        .setBlock();
-    grant(local, EDIT_TOPIC_NAME, DEVS, "refs/heads/*").setForce(true);
+    block(parent, EDIT_TOPIC_NAME, ANONYMOUS_USERS, "refs/heads/*");
+    allow(local, EDIT_TOPIC_NAME, DEVS, "refs/heads/*").setForce(true);
 
     ProjectControl u = util.user(local, REGISTERED_USERS);
     assertFalse("u can't edit topic name", u.controlForRef("refs/heads/master")
@@ -475,8 +487,8 @@
 
   @Test
   public void testUnblockRange() {
-    grant(local, LABEL + "Code-Review", -1, +1, ANONYMOUS_USERS, "refs/heads/*").setBlock();
-    grant(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
+    block(local, LABEL + "Code-Review", -1, +1, ANONYMOUS_USERS, "refs/heads/*");
+    allow(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
 
     ProjectControl u = util.user(local, DEVS);
     PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
@@ -486,8 +498,8 @@
 
   @Test
   public void testUnblockRangeOnMoreSpecificRef_Fails() {
-    grant(local, LABEL + "Code-Review", -1, +1, ANONYMOUS_USERS, "refs/heads/*").setBlock();
-    grant(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/master");
+    block(local, LABEL + "Code-Review", -1, +1, ANONYMOUS_USERS, "refs/heads/*");
+    allow(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/master");
 
     ProjectControl u = util.user(local, DEVS);
     PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
@@ -497,8 +509,8 @@
 
   @Test
   public void testUnblockRangeOnLargerScope_Fails() {
-    grant(local, LABEL + "Code-Review", -1, +1, ANONYMOUS_USERS, "refs/heads/master").setBlock();
-    grant(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
+    block(local, LABEL + "Code-Review", -1, +1, ANONYMOUS_USERS, "refs/heads/master");
+    allow(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
 
     ProjectControl u = util.user(local, DEVS);
     PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
@@ -508,9 +520,9 @@
 
   @Test
   public void testUnblockInLocalRange_Fails() {
-    grant(util.getParentConfig(), LABEL + "Code-Review", -1, 1, ANONYMOUS_USERS,
-        "refs/heads/*").setBlock();
-    grant(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
+    block(parent, LABEL + "Code-Review", -1, 1, ANONYMOUS_USERS,
+        "refs/heads/*");
+    allow(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
 
     ProjectControl u = util.user(local, DEVS);
     PermissionRange range =
@@ -520,7 +532,7 @@
   }
 
   public void testUnblockRangeForChangeOwner() {
-    grant(local, LABEL + "Code-Review", -2, +2, CHANGE_OWNER, "refs/heads/*");
+    allow(local, LABEL + "Code-Review", -2, +2, CHANGE_OWNER, "refs/heads/*");
 
     ProjectControl u = util.user(local, DEVS);
     PermissionRange range = u.controlForRef("refs/heads/master")
@@ -530,7 +542,7 @@
   }
 
   public void testUnblockRangeForNotChangeOwner() {
-    grant(local, LABEL + "Code-Review", -2, +2, CHANGE_OWNER, "refs/heads/*");
+    allow(local, LABEL + "Code-Review", -2, +2, CHANGE_OWNER, "refs/heads/*");
 
     ProjectControl u = util.user(local, DEVS);
     PermissionRange range = u.controlForRef("refs/heads/master")
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/project/Util.java b/gerrit-server/src/test/java/com/google/gerrit/server/project/Util.java
index ba2aeca..ac1e0d7 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/project/Util.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/project/Util.java
@@ -20,6 +20,7 @@
 import com.google.common.cache.Cache;
 import com.google.common.cache.CacheBuilder;
 import com.google.common.collect.Lists;
+import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelValue;
@@ -38,6 +39,8 @@
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupMembership;
 import com.google.gerrit.server.account.ListGroupMembership;
+import com.google.gerrit.server.change.ChangeKindCache;
+import com.google.gerrit.server.change.ChangeKindCacheImpl;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.AnonymousCowardNameProvider;
@@ -90,7 +93,7 @@
     return new LabelType(name, Arrays.asList(values));
   }
 
-  static public PermissionRule newRule(ProjectConfig project,
+  public static PermissionRule newRule(ProjectConfig project,
       AccountGroup.UUID groupUUID) {
     GroupReference group = new GroupReference(groupUUID, groupUUID.get());
     group = project.resolve(group);
@@ -98,7 +101,7 @@
     return new PermissionRule(group);
   }
 
-  static public PermissionRule grant(ProjectConfig project,
+  public static PermissionRule allow(ProjectConfig project,
       String permissionName, int min, int max, AccountGroup.UUID group,
       String ref) {
     PermissionRule rule = newRule(project, group);
@@ -107,24 +110,15 @@
     return grant(project, permissionName, rule, ref);
   }
 
-  static public PermissionRule grant(ProjectConfig project,
-      String permissionName, AccountGroup.UUID group, String ref) {
-    return grant(project, permissionName, newRule(project, group), ref);
-  }
-
-  static public void doNotInherit(ProjectConfig project, String permissionName,
+  public static PermissionRule block(ProjectConfig project,
+      String permissionName, int min, int max, AccountGroup.UUID group,
       String ref) {
-    project.getAccessSection(ref, true) //
-        .getPermission(permissionName, true) //
-        .setExclusiveGroup(true);
-  }
-
-  static private PermissionRule grant(ProjectConfig project,
-      String permissionName, PermissionRule rule, String ref) {
-    project.getAccessSection(ref, true) //
-        .getPermission(permissionName, true) //
-        .add(rule);
-    return rule;
+    PermissionRule rule = newRule(project, group);
+    rule.setMin(min);
+    rule.setMax(max);
+    PermissionRule r = grant(project, permissionName, rule, ref);
+    r.setBlock();
+    return r;
   }
 
   public static PermissionRule allow(ProjectConfig project,
@@ -132,6 +126,24 @@
     return grant(project, permissionName, newRule(project, group), ref);
   }
 
+  public static PermissionRule allow(ProjectConfig project,
+      String capabilityName, AccountGroup.UUID group) {
+    PermissionRule rule = newRule(project, group);
+    project.getAccessSection(AccessSection.GLOBAL_CAPABILITIES, true)
+        .getPermission(capabilityName, true)
+        .add(rule);
+    return rule;
+  }
+
+  public static PermissionRule block(ProjectConfig project,
+      String capabilityName, AccountGroup.UUID group) {
+    PermissionRule rule = newRule(project, group);
+    project.getAccessSection(AccessSection.GLOBAL_CAPABILITIES, true)
+        .getPermission(capabilityName, true)
+        .add(rule);
+    return rule;
+  }
+
   public static PermissionRule block(ProjectConfig project,
       String permissionName, AccountGroup.UUID group, String ref) {
     PermissionRule r = grant(project, permissionName, newRule(project, group), ref);
@@ -146,6 +158,21 @@
     return r;
   }
 
+  public static void doNotInherit(ProjectConfig project, String permissionName,
+      String ref) {
+    project.getAccessSection(ref, true) //
+        .getPermission(permissionName, true) //
+        .setExclusiveGroup(true);
+  }
+
+  private static PermissionRule grant(ProjectConfig project,
+      String permissionName, PermissionRule rule, String ref) {
+    project.getAccessSection(ref, true) //
+        .getPermission(permissionName, true) //
+        .add(rule);
+    return rule;
+  }
+
   private final Map<Project.NameKey, ProjectState> all;
   private final ProjectCache projectCache;
   private final CapabilityControl.Factory capabilityControlFactory;
@@ -153,17 +180,19 @@
   private final PermissionCollection.Factory sectionSorter;
   private final GitRepositoryManager repoManager;
 
-  private final AllProjectsName allProjectsName = new AllProjectsName("parent");
-  private final ProjectConfig parent = new ProjectConfig(allProjectsName);
+  private final AllProjectsName allProjectsName =
+      new AllProjectsName("All-Projects");
+  private final ProjectConfig allProjects;
 
   public Util() {
-    all = new HashMap<Project.NameKey, ProjectState>();
+    all = new HashMap<>();
     repoManager = new InMemoryRepositoryManager();
     try {
       Repository repo = repoManager.createRepository(allProjectsName);
-      parent.load(repo);
-      parent.getLabelSections().put(CR.getName(), CR);
-      add(parent);
+      allProjects = new ProjectConfig(new Project.NameKey(allProjectsName.get()));
+      allProjects.load(repo);
+      allProjects.getLabelSections().put(CR.getName(), CR);
+      add(allProjects);
     } catch (IOException | ConfigInvalidException e) {
       throw new RuntimeException(e);
     }
@@ -175,6 +204,11 @@
       }
 
       @Override
+      public ProjectState getAllUsers() {
+        return null;
+      }
+
+      @Override
       public ProjectState get(Project.NameKey projectName) {
         return all.get(projectName);
       }
@@ -236,6 +270,7 @@
             .toProvider(CanonicalWebUrlProvider.class);
         bind(String.class).annotatedWith(AnonymousCowardName.class)
             .toProvider(AnonymousCowardNameProvider.class);
+        bind(ChangeKindCache.class).to(ChangeKindCacheImpl.NoCache.class);
       }
     });
 
@@ -248,10 +283,6 @@
       injector.getInstance(ChangeControl.AssistedFactory.class);
   }
 
-  public ProjectConfig getParentConfig() {
-    return this.parent;
-  }
-
   public void add(ProjectConfig pc) {
     PrologEnvironment.Factory envFactory = null;
     ProjectControl.AssistedFactory projectControlFactory = null;
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 4342684..fb50744 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -28,11 +28,13 @@
 import com.google.common.hash.Hashing;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.projects.ProjectInput;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
@@ -110,6 +112,9 @@
     schemaCreator.create(db);
     userId = accountManager.authenticate(AuthRequest.forUser("user"))
         .getAccountId();
+    Account userAccount = db.accounts().get(userId);
+    userAccount.setPreferredEmail("user@example.com");
+    db.accounts().update(ImmutableList.of(userAccount));
     user = userFactory.create(userId);
 
     requestContext.setContext(new RequestContext() {
@@ -303,11 +308,30 @@
     Change change2 = newChange(repo2, null, null, null, null).insert();
 
     assertTrue(query("project:foo").isEmpty());
+    assertTrue(query("project:repo").isEmpty());
     assertResultEquals(change1, queryOne("project:repo1"));
     assertResultEquals(change2, queryOne("project:repo2"));
   }
 
   @Test
+  public void byProjectPrefix() throws Exception {
+    TestRepository<InMemoryRepository> repo1 = createProject("repo1");
+    TestRepository<InMemoryRepository> repo2 = createProject("repo2");
+    Change change1 = newChange(repo1, null, null, null, null).insert();
+    Change change2 = newChange(repo2, null, null, null, null).insert();
+
+    assertTrue(query("projects:foo").isEmpty());
+    assertResultEquals(change1, queryOne("projects:repo1"));
+    assertResultEquals(change2, queryOne("projects:repo2"));
+
+    List<ChangeInfo> results;
+    results = query("projects:repo");
+    assertEquals(results.toString(), 2, results.size());
+    assertResultEquals(change2, results.get(0));
+    assertResultEquals(change1, results.get(1));
+  }
+
+  @Test
   public void byBranchAndRef() throws Exception {
     TestRepository<InMemoryRepository> repo = createProject("repo");
     Change change1 = newChange(repo, null, null, null, "master").insert();
@@ -332,14 +356,18 @@
     Change change1 = ins1.getChange();
     change1.setTopic("feature1");
     ins1.insert();
+
     ChangeInserter ins2 = newChange(repo, null, null, null, null);
     Change change2 = ins2.getChange();
     change2.setTopic("feature2");
     ins2.insert();
 
+    Change change3 = newChange(repo, null, null, null, null).insert();
+
     assertTrue(query("topic:foo").isEmpty());
     assertResultEquals(change1, queryOne("topic:feature1"));
     assertResultEquals(change2, queryOne("topic:feature2"));
+    assertResultEquals(change3, queryOne("topic:\"\""));
   }
 
   @Test
@@ -690,11 +718,11 @@
 
     ReviewInput input = new ReviewInput();
     input.message = "toplevel";
-    ReviewInput.Comment comment = new ReviewInput.Comment();
+    ReviewInput.CommentInput comment = new ReviewInput.CommentInput();
     comment.line = 1;
     comment.message = "inline";
-    input.comments = ImmutableMap.<String, List<ReviewInput.Comment>> of(
-        "Foo.java", ImmutableList.<ReviewInput.Comment> of(comment));
+    input.comments = ImmutableMap.<String, List<ReviewInput.CommentInput>> of(
+        Patch.COMMIT_MSG, ImmutableList.<ReviewInput.CommentInput> of(comment));
     postReview.apply(new RevisionResource(
         changes.parse(change.getId()), ins.getPatchSet()), input);
 
@@ -785,6 +813,89 @@
     assertResultEquals(change1, results.get(1));
   }
 
+  @Test
+  public void bySize() throws Exception {
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+
+    // added = 3, deleted = 0, delta = 3
+    RevCommit commit1 = repo.parseBody(
+        repo.commit().add("file1", "foo\n\foo\nfoo").create());
+    // added = 0, deleted = 2, delta = 2
+    RevCommit commit2 = repo.parseBody(
+        repo.commit().parent(commit1).add("file1", "foo").create());
+
+    Change change1 = newChange(repo, commit1, null, null, null).insert();
+    Change change2 = newChange(repo, commit2, null, null, null).insert();
+
+    assertTrue(query("added:>4").isEmpty());
+    assertResultEquals(change1, queryOne("added:3"));
+    assertResultEquals(change1, queryOne("added:>2"));
+    assertResultEquals(change1, queryOne("added:>=3"));
+    assertResultEquals(change2, queryOne("added:<1"));
+    assertResultEquals(change2, queryOne("added:<=0"));
+
+    assertTrue(query("deleted:>3").isEmpty());
+    assertResultEquals(change2, queryOne("deleted:2"));
+    assertResultEquals(change2, queryOne("deleted:>1"));
+    assertResultEquals(change2, queryOne("deleted:>=2"));
+    assertResultEquals(change1, queryOne("deleted:<1"));
+    assertResultEquals(change1, queryOne("deleted:<=0"));
+
+    for (String str : Lists.newArrayList("delta", "size")) {
+      assertTrue(query(str + ":<2").isEmpty());
+      assertResultEquals(change1, queryOne(str + ":3"));
+      assertResultEquals(change1, queryOne(str + ":>2"));
+      assertResultEquals(change1, queryOne(str + ":>=3"));
+      assertResultEquals(change2, queryOne(str + ":<3"));
+      assertResultEquals(change2, queryOne(str + ":<=2"));
+    }
+  }
+
+  @Test
+  public void byDefault() throws Exception {
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+
+    Change change1 = newChange(repo, null, null, null, null).insert();
+
+    RevCommit commit2 = repo.parseBody(
+        repo.commit().message("foosubject").create());
+    Change change2 = newChange(repo, commit2, null, null, null).insert();
+
+    RevCommit commit3 = repo.parseBody(
+        repo.commit()
+        .add("Foo.java", "foo contents")
+        .create());
+    Change change3 = newChange(repo, commit3, null, null, null).insert();
+
+    ChangeInserter ins4 = newChange(repo, null, null, null, null);
+    Change change4 = ins4.insert();
+    ReviewInput ri4 = new ReviewInput();
+    ri4.message = "toplevel";
+    ri4.labels = ImmutableMap.<String, Short> of("Code-Review", (short) 1);
+    postReview.apply(new RevisionResource(
+        changes.parse(change4.getId()), ins4.getPatchSet()), ri4);
+
+    ChangeInserter ins5 = newChange(repo, null, null, null, null);
+    Change change5 = ins5.getChange();
+    change5.setTopic("feature5");
+    ins5.insert();
+
+    Change change6 = newChange(repo, null, null, null, "branch6").insert();
+
+    assertResultEquals(change1,
+        queryOne(Integer.toString(change1.getId().get())));
+    assertResultEquals(change2, queryOne("foosubject"));
+    assertResultEquals(change3, queryOne("Foo.java"));
+    assertResultEquals(change4, queryOne("Code-Review+1"));
+    assertResultEquals(change4, queryOne("toplevel"));
+    assertResultEquals(change5, queryOne("feature5"));
+    assertResultEquals(change6, queryOne("branch6"));
+    assertResultEquals(change6, queryOne("refs/heads/branch6"));
+
+    assertEquals(6, query("user@example.com").size());
+    assertEquals(6, query("repo").size());
+  }
+
   protected ChangeInserter newChange(
       TestRepository<InMemoryRepository> repo,
       @Nullable RevCommit commit, @Nullable String key, @Nullable Integer owner,
@@ -833,7 +944,7 @@
   protected TestRepository<InMemoryRepository> createProject(String name)
       throws Exception {
     CreateProject create = projectFactory.create(name);
-    create.apply(TLR, new CreateProject.Input());
+    create.apply(TLR, new ProjectInput());
     return new TestRepository<InMemoryRepository>(
         repoManager.openRepository(new Project.NameKey(name)));
   }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesV7Test.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesV7Test.java
index 948626e..1dcd83b 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesV7Test.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesV7Test.java
@@ -32,6 +32,7 @@
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
+import org.junit.Ignore;
 import org.junit.Test;
 
 import java.util.List;
@@ -43,6 +44,23 @@
     return Guice.createInjector(new InMemoryModule(cfg));
   }
 
+  // Tests for features not supported in V7.
+  @Ignore
+  @Override
+  @Test
+  public void byProjectPrefix() {}
+
+  @Ignore
+  @Override
+  @Test
+  public void byDefault() {}
+
+  @Ignore
+  @Override
+  @Test
+  public void bySize() {}
+  // End tests for features not supported in V7.
+
   @Test
   public void pagination() throws Exception {
     TestRepository<InMemoryRepository> repo = createProject("repo");
@@ -142,4 +160,26 @@
     assertResultEquals(change2, results.get(0));
     assertResultEquals(change1, results.get(1));
   }
+
+  @Override
+  @Test
+  public void byTopic() throws Exception {
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    ChangeInserter ins1 = newChange(repo, null, null, null, null);
+    Change change1 = ins1.getChange();
+    change1.setTopic("feature1");
+    ins1.insert();
+
+    ChangeInserter ins2 = newChange(repo, null, null, null, null);
+    Change change2 = ins2.getChange();
+    change2.setTopic("feature2");
+    ins2.insert();
+
+    newChange(repo, null, null, null, null).insert();
+
+    assertTrue(query("topic:\"\"").isEmpty());
+    assertTrue(query("topic:foo").isEmpty());
+    assertResultEquals(change1, queryOne("topic:feature1"));
+    assertResultEquals(change2, queryOne("topic:feature2"));
+  }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/RegexPathPredicateTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/RegexPathPredicateTest.java
index dd7ac56..8f1fa86 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/RegexPathPredicateTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/RegexPathPredicateTest.java
@@ -82,9 +82,9 @@
     return new RegexPathPredicate(ChangeQueryBuilder.FIELD_PATH, pattern);
   }
 
-  private static ChangeData change(String... files) {
+  private static ChangeData change(String... files) throws OrmException {
     Arrays.sort(files);
-    ChangeData cd = ChangeData.createForTest(new Change.Id(1));
+    ChangeData cd = ChangeData.createForTest(new Change.Id(1), 1);
     cd.setCurrentFilePaths(Arrays.asList(files));
     return cd;
   }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaCreatorTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaCreatorTest.java
index 4756390..996aafa 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaCreatorTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaCreatorTest.java
@@ -128,7 +128,7 @@
     LabelType codeReview = getLabelTypes().byLabel("Code-Review");
     assertNotNull(codeReview);
     assertEquals("Code-Review", codeReview.getName());
-    assertEquals("CR", codeReview.getAbbreviation());
+    assertEquals(0, codeReview.getDefaultValue());
     assertEquals("MaxWithBlock", codeReview.getFunctionName());
     assertTrue(codeReview.isCopyMinScore());
     assertValueRange(codeReview, 2, 1, 0, -1, -2);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java
index e6b7a3f..d8b6048 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.GerritPersonIdentProvider;
 import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.AnonymousCowardNameProvider;
 import com.google.gerrit.server.config.FactoryModule;
@@ -89,6 +90,8 @@
 
         bind(AllProjectsName.class)
             .toInstance(new AllProjectsName("All-Projects"));
+        bind(AllUsersName.class)
+            .toInstance(new AllUsersName("All-Users"));
 
         bind(GitRepositoryManager.class)
             .toInstance(new InMemoryRepositoryManager());
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/util/IdGeneratorTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/util/IdGeneratorTest.java
index 5546410..a2228d8 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/util/IdGeneratorTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/util/IdGeneratorTest.java
@@ -24,7 +24,7 @@
 public class IdGeneratorTest {
   @Test
   public void test1234() {
-    final HashSet<Integer> seen = new HashSet<Integer>();
+    final HashSet<Integer> seen = new HashSet<>();
     for (int i = 0; i < 1 << 16; i++) {
       final int e = IdGenerator.mix(i);
       assertTrue("no duplicates", seen.add(e));
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/util/MostSpecificComparatorTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/util/MostSpecificComparatorTest.java
new file mode 100644
index 0000000..e974f1f
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/util/MostSpecificComparatorTest.java
@@ -0,0 +1,80 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.util;
+
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class MostSpecificComparatorTest {
+
+  private MostSpecificComparator cmp;
+
+  @Test
+  public void shorterDistanceWins() {
+    cmp = new MostSpecificComparator("refs/heads/master");
+    moreSpecificFirst("refs/heads/master", "refs/heads/master2");
+    moreSpecificFirst("refs/heads/master", "refs/heads/maste");
+    moreSpecificFirst("refs/heads/master", "refs/heads/*");
+    moreSpecificFirst("refs/heads/master", "^refs/heads/.*");
+    moreSpecificFirst("refs/heads/master", "^refs/heads/master.*");
+  }
+
+  /**
+   * Assuming two patterns have the same Levenshtein distance,
+   * the pattern which represents a finite language wins over a pattern
+   * which represents an infinite language.
+   */
+  @Test
+  public void finiteWinsOverInfinite() {
+    cmp = new MostSpecificComparator("refs/heads/master");
+    moreSpecificFirst("^refs/heads/......", "refs/heads/*");
+    moreSpecificFirst("^refs/heads/maste.", "^refs/heads/maste.*");
+  }
+
+  /**
+   * Assuming two patterns have the same Levenshtein distance
+   * and are both either finite or infinite the one with the higher
+   * number of state transitions (in an equivalent automaton) wins
+   */
+  @Test
+  public void higherNumberOfTransitionsWins() {
+    cmp = new MostSpecificComparator("refs/heads/x");
+    moreSpecificFirst("^refs/heads/[a-z].*", "refs/heads/*");
+    // Previously there was a bug where having a '1' in a refname would cause a
+    // glob pattern's Levenshtein distance to decrease by 1.  These two
+    // patterns should be a Levenshtein distance of 12 from the both of the
+    // refnames, where previously the 'branch1' refname would be a distance of
+    // 11 from 'refs/heads/abc/*'
+    cmp = new MostSpecificComparator("refs/heads/abc/spam/branch2");
+    moreSpecificFirst("^refs/heads/.*spam.*", "refs/heads/abc/*");
+    cmp = new MostSpecificComparator("refs/heads/abc/spam/branch1");
+    moreSpecificFirst("^refs/heads/.*spam.*", "refs/heads/abc/*");
+  }
+
+  /**
+   * Assuming the same Levenshtein distance, (in)finity and the number
+   * of transitions, the longer pattern wins
+   */
+  @Test
+  public void longerPatternWins() {
+    cmp = new MostSpecificComparator("refs/heads/x");
+    moreSpecificFirst("^refs/heads/[a-z].*", "^refs/heads/..*");
+  }
+
+  private void moreSpecificFirst(String first, String second) {
+    assertTrue(cmp.compare(first, second) < 0);
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java
index edd79241..d87888f 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java
@@ -64,8 +64,7 @@
 
   @Test
   public void testSubmodulesParseWithCorrectSections() throws Exception {
-    final Map<String, SubmoduleSection> sectionsToReturn =
-        new TreeMap<String, SubmoduleSection>();
+    final Map<String, SubmoduleSection> sectionsToReturn = new TreeMap<>();
     sectionsToReturn.put("a", new SubmoduleSection("ssh://localhost/a", "a",
         "."));
     sectionsToReturn.put("b", new SubmoduleSection("ssh://localhost/b", "b",
@@ -77,7 +76,7 @@
     sectionsToReturn.put("e", new SubmoduleSection("ssh://localhost/e.git", "e",
         "."));
 
-    final Map<String, String> reposToBeFound = new HashMap<String, String>();
+    Map<String, String> reposToBeFound = new HashMap<>();
     reposToBeFound.put("a", "a");
     reposToBeFound.put("b", "b");
     reposToBeFound.put("c", "test/c");
@@ -88,8 +87,7 @@
         new Branch.NameKey(new Project.NameKey("super-project"),
             "refs/heads/master");
 
-    final List<SubmoduleSubscription> expectedSubscriptions =
-        new ArrayList<SubmoduleSubscription>();
+    List<SubmoduleSubscription> expectedSubscriptions = new ArrayList<>();
     expectedSubscriptions
         .add(new SubmoduleSubscription(superBranchNameKey, new Branch.NameKey(
             new Project.NameKey("a"), "refs/heads/master"), "a"));
@@ -112,8 +110,7 @@
 
   @Test
   public void testSubmodulesParseWithAnInvalidSection() throws Exception {
-    final Map<String, SubmoduleSection> sectionsToReturn =
-        new TreeMap<String, SubmoduleSection>();
+    final Map<String, SubmoduleSection> sectionsToReturn = new TreeMap<>();
     sectionsToReturn.put("a", new SubmoduleSection("ssh://localhost/a", "a",
         "."));
     // This one is invalid since "b" is not a recognized project
@@ -127,7 +124,7 @@
         "."));
 
     // "b" will not be in this list
-    final Map<String, String> reposToBeFound = new HashMap<String, String>();
+    Map<String, String> reposToBeFound = new HashMap<>();
     reposToBeFound.put("a", "a");
     reposToBeFound.put("c", "test/c");
     reposToBeFound.put("d", "d");
@@ -137,8 +134,7 @@
         new Branch.NameKey(new Project.NameKey("super-project"),
             "refs/heads/master");
 
-    final List<SubmoduleSubscription> expectedSubscriptions =
-        new ArrayList<SubmoduleSubscription>();
+    List<SubmoduleSubscription> expectedSubscriptions = new ArrayList<>();
     expectedSubscriptions
         .add(new SubmoduleSubscription(superBranchNameKey, new Branch.NameKey(
             new Project.NameKey("a"), "refs/heads/master"), "a"));
@@ -158,8 +154,7 @@
 
   @Test
   public void testSubmoduleSectionToOtherServer() throws Exception {
-    Map<String, SubmoduleSection> sectionsToReturn =
-        new HashMap<String, SubmoduleSection>();
+    Map<String, SubmoduleSection> sectionsToReturn = new HashMap<>();
     // The url is not to this server.
     sectionsToReturn.put("a", new SubmoduleSection("ssh://review.source.com/a",
         "a", "."));
@@ -171,8 +166,7 @@
 
   @Test
   public void testProjectNotFound() throws Exception {
-    Map<String, SubmoduleSection> sectionsToReturn =
-        new HashMap<String, SubmoduleSection>();
+    Map<String, SubmoduleSection> sectionsToReturn = new HashMap<>();
     sectionsToReturn.put("a", new SubmoduleSection("ssh://localhost/a", "a",
         "."));
 
@@ -183,8 +177,7 @@
 
   @Test
   public void testProjectWithSlashesNotFound() throws Exception {
-    Map<String, SubmoduleSection> sectionsToReturn =
-        new HashMap<String, SubmoduleSection>();
+    Map<String, SubmoduleSection> sectionsToReturn = new HashMap<>();
     sectionsToReturn.put("project", new SubmoduleSection(
         "ssh://localhost/company/tools/project", "project", "."));
 
@@ -222,12 +215,12 @@
           }
           if (projectNameCandidate.equals(reposToBeFound.get(id))) {
             expect(repoManager.list()).andReturn(
-                new TreeSet<Project.NameKey>(Collections
-                    .singletonList(new Project.NameKey(projectNameCandidate))));
+                new TreeSet<>(Collections.singletonList(
+                    new Project.NameKey(projectNameCandidate))));
             break;
           } else {
             expect(repoManager.list()).andReturn(
-                new TreeSet<Project.NameKey>(Collections.<Project.NameKey> emptyList()));
+                new TreeSet<>(Collections.<Project.NameKey> emptyList()));
           }
         }
       }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/FilesystemLoggingMockingTestCase.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/FilesystemLoggingMockingTestCase.java
new file mode 100644
index 0000000..0ccdae7
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/FilesystemLoggingMockingTestCase.java
@@ -0,0 +1,177 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.testutil;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+
+import org.eclipse.jgit.util.FileUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Collection;
+
+public abstract class FilesystemLoggingMockingTestCase extends LoggingMockingTestCase {
+
+  private Collection<File> toCleanup = Lists.newArrayList();
+
+  /**
+   * Asserts that a given file exists.
+   *
+   * @param file The file to test.
+   */
+  protected void assertExists(File file) {
+    assertTrue("File '" + file.getAbsolutePath() + "' does not exist",
+        file.exists());
+  }
+
+  /**
+   * Asserts that a given file does not exist.
+   *
+   * @param file The file to test.
+   */
+  protected void assertDoesNotExist(File file) {
+    assertFalse("File '" + file.getAbsolutePath() + "' exists", file.exists());
+  }
+
+  /**
+   * Asserts that a given file exists and is a directory.
+   *
+   * @param file The file to test.
+   */
+  protected void assertDirectory(File file) {
+    // Although isDirectory includes checking for existence, we nevertheless
+    // explicitly check for existence, to get more appropriate error messages
+    assertExists(file);
+    assertTrue("File '" + file.getAbsolutePath() + "' is not a directory",
+        file.isDirectory());
+  }
+
+  /**
+   * Asserts that creating a directory from the given file worked
+   *
+   * @param file The directory to create
+   */
+  protected void assertMkdirs(File file) {
+    assertTrue("Could not create directory '" + file.getAbsolutePath() + "'",
+        file.mkdirs());
+  }
+
+  /**
+   * Asserts that creating a directory from the specified file worked
+   *
+   * @param parent The parent of the directory to create
+   * @param name The name of the directoryto create (relative to {@code parent}
+   * @return The created directory
+   */
+  protected File assertMkdirs(File parent, String name) {
+    File file = new File(parent, name);
+    assertMkdirs(file);
+    return file;
+  }
+
+  /**
+   * Asserts that creating a file worked
+   *
+   * @param file The file to create
+   */
+  protected void assertCreateFile(File file) throws IOException {
+    assertTrue("Could not create file '" + file.getAbsolutePath() + "'",
+        file.createNewFile());
+  }
+
+  /**
+   * Asserts that creating a file worked
+   *
+   * @param parent The parent of the file to create
+   * @param name The name of the file to create (relative to {@code parent}
+   * @return The created file
+   */
+  protected File assertCreateFile(File parent, String name) throws IOException {
+    File file = new File(parent, name);
+    assertCreateFile(file);
+    return file;
+  }
+
+  /**
+   * Creates a file in the system's default folder for temporary files.
+   *
+   * The file/directory automatically gets removed during tearDown.
+   *
+   * The name of the created file begins with 'gerrit_test_', and is located
+   * in the system's default folder for temporary files.
+   *
+   * @param suffix Trailing part of the file name.
+   * @return The temporary file.
+   * @throws IOException If a file could not be created.
+   */
+  private File createTempFile(String suffix) throws IOException {
+    String prefix ="gerrit_test_";
+    if (!Strings.isNullOrEmpty(getName())) {
+      prefix += getName() + "_";
+    }
+    File tmp = File.createTempFile(prefix, suffix);
+    toCleanup.add(tmp);
+    return tmp;
+  }
+
+  /**
+   * Creates a file in the system's default folder for temporary files.
+   *
+   * The file/directory automatically gets removed during tearDown.
+   *
+   * The name of the created file begins with 'gerrit_test_', and is located
+   * in the system's default folder for temporary files.
+   *
+   * @return The temporary file.
+   * @throws IOException If a file could not be created.
+   */
+  protected File createTempFile() throws IOException {
+    return createTempFile("");
+  }
+
+  /**
+   * Creates a directory in the system's default folder for temporary files.
+   *
+   * The directory (and all it's contained files/directory) automatically get
+   * removed during tearDown.
+   *
+   * The name of the created directory begins with 'gerrit_test_', and is be
+   * located in the system's default folder for temporary files.
+   *
+   * @return The temporary directory.
+   * @throws IOException If a file could not be created.
+   */
+  protected File createTempDir() throws IOException {
+    File tmp = createTempFile(".dir");
+    if (!tmp.delete()) {
+      throw new IOException("Cannot delete temporary file '" + tmp.getPath()
+          + "'");
+    }
+    tmp.mkdir();
+    return tmp;
+  }
+
+  private void cleanupCreatedFiles() throws IOException {
+    for (File file : toCleanup) {
+      FileUtils.delete(file,  FileUtils.RECURSIVE);
+    }
+  }
+
+  public void tearDown() throws Exception {
+    cleanupCreatedFiles();
+    super.tearDown();
+  }
+}
\ No newline at end of file
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java
index bc344fa..6353904 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java
@@ -91,7 +91,7 @@
 
       // Build the access layer around the connection factory.
       //
-      database = new Database<ReviewDb>(dataSource, ReviewDb.class);
+      database = new Database<>(dataSource, ReviewDb.class);
 
     } catch (SQLException e) {
       throw new OrmException(e);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
index 9e698f7..d96e861 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
@@ -18,6 +18,7 @@
 import static com.google.inject.Scopes.SINGLETON;
 
 import com.google.common.net.InetAddresses;
+import com.google.common.util.concurrent.MoreExecutors;
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.DisabledChangeHooks;
 import com.google.gerrit.reviewdb.client.AuthType;
@@ -29,6 +30,8 @@
 import com.google.gerrit.server.change.MergeabilityChecksExecutorModule;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AllProjectsNameProvider;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.AllUsersNameProvider;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.AnonymousCowardNameProvider;
 import com.google.gerrit.server.config.CanonicalWebUrlModule;
@@ -47,6 +50,7 @@
 import com.google.gerrit.server.index.IndexModule.IndexType;
 import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
 import com.google.gerrit.server.mail.SmtpEmailSender;
+import com.google.gerrit.server.patch.DiffExecutor;
 import com.google.gerrit.server.schema.Current;
 import com.google.gerrit.server.schema.DataSourceType;
 import com.google.gerrit.server.schema.SchemaCreator;
@@ -73,6 +77,7 @@
 import java.lang.reflect.InvocationTargetException;
 import java.net.InetSocketAddress;
 import java.net.SocketAddress;
+import java.util.concurrent.ExecutorService;
 
 public class InMemoryModule extends FactoryModule {
   public static Config newDefaultConfig() {
@@ -134,6 +139,8 @@
       .toProvider(AnonymousCowardNameProvider.class);
     bind(AllProjectsName.class)
         .toProvider(AllProjectsNameProvider.class);
+    bind(AllUsersName.class)
+        .toProvider(AllUsersNameProvider.class);
     bind(GitRepositoryManager.class)
         .to(InMemoryRepositoryManager.class);
     bind(InMemoryRepositoryManager.class).in(SINGLETON);
@@ -153,6 +160,18 @@
         return CanonicalWebUrlProvider.class;
       }
     });
+    //Replacement of DiffExecutorModule to not use thread pool in the tests
+    install(new AbstractModule() {
+      @Override
+      protected void configure() {
+      }
+      @Provides
+      @Singleton
+      @DiffExecutor
+      public ExecutorService createDiffExecutor() {
+        return MoreExecutors.sameThreadExecutor();
+      }
+    });
     install(new DefaultCacheFactory.Module());
     install(new SmtpEmailSender.Module());
     install(new SignedTokenEmailTokenVerifier.Module());
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/LoggingMockingTestCase.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/LoggingMockingTestCase.java
new file mode 100644
index 0000000..218fa0a
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/LoggingMockingTestCase.java
@@ -0,0 +1,134 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.testutil;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.testutil.log.LogUtil;
+
+import org.apache.log4j.LogManager;
+import org.apache.log4j.Logger;
+import org.apache.log4j.spi.LoggingEvent;
+import org.junit.After;
+
+import java.util.Iterator;
+
+/**
+ * Testcase capturing associated logs and allowing to assert on them.
+ *
+ * For a test case SomeNameTest, the log for SomeName gets captured. Assertions
+ * on logs run against the coptured log events from this logger. After the
+ * tests, the logger are set back to their original settings.
+ */
+public abstract class LoggingMockingTestCase extends MockingTestCase {
+  private String loggerName;
+  private LogUtil.LoggerSettings loggerSettings;
+  private java.util.Collection<LoggingEvent> loggedEvents;
+
+  /**
+   * Assert a logged event with a given string.
+   * <p>
+   * If such a event is found, it is removed from the captured logs.
+   *
+   * @param needle The string to look for.
+   */
+  protected final void assertLogMessageContains(String needle) {
+    LoggingEvent hit = null;
+    Iterator<LoggingEvent> iter = loggedEvents.iterator();
+    while (hit == null && iter.hasNext()) {
+      LoggingEvent event = iter.next();
+      if (event.getRenderedMessage().contains(needle)) {
+        hit = event;
+      }
+    }
+    assertNotNull("Could not find log message containing '" + needle + "'",
+        hit);
+    assertTrue("Could not remove log message containing '" + needle + "'",
+        loggedEvents.remove(hit));
+  }
+
+  /**
+   * Assert a logged event whose throwable contains a given string
+   * <p>
+   * If such a event is found, it is removed from the captured logs.
+   *
+   * @param needle The string to look for.
+   */
+  protected final void assertLogThrowableMessageContains(String needle) {
+    LoggingEvent hit = null;
+    Iterator<LoggingEvent> iter = loggedEvents.iterator();
+    while (hit == null && iter.hasNext()) {
+      LoggingEvent event = iter.next();
+      if (event.getThrowableInformation().getThrowable().toString()
+          .contains(needle)) {
+        hit = event;
+      }
+    }
+    assertNotNull("Could not find log message with a Throwable containing '"
+        + needle + "'", hit);
+    assertTrue("Could not remove log message with a Throwable containing '"
+        + needle + "'", loggedEvents.remove(hit));
+  }
+
+  /**
+   * Assert that all logged events have been asserted
+   */
+  // As the PowerMock runner does not pass through runTest, we inject log
+  // verification through @After
+  @After
+  public final void assertNoUnassertedLogEvents() {
+    if (loggedEvents.size() > 0) {
+      LoggingEvent event = loggedEvents.iterator().next();
+      String msg = "Found untreated logged events. First one is:\n";
+      msg += event.getRenderedMessage();
+      if (event.getThrowableInformation() != null) {
+        msg += "\n" + event.getThrowableInformation().getThrowable();
+      }
+      fail(msg);
+    }
+  }
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    loggedEvents = Lists.newArrayList();
+
+    // The logger we're interested is class name without the trailing "Test".
+    // While this is not the most general approach it is sufficient for now,
+    // and we can improve later to allow tests to specify which loggers are
+    // to check.
+    loggerName = this.getClass().getCanonicalName();
+    loggerName = loggerName.substring(0, loggerName.length()-4);
+    loggerSettings = LogUtil.logToCollection(loggerName, loggedEvents);
+  }
+
+  @Override
+  protected void runTest() throws Throwable {
+    super.runTest();
+    // Plain JUnit runner does not pick up @After, so we add it here
+    // explicitly. Note, that we cannot put this into tearDown, as failure
+    // to verify mocks would bail out and might leave open resources from
+    // subclasses open.
+    assertNoUnassertedLogEvents();
+  }
+
+  @Override
+  public void tearDown() throws Exception {
+    if (loggerName != null && loggerSettings != null) {
+      Logger logger = LogManager.getLogger(loggerName);
+      loggerSettings.pushOntoLogger(logger);
+    }
+    super.tearDown();
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/MockingTestCase.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/MockingTestCase.java
new file mode 100644
index 0000000..569a57f
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/MockingTestCase.java
@@ -0,0 +1,155 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.testutil;
+
+import junit.framework.TestCase;
+
+import org.easymock.EasyMock;
+import org.easymock.IMocksControl;
+import org.junit.After;
+import org.junit.runner.RunWith;
+import org.powermock.api.easymock.PowerMock;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+/**
+ * Test case with some support for automatically verifying mocks.
+ *
+ * This test case works transparently with EasyMock and PowerMock.
+ */
+public abstract class MockingTestCase extends TestCase {
+  private Collection<Object> mocks;
+  private Collection<IMocksControl> mockControls;
+  private boolean mocksReplayed;
+  private boolean usePowerMock;
+
+  /**
+   * Create and register a mock control.
+   *
+   * @return The mock control instance.
+   */
+  protected final IMocksControl createMockControl() {
+    IMocksControl mockControl = EasyMock.createControl();
+    assertTrue("Adding mock control failed", mockControls.add(mockControl));
+    return mockControl;
+  }
+
+  /**
+   * Create and register a mock.
+   *
+   * Creates a mock and registers it in the list of created mocks, so it gets
+   * treated automatically upon {@code replay} and {@code verify};
+   * @param toMock The class to create a mock for.
+   * @return The mock instance.
+   */
+  protected final <T> T createMock(Class<T> toMock) {
+    return createMock(toMock, null);
+  }
+
+  /**
+   * Create a mock for a mock control and register a mock.
+   *
+   * Creates a mock and registers it in the list of created mocks, so it gets
+   * treated automatically upon {@code replay} and {@code verify};
+   * @param toMock The class to create a mock for.
+   * @param control The mock control to create the mock on. If null, do not use
+   *    a specific control.
+   * @return The mock instance.
+   */
+  protected final <T> T createMock(Class<T> toMock, IMocksControl control) {
+    assertFalse("Mocks have already been set to replay", mocksReplayed);
+    final T mock;
+    if (control == null) {
+      if (usePowerMock) {
+        mock = PowerMock.createMock(toMock);
+      } else {
+        mock = EasyMock.createMock(toMock);
+      }
+      assertTrue("Adding " + toMock.getName() + " mock failed",
+          mocks.add(mock));
+    } else {
+      mock = control.createMock(toMock);
+    }
+    return mock;
+  }
+
+  /**
+   * Set all registered mocks to replay
+   */
+  protected final void replayMocks() {
+    assertFalse("Mocks have already been set to replay", mocksReplayed);
+    if (usePowerMock) {
+      PowerMock.replayAll();
+    } else {
+      EasyMock.replay(mocks.toArray());
+    }
+    for (IMocksControl mockControl : mockControls) {
+      mockControl.replay();
+    }
+    mocksReplayed = true;
+  }
+
+  /**
+   * Verify all registered mocks
+   *
+   * This method is called automatically at the end of a test. Nevertheless,
+   * it is safe to also call it beforehand, if this better meets the
+   * verification part of a test.
+   */
+  // As the PowerMock runner does not pass through runTest, we inject mock
+  // verification through @After
+  @After
+  public final void verifyMocks() {
+    if (!mocks.isEmpty() || !mockControls.isEmpty()) {
+      assertTrue("Created mocks have not been set to replay. Call replayMocks "
+          + "within the test", mocksReplayed);
+      if (usePowerMock) {
+        PowerMock.verifyAll();
+      } else {
+        EasyMock.verify(mocks.toArray());
+      }
+      for (IMocksControl mockControl : mockControls) {
+        mockControl.verify();
+      }
+    }
+  }
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+
+    usePowerMock = false;
+    RunWith runWith = this.getClass().getAnnotation(RunWith.class);
+    if (runWith != null) {
+      usePowerMock = PowerMockRunner.class.isAssignableFrom(runWith.value());
+    }
+
+    mocks = new ArrayList<>();
+    mockControls = new ArrayList<>();
+    mocksReplayed = false;
+  }
+
+  @Override
+  protected void runTest() throws Throwable {
+    super.runTest();
+    // Plain JUnit runner does not pick up @After, so we add it here
+    // explicitly. Note, that we cannot put this into tearDown, as failure
+    // to verify mocks would bail out and might leave open resources from
+    // subclasses open.
+    verifyMocks();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_75.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/PassThroughKeyUtilEncoder.java
similarity index 69%
copy from gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_75.java
copy to gerrit-server/src/test/java/com/google/gerrit/testutil/PassThroughKeyUtilEncoder.java
index a847f25..e008b78 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_75.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/PassThroughKeyUtilEncoder.java
@@ -12,14 +12,18 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.schema;
+package com.google.gerrit.testutil;
 
-import com.google.inject.Inject;
-import com.google.inject.Provider;
+import com.google.gwtorm.client.KeyUtil.Encoder;
 
-public class Schema_75 extends SchemaVersion {
-  @Inject
-  Schema_75(Provider<Schema_74> prior) {
-    super(prior);
+public class PassThroughKeyUtilEncoder extends Encoder {
+  @Override
+  public String encode(String e) {
+    return e;
+  }
+
+  @Override
+  public String decode(String e) {
+    return e;
   }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/SetMatcher.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/SetMatcher.java
new file mode 100644
index 0000000..f1e7b7b
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/SetMatcher.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.testutil;
+
+import com.google.common.collect.Sets;
+
+import org.easymock.EasyMock;
+import org.easymock.IArgumentMatcher;
+
+import java.util.Set;
+
+/**
+ * Match for Iterables via set equals
+ *
+ * Converts both expected and actual parameter to a set and compares those two
+ * sets via equals to determine whether or not they match.
+ */
+public class SetMatcher<T> implements IArgumentMatcher {
+  public static <S extends Iterable<T>,T> S setEq(S expected) {
+    EasyMock.reportMatcher(new SetMatcher<>(expected));
+    return null;
+  }
+
+  Set<T> expected;
+
+  public SetMatcher(Iterable<T> expected) {
+    this.expected = Sets.newHashSet(expected);
+  }
+
+  @Override
+  public boolean matches(Object actual) {
+    if (actual instanceof Iterable<?>) {
+      Set<?> actualSet = Sets.newHashSet((Iterable<?>)actual);
+      return expected.equals(actualSet);
+    }
+    return false;
+  }
+
+  @Override
+  public void appendTo(StringBuffer buffer) {
+    buffer.append(expected);
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestChanges.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestChanges.java
new file mode 100644
index 0000000..1a0ccaf
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestChanges.java
@@ -0,0 +1,88 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.testutil;
+
+import static org.easymock.EasyMock.expect;
+
+import com.google.common.collect.Ordering;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetInfo;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.FactoryModule;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.util.TimeUtil;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Injector;
+
+import org.easymock.EasyMock;
+
+/**
+ * Utility functions to create and manipulate Change, ChangeUpdate, and
+ * ChangeControl objects for testing.
+ */
+public class TestChanges {
+  public static Change newChange(Project.NameKey project, IdentifiedUser user) {
+    Change.Id changeId = new Change.Id(1);
+    Change c = new Change(
+        new Change.Key("Iabcd1234abcd1234abcd1234abcd1234abcd1234"),
+        changeId,
+        user.getAccount().getId(),
+        new Branch.NameKey(project, "master"),
+        TimeUtil.nowTs());
+    incrementPatchSet(c);
+    return c;
+  }
+
+  public static ChangeUpdate newUpdate(Injector injector,
+      GitRepositoryManager repoManager, Change c, final IdentifiedUser user)
+      throws OrmException {
+    return injector.createChildInjector(new FactoryModule() {
+      @Override
+      public void configure() {
+        factory(ChangeUpdate.Factory.class);
+        bind(IdentifiedUser.class).toInstance(user);
+      }
+    }).getInstance(ChangeUpdate.Factory.class).create(
+        stubChangeControl(repoManager, c, user), TimeUtil.nowTs(),
+        Ordering.<String> natural());
+  }
+
+  public static ChangeControl stubChangeControl(
+      GitRepositoryManager repoManager, Change c, IdentifiedUser user)
+      throws OrmException {
+    ChangeControl ctl = EasyMock.createNiceMock(ChangeControl.class);
+    expect(ctl.getChange()).andStubReturn(c);
+    expect(ctl.getCurrentUser()).andStubReturn(user);
+    ChangeNotes notes = new ChangeNotes(repoManager, c);
+    notes = notes.load();
+    expect(ctl.getNotes()).andStubReturn(notes);
+    EasyMock.replay(ctl);
+    return ctl;
+  }
+
+  public static void incrementPatchSet(Change change) {
+    PatchSet.Id curr = change.currentPatchSetId();
+    PatchSetInfo ps = new PatchSetInfo(new PatchSet.Id(
+        change.getId(), curr != null ? curr.get() + 1 : 1));
+    ps.setSubject("Change subject");
+    change.setCurrentPatchSet(ps);
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/log/CollectionAppender.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/log/CollectionAppender.java
new file mode 100644
index 0000000..05f52c0
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/log/CollectionAppender.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.testutil.log;
+
+import com.google.common.collect.Lists;
+
+import org.apache.log4j.AppenderSkeleton;
+import org.apache.log4j.spi.LoggingEvent;
+
+import java.util.Collection;
+import java.util.LinkedList;
+
+/**
+ * Log4j appender that logs into a list
+ */
+public class CollectionAppender extends AppenderSkeleton {
+  private Collection<LoggingEvent> events;
+
+  public CollectionAppender() {
+    events = new LinkedList<>();
+  }
+
+  public CollectionAppender(Collection<LoggingEvent> events) {
+    this.events = events;
+  }
+
+  @Override
+  public boolean requiresLayout() {
+    return false;
+  }
+
+  @Override
+  protected void append(LoggingEvent event) {
+    if (! events.add(event)) {
+      throw new RuntimeException("Could not append event " + event);
+    }
+  }
+
+  @Override
+  public void close() {
+  }
+
+  public Collection<LoggingEvent> getLoggedEvents() {
+    return Lists.newLinkedList(events);
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/log/LogUtil.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/log/LogUtil.java
new file mode 100644
index 0000000..e3c83ca
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/log/LogUtil.java
@@ -0,0 +1,88 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.testutil.log;
+
+import org.apache.log4j.Appender;
+import org.apache.log4j.LogManager;
+import org.apache.log4j.Logger;
+import org.apache.log4j.spi.LoggingEvent;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.List;
+
+public class LogUtil {
+  /**
+   * Change logger's setting so it only logs to a collection.
+   *
+   * @param logName Name of the logger to modify.
+   * @param collection The collection to log into.
+   * @return The logger's original settings.
+   */
+  public static LoggerSettings logToCollection(String logName,
+      Collection<LoggingEvent> collection) {
+    Logger logger = LogManager.getLogger(logName);
+    LoggerSettings loggerSettings = new LoggerSettings(logger);
+    logger.removeAllAppenders();
+    logger.setAdditivity(false);
+    CollectionAppender listAppender = new CollectionAppender(collection);
+    logger.addAppender(listAppender);
+    return loggerSettings;
+  }
+
+  /**
+   * Capsule for a logger's settings that get mangled by rerouting logging to a collection
+   */
+  public static class LoggerSettings {
+    private final boolean additive;
+    private final List<Appender> appenders;
+
+    /**
+     * Read off logger settings from an instance.
+     *
+     * @param logger The logger to read the settings off from.
+     */
+    private LoggerSettings(Logger logger) {
+      this.additive = logger.getAdditivity();
+
+      Enumeration<?> appenders = logger.getAllAppenders();
+      this.appenders = new ArrayList<>();
+      while (appenders.hasMoreElements()) {
+        Object appender = appenders.nextElement();
+        if (appender instanceof Appender) {
+          this.appenders.add((Appender)appender);
+        } else {
+          throw new RuntimeException("getAllAppenders of " + logger
+              + " contained an object that is not an Appender");
+        }
+      }
+    }
+
+    /**
+     * Pushes this settings back onto a logger.
+     *
+     * @param logger the logger on which to push the settings.
+     */
+    public void pushOntoLogger(Logger logger) {
+      logger.setAdditivity(additive);
+
+      logger.removeAllAppenders();
+      for (Appender appender : appenders) {
+        logger.addAppender(appender);
+      }
+    }
+  }
+}
diff --git a/gerrit-sshd/BUCK b/gerrit-sshd/BUCK
index 4728c31..fad371a 100644
--- a/gerrit-sshd/BUCK
+++ b/gerrit-sshd/BUCK
@@ -1,6 +1,6 @@
 SRCS = glob(['src/main/java/**/*.java'])
 
-java_library2(
+java_library(
   name = 'sshd',
   srcs = SRCS,
   deps = [
@@ -18,6 +18,7 @@
     '//lib:gwtorm',
     '//lib:jsch',
     '//lib/commons:codec',
+    '//lib/commons:collections',
     '//lib/guice:guice',
     '//lib/guice:guice-assistedinject',
     '//lib/guice:guice-servlet',  # SSH should not depend on servlet
@@ -27,7 +28,7 @@
     '//lib/mina:sshd',
     '//lib/jgit:jgit',
   ],
-  compile_deps = [
+  provided_deps = [
     '//lib/bouncycastle:bcprov',
   ],
   visibility = ['PUBLIC'],
@@ -46,6 +47,7 @@
   ),
   deps = [
     ':sshd',
+    '//gerrit-extension-api:api',
     '//gerrit-server:server',
     '//lib:guava',
     '//lib:junit',
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_58.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CachingPublicKeyAuthenticator.java
similarity index 62%
copy from gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_58.java
copy to gerrit-sshd/src/main/java/com/google/gerrit/sshd/CachingPublicKeyAuthenticator.java
index 72b0c8b..0471af8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_58.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CachingPublicKeyAuthenticator.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2011 The Android Open Source Project
+// Copyright (C) 2014 The Android Open 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,14 +12,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.schema;
+package com.google.gerrit.sshd;
 
 import com.google.inject.Inject;
-import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
-public class Schema_58 extends SchemaVersion {
+@Singleton
+public class CachingPublicKeyAuthenticator
+    extends org.apache.sshd.server.auth.CachingPublicKeyAuthenticator {
+
   @Inject
-  Schema_58(Provider<Schema_57> prior) {
-    super(prior);
+  public CachingPublicKeyAuthenticator(DatabasePubKeyAuth authenticator) {
+    super(authenticator);
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
index a2e6542..f394766 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
@@ -246,7 +246,7 @@
 
   /** Split a command line into a string array. */
   static public String[] split(String commandLine) {
-    final List<String> list = new ArrayList<String>();
+    final List<String> list = new ArrayList<>();
     boolean inquote = false;
     boolean inDblQuote = false;
     StringBuilder r = new StringBuilder();
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
index 40e58f2..cc7b637 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.sshd;
 
+import com.google.common.base.Preconditions;
 import com.google.gerrit.reviewdb.client.AccountSshKey;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PeerDaemonUser;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.Inject;
-import com.google.inject.Singleton;
 
 import org.apache.commons.codec.binary.Base64;
 import org.apache.sshd.common.KeyPairProvider;
@@ -48,7 +48,6 @@
 /**
  * Authenticates by public key through {@link AccountSshKey} entities.
  */
-@Singleton
 class DatabasePubKeyAuth implements PublickeyAuthenticator {
   private static final Logger log =
       LoggerFactory.getLogger(DatabasePubKeyAuth.class);
@@ -78,7 +77,7 @@
   }
 
   private static Set<PublicKey> myHostKeys(KeyPairProvider p) {
-    final Set<PublicKey> keys = new HashSet<PublicKey>(2);
+    final Set<PublicKey> keys = new HashSet<>(2);
     addPublicKey(keys, p, KeyPairProvider.SSH_RSA);
     addPublicKey(keys, p, KeyPairProvider.SSH_DSS);
     return keys;
@@ -92,10 +91,11 @@
     }
   }
 
-  public boolean authenticate(String username,
-      final PublicKey suppliedKey, final ServerSession session) {
-    final SshSession sd = session.getAttribute(SshSession.KEY);
-
+  @Override
+  public boolean authenticate(String username, PublicKey suppliedKey,
+      ServerSession session) {
+    SshSession sd = session.getAttribute(SshSession.KEY);
+    Preconditions.checkState(sd.getCurrentUser() == null);
     if (PeerDaemonUser.USER_NAME.equals(username)) {
       if (myHostKeys.contains(suppliedKey)
           || getPeerKeys().contains(suppliedKey)) {
@@ -112,10 +112,10 @@
       username = username.toLowerCase(Locale.US);
     }
 
-    final Iterable<SshKeyCacheEntry> keyList = sshKeyCache.get(username);
-    final SshKeyCacheEntry key = find(keyList, suppliedKey);
+    Iterable<SshKeyCacheEntry> keyList = sshKeyCache.get(username);
+    SshKeyCacheEntry key = find(keyList, suppliedKey);
     if (key == null) {
-      final String err;
+      String err;
       if (keyList == SshKeyCacheImpl.NO_SUCH_USER) {
         err = "user-not-found";
       } else if (keyList == SshKeyCacheImpl.NO_KEYS) {
@@ -133,7 +133,7 @@
     // security check to ensure there aren't two users sharing the same
     // user name on the server.
     //
-    for (final SshKeyCacheEntry otherKey : keyList) {
+    for (SshKeyCacheEntry otherKey : keyList) {
       if (!key.getAccount().equals(otherKey.getAccount())) {
         sd.authenticationError(username, "keys-cross-accounts");
         return false;
@@ -183,7 +183,7 @@
       try {
         final BufferedReader br = new BufferedReader(new FileReader(path));
         try {
-          final Set<PublicKey> keys = new HashSet<PublicKey>();
+          final Set<PublicKey> keys = new HashSet<>();
           String line;
           while ((line = br.readLine()) != null) {
             line = line.trim();
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
index fa5ab53..a7748ff 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
@@ -52,7 +52,7 @@
   private String commandName;
 
   @Argument(index = 1, multiValued = true, metaVar = "ARG")
-  private List<String> args = new ArrayList<String>();
+  private List<String> args = new ArrayList<>();
 
   @Inject
   DispatchCommand(final Provider<CurrentUser> cu,
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/HostKeyProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/HostKeyProvider.java
index f901a77..241f853 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/HostKeyProvider.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/HostKeyProvider.java
@@ -42,7 +42,7 @@
     final File rsaKey = site.ssh_rsa;
     final File dsaKey = site.ssh_dsa;
 
-    final List<String> stdKeys = new ArrayList<String>(2);
+    final List<String> stdKeys = new ArrayList<>(2);
     if (rsaKey.exists()) {
       stdKeys.add(rsaKey.getAbsolutePath());
     }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
index ba54655..7b910b2 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
@@ -45,6 +45,7 @@
 import org.apache.sshd.common.KeyExchange;
 import org.apache.sshd.common.KeyPairProvider;
 import org.apache.sshd.common.NamedFactory;
+import org.apache.sshd.common.RequestHandler;
 import org.apache.sshd.common.Session;
 import org.apache.sshd.common.Signature;
 import org.apache.sshd.common.SshdSocketAddress;
@@ -67,10 +68,11 @@
 import org.apache.sshd.common.future.CloseFuture;
 import org.apache.sshd.common.future.SshFutureListener;
 import org.apache.sshd.common.io.IoAcceptor;
-import org.apache.sshd.common.io.IoServiceFactory;
+import org.apache.sshd.common.io.IoServiceFactoryFactory;
 import org.apache.sshd.common.io.IoSession;
-import org.apache.sshd.common.io.mina.MinaServiceFactory;
+import org.apache.sshd.common.io.mina.MinaServiceFactoryFactory;
 import org.apache.sshd.common.io.mina.MinaSession;
+import org.apache.sshd.common.io.nio2.Nio2ServiceFactoryFactory;
 import org.apache.sshd.common.mac.HMACMD5;
 import org.apache.sshd.common.mac.HMACMD596;
 import org.apache.sshd.common.mac.HMACSHA1;
@@ -79,7 +81,9 @@
 import org.apache.sshd.common.random.JceRandom;
 import org.apache.sshd.common.random.SingletonRandomFactory;
 import org.apache.sshd.common.session.AbstractSession;
+import org.apache.sshd.common.session.ConnectionService;
 import org.apache.sshd.common.signature.SignatureDSA;
+import org.apache.sshd.common.signature.SignatureECDSA;
 import org.apache.sshd.common.signature.SignatureRSA;
 import org.apache.sshd.common.util.Buffer;
 import org.apache.sshd.common.util.SecurityUtils;
@@ -91,6 +95,10 @@
 import org.apache.sshd.server.auth.gss.GSSAuthenticator;
 import org.apache.sshd.server.auth.gss.UserAuthGSS;
 import org.apache.sshd.server.channel.ChannelSession;
+import org.apache.sshd.server.global.CancelTcpipForwardHandler;
+import org.apache.sshd.server.global.KeepAliveHandler;
+import org.apache.sshd.server.global.NoMoreSessionsHandler;
+import org.apache.sshd.server.global.TcpipForwardHandler;
 import org.apache.sshd.server.kex.DHG1;
 import org.apache.sshd.server.kex.DHG14;
 import org.apache.sshd.server.session.SessionFactory;
@@ -181,6 +189,15 @@
         IDLE_TIMEOUT,
         String.valueOf(SECONDS.toMillis(idleTimeoutSeconds)));
 
+    long rekeyTimeLimit = ConfigUtil.getTimeUnit(cfg, "sshd", null,
+        "rekeyTimeLimit", 3600, SECONDS);
+    getProperties().put(
+        REKEY_TIME_LIMIT,
+        String.valueOf(SECONDS.toMillis(rekeyTimeLimit)));
+
+    getProperties().put(REKEY_BYTES_LIMIT,
+        String.valueOf(cfg.getLong("sshd", "rekeyBytesLimit", 1024 * 1024 * 1024 /* 1GB */)));
+
     final int maxConnectionsPerUser =
         cfg.getInt("sshd", "maxConnectionsPerUser", 64);
     if (0 < maxConnectionsPerUser) {
@@ -193,8 +210,13 @@
     final String kerberosPrincipal = cfg.getString(
         "sshd", null, "kerberosPrincipal");
 
-    System.setProperty(IoServiceFactory.class.getName(),
-        MinaServiceFactory.class.getName());
+    SshSessionBackend backend = cfg.getEnum(
+        "sshd", null, "backend", SshSessionBackend.MINA);
+
+    System.setProperty(IoServiceFactoryFactory.class.getName(),
+        backend == SshSessionBackend.MINA
+            ? MinaServiceFactoryFactory.class.getName()
+            : Nio2ServiceFactoryFactory.class.getName());
 
     if (SecurityUtils.isBouncyCastleRegistered()) {
       initProviderBouncyCastle();
@@ -251,6 +273,12 @@
         return new GerritServerSession(server, ioSession);
       }
     });
+    setGlobalRequestHandlers(Arrays.<RequestHandler<ConnectionService>> asList(
+          new KeepAliveHandler(),
+          new NoMoreSessionsHandler(),
+          new TcpipForwardHandler(),
+          new CancelTcpipForwardHandler()
+        ));
 
     hostKeys = computeHostKeys();
   }
@@ -300,8 +328,10 @@
   public synchronized void stop() {
     if (acceptor != null) {
       try {
-        acceptor.dispose();
+        acceptor.close(true).await();
         log.info("Stopped Gerrit SSHD");
+      } catch (InterruptedException e) {
+        log.warn("Exception caught while closing", e);
       } finally {
         acceptor = null;
       }
@@ -322,7 +352,7 @@
     }
 
     final List<PublicKey> keys = myHostKeys();
-    final ArrayList<HostKey> r = new ArrayList<HostKey>();
+    final List<HostKey> r = new ArrayList<>();
     for (final PublicKey pub : keys) {
       final Buffer buf = new Buffer();
       buf.putRawPublicKey(pub);
@@ -341,7 +371,7 @@
 
   private List<PublicKey> myHostKeys() {
     final KeyPairProvider p = getKeyPairProvider();
-    final List<PublicKey> keys = new ArrayList<PublicKey>(2);
+    final List<PublicKey> keys = new ArrayList<>(2);
     addPublicKey(keys, p, KeyPairProvider.SSH_RSA);
     addPublicKey(keys, p, KeyPairProvider.SSH_DSS);
     return keys;
@@ -381,7 +411,7 @@
 
   @SuppressWarnings("unchecked")
   private void initCiphers(final Config cfg) {
-    final List<NamedFactory<Cipher>> a = new LinkedList<NamedFactory<Cipher>>();
+    final List<NamedFactory<Cipher>> a = new LinkedList<>();
     a.add(new AES128CBC.Factory());
     a.add(new TripleDESCBC.Factory());
     a.add(new BlowfishCBC.Factory());
@@ -424,7 +454,7 @@
   @SafeVarargs
   private static <T> List<NamedFactory<T>> filter(final Config cfg,
       final String key, final NamedFactory<T>... avail) {
-    final ArrayList<NamedFactory<T>> def = new ArrayList<NamedFactory<T>>();
+    final ArrayList<NamedFactory<T>> def = new ArrayList<>();
     for (final NamedFactory<T> n : avail) {
       if (n == null) {
         break;
@@ -492,7 +522,11 @@
 
   private void initSignatures() {
     setSignatureFactories(Arrays.<NamedFactory<Signature>> asList(
-        new SignatureDSA.Factory(), new SignatureRSA.Factory()));
+        new SignatureDSA.Factory(),
+        new SignatureRSA.Factory(),
+        new SignatureECDSA.NISTP256Factory(),
+        new SignatureECDSA.NISTP384Factory(),
+        new SignatureECDSA.NISTP521Factory()));
   }
 
   private void initCompression() {
@@ -587,6 +621,11 @@
           @Override
           public SshFile getFile(String file) {
             return null;
+          }
+
+          @Override
+          public FileSystemView getNormalizedView() {
+            return this;
           }};
       }
     });
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
index 0a1f708..2ec67a4 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
@@ -135,7 +135,7 @@
           return NO_SUCH_USER;
         }
 
-        final List<SshKeyCacheEntry> kl = new ArrayList<SshKeyCacheEntry>(4);
+        final List<SshKeyCacheEntry> kl = new ArrayList<>(4);
         for (AccountSshKey k : db.accountSshKeys().byAccount(
             user.getAccountId())) {
           if (k.isValid()) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
index 3e6dc97..82394af 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
@@ -23,27 +23,20 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PeerDaemonUser;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.util.IdGenerator;
-import com.google.gerrit.server.util.LogUtil;
+import com.google.gerrit.server.util.SystemLog;
 import com.google.gerrit.server.util.TimeUtil;
 import com.google.gerrit.sshd.SshScope.Context;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
-import org.apache.log4j.Appender;
 import org.apache.log4j.AsyncAppender;
-import org.apache.log4j.DailyRollingFileAppender;
 import org.apache.log4j.Level;
 import org.apache.log4j.Logger;
-import org.apache.log4j.spi.ErrorHandler;
 import org.apache.log4j.spi.LoggingEvent;
 import org.eclipse.jgit.lib.Config;
 
-import java.io.File;
-import java.io.IOException;
-
 @Singleton
 class SshLog implements LifecycleListener {
   private static final Logger log = Logger.getLogger(SshLog.class);
@@ -62,7 +55,8 @@
 
   @Inject
   SshLog(final Provider<SshSession> session, final Provider<Context> context,
-      final SitePaths site, @GerritServerConfig Config config, AuditService auditService) {
+      SystemLog systemLog, @GerritServerConfig Config config,
+      AuditService auditService) {
     this.session = session;
     this.context = context;
     this.auditService = auditService;
@@ -71,36 +65,7 @@
       async = null;
       return;
     }
-
-    async = new AsyncAppender();
-    async.setBlocking(true);
-    async.setBufferSize(config.getInt("core", "asyncLoggingBufferSize", 64));
-    async.setLocationInfo(false);
-
-    if (LogUtil.shouldConfigureLogSystem()) {
-      final DailyRollingFileAppender dst = new DailyRollingFileAppender();
-      dst.setName(LOG_NAME);
-      dst.setLayout(new SshLogLayout());
-      dst.setEncoding("UTF-8");
-      dst.setFile(new File(resolve(site.logs_dir), LOG_NAME).getPath());
-      dst.setImmediateFlush(true);
-      dst.setAppend(true);
-      dst.setThreshold(Level.INFO);
-      dst.setErrorHandler(new DieErrorHandler());
-      dst.activateOptions();
-      dst.setErrorHandler(new LogLogHandler());
-      async.addAppender(dst);
-    } else {
-      Appender appender = log.getAppender(LOG_NAME);
-      if (appender != null) {
-        async.addAppender(appender);
-      } else {
-        log.warn("No appender with the name: "
-            + LOG_NAME
-            + " was found. SSHD logging is disabled");
-      }
-    }
-    async.activateOptions();
+    async = systemLog.createAsyncAppender(LOG_NAME, new SshLogLayout());
   }
 
   @Override
@@ -276,125 +241,38 @@
     return IdGenerator.format(id);
   }
 
-  private static File resolve(final File logs_dir) {
-    try {
-      return logs_dir.getCanonicalFile();
-    } catch (IOException e) {
-      return logs_dir.getAbsoluteFile();
-    }
-  }
-
-  private static final class DieErrorHandler implements ErrorHandler {
-    @Override
-    public void error(String message, Exception e, int errorCode,
-        LoggingEvent event) {
-      error(e != null ? e.getMessage() : message);
-    }
-
-    @Override
-    public void error(String message, Exception e, int errorCode) {
-      error(e != null ? e.getMessage() : message);
-    }
-
-    @Override
-    public void error(String message) {
-      throw new RuntimeException("Cannot open log file: " + message);
-    }
-
-    @Override
-    public void activateOptions() {
-    }
-
-    @Override
-    public void setAppender(Appender appender) {
-    }
-
-    @Override
-    public void setBackupAppender(Appender appender) {
-    }
-
-    @Override
-    public void setLogger(Logger logger) {
-    }
-  }
-
-  private static final class LogLogHandler implements ErrorHandler {
-    @Override
-    public void error(String message, Exception e, int errorCode,
-        LoggingEvent event) {
-      log.error(message, e);
-    }
-
-    @Override
-    public void error(String message, Exception e, int errorCode) {
-      log.error(message, e);
-    }
-
-    @Override
-    public void error(String message) {
-      log.error(message);
-    }
-
-    @Override
-    public void activateOptions() {
-    }
-
-    @Override
-    public void setAppender(Appender appender) {
-    }
-
-    @Override
-    public void setBackupAppender(Appender appender) {
-    }
-
-    @Override
-    public void setLogger(Logger logger) {
-    }
-  }
-
   void audit(Context ctx, Object result, String cmd) {
-    final String sid = extractSessionId(ctx);
-    final long created = extractCreated(ctx);
-    auditService.dispatch(new SshAuditEvent(sid, extractCurrentUser(ctx), cmd,
-        created, null, result));
+    audit(ctx, result, cmd, null);
   }
 
   void audit(Context ctx, Object result, DispatchCommand cmd) {
-    final String sid = extractSessionId(ctx);
-    final long created = extractCreated(ctx);
-    auditService.dispatch(new SshAuditEvent(sid, extractCurrentUser(ctx),
-        extractWhat(cmd), created, extractParameters(cmd), result));
+    audit(ctx, result, extractWhat(cmd), extractParameters(cmd));
+  }
+
+  private void audit(Context ctx, Object result, String cmd, Multimap<String, ?> params) {
+    String sessionId;
+    CurrentUser currentUser;
+    long created;
+    if (ctx == null) {
+      sessionId = null;
+      currentUser = null;
+      created = TimeUtil.nowMs();
+    } else {
+      SshSession session = ctx.getSession();
+      sessionId = IdGenerator.format(session.getSessionId());
+      currentUser = session.getCurrentUser();
+      created = ctx.created;
+    }
+    auditService.dispatch(new SshAuditEvent(sessionId, currentUser,
+        cmd, created, params, result));
   }
 
   private String extractWhat(DispatchCommand dcmd) {
     String commandName = dcmd.getCommandName();
     String[] args = dcmd.getArguments();
-    if (args.length > 1) {
-      return commandName + "." + args[1];
-    } else {
-      return commandName;
+    for (int i = 1; i < args.length; i++) {
+      commandName = commandName + "." + args[i];
     }
-  }
-
-  private long extractCreated(final Context ctx) {
-    return (ctx != null) ? ctx.created : TimeUtil.nowMs();
-  }
-
-  private CurrentUser extractCurrentUser(final Context ctx) {
-    if (ctx != null) {
-      SshSession session = ctx.getSession();
-      return (session == null) ? null : session.getCurrentUser();
-    } else {
-      return null;
-    }
-  }
-
-  private String extractSessionId(final Context ctx) {
-    if (ctx != null) {
-      SshSession session = ctx.getSession();
-      return (session == null) ? null : IdGenerator.format(session.getSessionId());
-    } else {
-      return null;
-    }
+    return commandName;
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
index 50ab639..7dd12b0 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
@@ -81,7 +81,7 @@
     bind(QueueProvider.class).to(CommandExecutorQueueProvider.class).in(SINGLETON);
 
     bind(GSSAuthenticator.class).to(GerritGSSAuthenticator.class);
-    bind(PublickeyAuthenticator.class).to(DatabasePubKeyAuth.class);
+    bind(PublickeyAuthenticator.class).to(CachingPublicKeyAuthenticator.class);
 
     bind(ModuleGenerator.class).to(SshAutoRegisterModuleGenerator.class);
     bind(SshPluginStarterCallback.class);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java
index 9067b9b..f8b5ddb 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java
@@ -144,8 +144,7 @@
     }
   }
 
-  private static final ThreadLocal<Context> current =
-      new ThreadLocal<Context>();
+  private static final ThreadLocal<Context> current = new ThreadLocal<>();
 
   private static Context requireContext() {
     final Context ctx = current.get();
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshSession.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshSession.java
index 04bf603..f055b2f 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshSession.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshSession.java
@@ -26,8 +26,7 @@
 /** Global data related to an active SSH connection. */
 public class SshSession {
   /** ServerSession attribute key for this object instance. */
-  public static final AttributeKey<SshSession> KEY =
-      new AttributeKey<SshSession>();
+  public static final AttributeKey<SshSession> KEY = new AttributeKey<>();
 
   private final int sessionId;
   private final SocketAddress remoteAddress;
@@ -65,6 +64,14 @@
     return identity;
   }
 
+  public SocketAddress getRemoteAddress() {
+    return remoteAddress;
+  }
+
+  public String getRemoteAddressAsString() {
+    return remoteAsString;
+  }
+
   String getUsername() {
     return username;
   }
@@ -95,14 +102,6 @@
     return authError != null;
   }
 
-  SocketAddress getRemoteAddress() {
-    return remoteAddress;
-  }
-
-  String getRemoteAddressAsString() {
-    return remoteAsString;
-  }
-
   private static String format(final SocketAddress remote) {
     if (remote instanceof InetSocketAddress) {
       final InetSocketAddress sa = (InetSocketAddress) remote;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
index 298a099..77738d0 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
@@ -59,7 +59,7 @@
   private SocketAddress peerAddress;
 
   @Argument(index = 0, multiValued = true, metaVar = "COMMAND")
-  private List<String> args = new ArrayList<String>();
+  private List<String> args = new ArrayList<>();
 
   private final AtomicReference<Command> atomicCmd;
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
index fa48084..d448bd2 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
@@ -20,6 +20,7 @@
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.git.MetaDataUpdate;
@@ -27,7 +28,6 @@
 import com.google.gerrit.server.project.ListChildProjects;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.server.project.ProjectJson.ProjectInfo;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.sshd.CommandMetaData;
@@ -63,11 +63,11 @@
 
   @Option(name = "--exclude", metaVar = "NAME",
       usage = "child project of old parent project which should not be reparented")
-  private List<ProjectControl> excludedChildren = new ArrayList<ProjectControl>();
+  private List<ProjectControl> excludedChildren = new ArrayList<>();
 
   @Argument(index = 0, required = false, multiValued = true, metaVar = "NAME",
       usage = "projects to modify")
-  private List<ProjectControl> children = new ArrayList<ProjectControl>();
+  private List<ProjectControl> children = new ArrayList<>();
 
   @Inject
   private ProjectCache projectCache;
@@ -95,7 +95,7 @@
     }
 
     final StringBuilder err = new StringBuilder();
-    final Set<Project.NameKey> grandParents = new HashSet<Project.NameKey>();
+    final Set<Project.NameKey> grandParents = new HashSet<>();
 
     grandParents.add(allProjectsName);
 
@@ -187,12 +187,12 @@
   private List<Project.NameKey> getChildrenForReparenting(final ProjectControl parent) {
     final List<Project.NameKey> childProjects = Lists.newArrayList();
     final List<Project.NameKey> excluded =
-      new ArrayList<Project.NameKey>(excludedChildren.size());
+        new ArrayList<>(excludedChildren.size());
     for (final ProjectControl excludedChild : excludedChildren) {
       excluded.add(excludedChild.getProject().getNameKey());
     }
     final List<Project.NameKey> automaticallyExcluded =
-      new ArrayList<Project.NameKey>(excludedChildren.size());
+        new ArrayList<>(excludedChildren.size());
     if (newParentKey != null) {
       automaticallyExcluded.addAll(getAllParents(newParentKey));
     }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
index 1d4b900..647d28d 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
@@ -16,22 +16,24 @@
 
 import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
 
-import com.google.gerrit.common.errors.PermissionDeniedException;
-import com.google.gerrit.server.git.BanCommit;
-import com.google.gerrit.server.git.BanCommitResult;
-import com.google.gerrit.server.git.MergeException;
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.project.BanCommit;
+import com.google.gerrit.server.project.BanCommit.BanResultInfo;
 import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.ProjectResource;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
-import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
+import org.apache.commons.collections.CollectionUtils;
 import org.eclipse.jgit.lib.ObjectId;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
 
 import java.io.IOException;
-import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -47,60 +49,38 @@
 
   @Argument(index = 1, required = true, multiValued = true, metaVar = "COMMIT",
       usage = "commit(s) that should be banned")
-  private List<ObjectId> commitsToBan = new ArrayList<ObjectId>();
+  private List<ObjectId> commitsToBan = new ArrayList<>();
 
   @Inject
-  private BanCommit.Factory banCommitFactory;
+  private BanCommit banCommit;
 
   @Override
   protected void run() throws Failure {
     try {
-      final BanCommitResult result =
-          banCommitFactory.create().ban(projectControl, commitsToBan, reason);
+      BanCommit.Input input =
+          BanCommit.Input.fromCommits(Lists.transform(commitsToBan,
+              new Function<ObjectId, String>() {
+                @Override
+                public String apply(ObjectId oid) {
+                  return oid.getName();
+                }
+              }));
+      input.reason = reason;
 
-      final List<ObjectId> newlyBannedCommits =
-          result.getNewlyBannedCommits();
-      if (!newlyBannedCommits.isEmpty()) {
-        stdout.print("The following commits were banned:\n");
-        printCommits(stdout, newlyBannedCommits);
-      }
-
-      final List<ObjectId> alreadyBannedCommits =
-          result.getAlreadyBannedCommits();
-      if (!alreadyBannedCommits.isEmpty()) {
-        stdout.print("The following commits were already banned:\n");
-        printCommits(stdout, alreadyBannedCommits);
-      }
-
-      final List<ObjectId> ignoredIds = result.getIgnoredObjectIds();
-      if (!ignoredIds.isEmpty()) {
-        stdout.print("The following ids do not represent commits"
-            + " and were ignored:\n");
-        printCommits(stdout, ignoredIds);
-      }
-    } catch (PermissionDeniedException e) {
-      throw die(e);
-    } catch (IOException e) {
-      throw die(e);
-    } catch (MergeException e) {
-      throw die(e);
-    } catch (InterruptedException e) {
-      throw die(e);
-    } catch (ConcurrentRefUpdateException e) {
+      BanResultInfo r = banCommit.apply(new ProjectResource(projectControl), input);
+      printCommits(r.newlyBanned, "The following commits were banned");
+      printCommits(r.alreadyBanned, "The following commits were already banned");
+      printCommits(r.ignored, "The following ids do not represent commits and were ignored");
+    } catch (RestApiException | IOException | InterruptedException e) {
       throw die(e);
     }
   }
 
-  private static void printCommits(final PrintWriter stdout,
-      final List<ObjectId> commits) {
-    boolean first = true;
-    for (final ObjectId c : commits) {
-      if (!first) {
-        stdout.print(",\n");
-      }
-      stdout.print(c.getName());
-      first = false;
+  private void printCommits(List<String> commits, String message) {
+    if (CollectionUtils.isNotEmpty(commits)) {
+      stdout.print(message + ":\n");
+      stdout.print(Joiner.on(",\n").join(commits));
+      stdout.print("\n\n");
     }
-    stdout.print("\n\n");
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CacheCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CacheCommand.java
deleted file mode 100644
index 7f9e5db..0000000
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CacheCommand.java
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.sshd.commands;
-
-import com.google.common.cache.Cache;
-import com.google.common.collect.Sets;
-import com.google.gerrit.extensions.registration.DynamicMap;
-import com.google.gerrit.sshd.SshCommand;
-import com.google.inject.Inject;
-
-import java.util.SortedSet;
-
-abstract class CacheCommand extends SshCommand {
-  @Inject
-  protected DynamicMap<Cache<?, ?>> cacheMap;
-
-  protected SortedSet<String> cacheNames() {
-    SortedSet<String> names = Sets.newTreeSet();
-    for (DynamicMap.Entry<Cache<?, ?>> e : cacheMap) {
-      names.add(cacheNameOf(e.getPluginName(), e.getExportName()));
-    }
-    return names;
-  }
-
-  protected String cacheNameOf(String plugin, String name) {
-    if ("gerrit".equals(plugin)) {
-      return name;
-    } else {
-      return plugin + "." + name;
-    }
-  }
-}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
index aef1560..aeb69d0 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
@@ -42,7 +42,7 @@
 @CommandMetaData(name = "create-account", description = "Create a new batch/role account")
 final class CreateAccountCommand extends SshCommand {
   @Option(name = "--group", aliases = {"-g"}, metaVar = "GROUP", usage = "groups to add account to")
-  private List<AccountGroup.Id> groups = new ArrayList<AccountGroup.Id>();
+  private List<AccountGroup.Id> groups = new ArrayList<>();
 
   @Option(name = "--full-name", metaVar = "NAME", usage = "display name of the account")
   private String fullName;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
index 91996b1..75194dc 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
@@ -53,7 +53,7 @@
   @Argument(index = 0, required = true, metaVar = "GROUP", usage = "name of group to be created")
   private String groupName;
 
-  private final Set<Account.Id> initialMembers = new HashSet<Account.Id>();
+  private final Set<Account.Id> initialMembers = new HashSet<>();
 
   @Option(name = "--member", aliases = {"-m"}, metaVar = "USERNAME", usage = "initial set of users to become members of the group")
   void addMember(final Account.Id id) {
@@ -63,7 +63,7 @@
   @Option(name = "--visible-to-all", usage = "to make the group visible to all registered users")
   private boolean visibleToAll;
 
-  private final Set<AccountGroup.UUID> initialGroups = new HashSet<AccountGroup.UUID>();
+  private final Set<AccountGroup.UUID> initialGroups = new HashSet<>();
 
   @Option(name = "--group", aliases = "-g", metaVar = "GROUP", usage = "initial set of groups to be included in the group")
   void addGroup(final AccountGroup.UUID id) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
index 67d2738..fb39dee5 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
@@ -19,29 +19,26 @@
 import com.google.common.base.Splitter;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.common.errors.ProjectCreationFailedException;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.extensions.api.projects.ProjectInput;
+import com.google.gerrit.extensions.api.projects.ProjectInput.ConfigValue;
+import com.google.gerrit.extensions.common.InheritableBoolean;
+import com.google.gerrit.extensions.common.SubmitType;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
-import com.google.gerrit.reviewdb.client.Project.SubmitType;
-import com.google.gerrit.server.project.CreateProject;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.server.project.PutConfig.ConfigValue;
 import com.google.gerrit.server.project.SuggestParentCandidates;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
 
-import java.io.IOException;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -135,10 +132,10 @@
   }
 
   @Inject
-  private Provider<CreateProject.Factory> createProjectFactory;
+  private GerritApi gApi;
 
   @Inject
-  private SuggestParentCandidates.Factory suggestParentCandidatesFactory;
+  private SuggestParentCandidates suggestParentCandidates;
 
   @Override
   protected void run() throws UnloggedFailure {
@@ -148,7 +145,7 @@
           throw new UnloggedFailure(1, "fatal: Project name is required.");
         }
 
-        CreateProject.Input input = new CreateProject.Input();
+        ProjectInput input = new ProjectInput();
         input.name = projectName;
         if (ownerIds != null) {
           input.owners = Lists.transform(ownerIds,
@@ -176,18 +173,16 @@
           input.pluginConfigValues = parsePluginConfigValues(pluginConfigValues);
         }
 
-        createProjectFactory.get().create(projectName)
-            .apply(TopLevelResource.INSTANCE, input);
+        gApi.projects().name(projectName).create(input);
       } else {
         List<Project.NameKey> parentCandidates =
-            suggestParentCandidatesFactory.create().getNameKeys();
+            suggestParentCandidates.getNameKeys();
 
         for (Project.NameKey parent : parentCandidates) {
           stdout.print(parent + "\n");
         }
       }
-    } catch (RestApiException | ProjectCreationFailedException | IOException
-        | NoSuchProjectException | OrmException err) {
+    } catch (RestApiException | OrmException | NoSuchProjectException err) {
       throw new UnloggedFailure(1, "fatal: " + err.getMessage(), err);
     }
   }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
index 40152b0..6dfcb51 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
@@ -14,32 +14,34 @@
 
 package com.google.gerrit.sshd.commands;
 
+import static com.google.gerrit.server.config.PostCaches.Operation.FLUSH;
+import static com.google.gerrit.server.config.PostCaches.Operation.FLUSH_ALL;
 import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
 
-import com.google.common.cache.Cache;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
-import com.google.gerrit.extensions.registration.DynamicMap;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.config.ConfigResource;
+import com.google.gerrit.server.config.ListCaches;
+import com.google.gerrit.server.config.ListCaches.OutputFormat;
+import com.google.gerrit.server.config.PostCaches;
 import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 
 import org.kohsuke.args4j.Option;
 
 import java.util.ArrayList;
 import java.util.List;
-import java.util.SortedSet;
 
 /** Causes the caches to purge all entries and reload. */
 @RequiresCapability(GlobalCapability.FLUSH_CACHES)
 @CommandMetaData(name = "flush-caches", description = "Flush some/all server caches from memory",
   runsAt = MASTER_OR_SLAVE)
-final class FlushCaches extends CacheCommand {
-  private static final String WEB_SESSIONS = "web_sessions";
-
+final class FlushCaches extends SshCommand {
   @Option(name = "--cache", usage = "flush named cache", metaVar = "NAME")
-  private List<String> caches = new ArrayList<String>();
+  private List<String> caches = new ArrayList<>();
 
   @Option(name = "--all", usage = "flush all caches")
   private boolean all;
@@ -48,85 +50,54 @@
   private boolean list;
 
   @Inject
-  IdentifiedUser currentUser;
+  private Provider<ListCaches> listCaches;
+
+  @Inject
+  private PostCaches postCaches;
 
   @Override
   protected void run() throws Failure {
-    if (caches.contains(WEB_SESSIONS)
-        && !currentUser.getCapabilities().canAdministrateServer()) {
-      String msg = String.format(
-          "fatal: only site administrators can flush %s",
-          WEB_SESSIONS);
-      throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
-    }
-
-    if (list) {
-      if (all || caches.size() > 0) {
-        throw error("error: cannot use --list with --all or --cache");
+    try {
+      if (list) {
+        if (all || caches.size() > 0) {
+          throw error("error: cannot use --list with --all or --cache");
+        }
+        doList();
+        return;
       }
-      doList();
-      return;
-    }
 
-    if (all && caches.size() > 0) {
-      throw error("error: cannot combine --all and --cache");
-    } else if (!all && caches.size() == 1 && caches.contains("all")) {
-      caches.clear();
-      all = true;
-    } else if (!all && caches.isEmpty()) {
-      all = true;
-    }
-
-    final SortedSet<String> names = cacheNames();
-    for (final String n : caches) {
-      if (!names.contains(n)) {
-        throw error("error: cache \"" + n + "\" not recognized");
+      if (all && caches.size() > 0) {
+        throw error("error: cannot combine --all and --cache");
+      } else if (!all && caches.size() == 1 && caches.contains("all")) {
+        caches.clear();
+        all = true;
+      } else if (!all && caches.isEmpty()) {
+        all = true;
       }
+
+      if (all) {
+        postCaches.apply(new ConfigResource(),
+            new PostCaches.Input(FLUSH_ALL));
+      } else {
+        postCaches.apply(new ConfigResource(),
+            new PostCaches.Input(FLUSH, caches));
+      }
+    } catch (RestApiException e) {
+      throw die(e.getMessage());
     }
-    doBulkFlush();
   }
 
-  private static UnloggedFailure error(final String msg) {
+  private static UnloggedFailure error(String msg) {
     return new UnloggedFailure(1, msg);
   }
 
+  @SuppressWarnings("unchecked")
   private void doList() {
-    for (final String name : cacheNames()) {
+    for (String name : (List<String>) listCaches.get()
+        .setFormat(OutputFormat.LIST).apply(new ConfigResource())) {
       stderr.print(name);
       stderr.print('\n');
     }
     stderr.flush();
   }
-
-  private void doBulkFlush() {
-    try {
-      for (DynamicMap.Entry<Cache<?, ?>> e : cacheMap) {
-        String n = cacheNameOf(e.getPluginName(), e.getExportName());
-        if (flush(n)) {
-          try {
-            e.getProvider().get().invalidateAll();
-          } catch (Throwable err) {
-            stderr.println("error: cannot flush cache \"" + n + "\": " + err);
-          }
-        }
-      }
-    } finally {
-      stderr.flush();
-    }
-  }
-
-  private boolean flush(final String cacheName) {
-    if (caches.contains(cacheName)) {
-      return true;
-
-    } else if (all) {
-      if (WEB_SESSIONS.equals(cacheName)) {
-        return false;
-      }
-      return true;
-
-    } else {
-      return false;
-    }
-  }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java
index 821701d..a1099dc 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java
@@ -51,7 +51,7 @@
 
   @Argument(index = 0, required = false, multiValued = true, metaVar = "NAME",
       usage = "projects for which the Git garbage collection should be run")
-  private List<ProjectControl> projects = new ArrayList<ProjectControl>();
+  private List<ProjectControl> projects = new ArrayList<>();
 
   @Inject
   private ProjectCache projectCache;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/KillCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/KillCommand.java
index 83e88e5..95beaf3 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/KillCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/KillCommand.java
@@ -16,44 +16,44 @@
 
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
-import com.google.gerrit.server.git.WorkQueue;
-import com.google.gerrit.server.git.WorkQueue.Task;
-import com.google.gerrit.server.util.IdGenerator;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.server.config.ConfigResource;
+import com.google.gerrit.server.config.DeleteTask;
+import com.google.gerrit.server.config.TaskResource;
+import com.google.gerrit.server.config.TasksCollection;
 import com.google.gerrit.sshd.AdminHighPriorityCommand;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
 import org.kohsuke.args4j.Argument;
 
-import java.util.HashSet;
-import java.util.Set;
+import java.util.ArrayList;
+import java.util.List;
 
 /** Kill a task in the work queue. */
 @AdminHighPriorityCommand
 @RequiresCapability(GlobalCapability.KILL_TASK)
 final class KillCommand extends SshCommand {
   @Inject
-  private WorkQueue workQueue;
+  private TasksCollection tasksCollection;
 
-  private final Set<Integer> taskIds = new HashSet<Integer>();
+  @Inject
+  private DeleteTask deleteTask;
 
   @Argument(index = 0, multiValued = true, required = true, metaVar = "ID")
-  void addTaskId(final String taskId) {
-    int p = 0;
-    while (p < taskId.length() - 1 && taskId.charAt(p) == '0') {
-      p++;
-    }
-    taskIds.add((int) Long.parseLong(taskId.substring(p), 16));
-  }
+  private final List<String> taskIds = new ArrayList<>();
 
   @Override
   protected void run() {
-    for (final Integer id : taskIds) {
-      final Task<?> task = workQueue.getTask(id);
-      if (task != null) {
-        task.cancel(true);
-      } else {
-        stderr.print("kill: " + IdGenerator.format(id) + ": No such task\n");
+    ConfigResource cfgRsrc = new ConfigResource();
+    for (String id : taskIds) {
+      try {
+        TaskResource taskRsrc = tasksCollection.parse(cfgRsrc, IdString.fromDecoded(id));
+        deleteTask.apply(taskRsrc, null);
+      } catch (AuthException | ResourceNotFoundException e) {
+        stderr.print("kill: " + id + ": No such task\n");
       }
     }
   }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java
index 77c5df4..39ec81f5 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java
@@ -306,7 +306,7 @@
     try {
       ResultSet rs = meta.getIndexInfo(null, null, tableName, false, true);
       try {
-        Map<String, IndexInfo> indexes = new TreeMap<String, IndexInfo>();
+        Map<String, IndexInfo> indexes = new TreeMap<>();
         while (rs.next()) {
           final String indexName = rs.getString("INDEX_NAME");
           IndexInfo def = indexes.get(indexName);
@@ -550,7 +550,7 @@
       widths[c] = columnMap[c].name.length();
     }
 
-    final List<String[]> rows = new ArrayList<String[]>();
+    final List<String[]> rows = new ArrayList<>();
     while (alreadyOnRow || rs.next()) {
       final String[] row = new String[columnMap.length];
       for (int c = 0; c < colCnt; c++) {
@@ -759,7 +759,7 @@
   private static class IndexInfo {
     String name;
     boolean unique;
-    final Map<Integer, String> columns = new TreeMap<Integer, String>();
+    final Map<Integer, String> columns = new TreeMap<>();
     final StringBuilder filter = new StringBuilder();
 
     void addColumn(int pos, String column) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java
index a3c2cf5..abb788d 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java
@@ -70,8 +70,8 @@
   @Inject
   private DynamicSet<PostReceiveHook> postReceiveHooks;
 
-  private final Set<Account.Id> reviewerId = new HashSet<Account.Id>();
-  private final Set<Account.Id> ccId = new HashSet<Account.Id>();
+  private final Set<Account.Id> reviewerId = new HashSet<>();
+  private final Set<Account.Id> ccId = new HashSet<>();
 
   @Option(name = "--reviewer", aliases = {"--re"}, metaVar = "EMAIL", usage = "request reviewer for change(s)")
   void addReviewer(final Account.Id id) {
@@ -158,7 +158,7 @@
 
         Map<String, Ref> allRefs =
             rp.getRepository().getRefDatabase().getRefs(RefDatabase.ALL);
-        List<Ref> hidden = new ArrayList<Ref>();
+        List<Ref> hidden = new ArrayList<>();
         for (Ref ref : allRefs.values()) {
           if (!adv.containsKey(ref.getName())) {
             hidden.add(ref);
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 da29139..f39cd36 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
@@ -25,9 +25,6 @@
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.RevisionApi;
-import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -59,7 +56,7 @@
 import java.util.Map;
 import java.util.Set;
 
-@CommandMetaData(name = "review", description = "Verify, approve and/or submit one or more patch sets")
+@CommandMetaData(name = "review", description = "Apply reviews to one or more patch sets")
 public class ReviewCommand extends SshCommand {
   private static final Logger log =
       LoggerFactory.getLogger(ReviewCommand.class);
@@ -73,7 +70,7 @@
     return parser;
   }
 
-  private final Set<PatchSet> patchSets = new HashSet<PatchSet>();
+  private final Set<PatchSet> patchSets = new HashSet<>();
 
   @Argument(index = 0, required = true, multiValued = true, metaVar = "{COMMIT | CHANGE,PATCHSET}", usage = "list of commits or patch sets to review")
   void addPatchSetId(final String token) {
@@ -171,7 +168,7 @@
     boolean ok = true;
     for (final PatchSet patchSet : patchSets) {
       try {
-        approveOne(patchSet);
+        reviewPatchSet(patchSet);
       } catch (UnloggedFailure e) {
         ok = false;
         writeError("error: " + e.getMessage() + "\n");
@@ -180,14 +177,14 @@
         writeError("no such change " + patchSet.getId().getParentKey().get());
       } catch (Exception e) {
         ok = false;
-        writeError("fatal: internal server error while approving "
+        writeError("fatal: internal server error while reviewing "
             + patchSet.getId() + "\n");
-        log.error("internal error while approving " + patchSet.getId(), e);
+        log.error("internal error while reviewing " + patchSet.getId(), e);
       }
     }
 
     if (!ok) {
-      throw new UnloggedFailure(1, "one or more approvals failed;"
+      throw new UnloggedFailure(1, "one or more reviews failed;"
           + " review output above");
     }
   }
@@ -200,7 +197,7 @@
         .review(review);
   }
 
-  private void approveOne(final PatchSet patchSet) throws Exception {
+  private void reviewPatchSet(final PatchSet patchSet) throws Exception {
 
     if (changeComment == null) {
       changeComment = "";
@@ -254,17 +251,8 @@
       } else if (deleteDraftPatchSet) {
         revisionApi(patchSet).delete();
       }
-    } catch (InvalidChangeOperationException e) {
-      throw error(e.getMessage());
-    } catch (IllegalStateException e) {
-      throw error(e.getMessage());
-    } catch (AuthException e) {
-      throw error(e.getMessage());
-    } catch (BadRequestException e) {
-      throw error(e.getMessage());
-    } catch (ResourceConflictException e) {
-      throw error(e.getMessage());
-    } catch (RestApiException e) {
+    } catch (IllegalStateException | InvalidChangeOperationException
+        | RestApiException e) {
       throw error(e.getMessage());
     }
   }
@@ -290,7 +278,7 @@
         patches = db.patchSets().byRevisionRange(id, id.max());
       }
 
-      final Set<PatchSet> matches = new HashSet<PatchSet>();
+      final Set<PatchSet> matches = new HashSet<>();
       for (final PatchSet ps : patches) {
         final Change change = db.changes().get(ps.getId().getParentKey());
         if (inProject(change) && inBranch(change)) {
@@ -356,7 +344,7 @@
 
   @Override
   protected void parseCommandLine() throws UnloggedFailure {
-    optionList = new ArrayList<ApproveOption>();
+    optionList = new ArrayList<>();
     customLabels = Maps.newHashMap();
 
     ProjectControl allProjectsControl;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
index f3f35db..5088424 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
@@ -71,16 +71,16 @@
   private boolean inactive;
 
   @Option(name = "--add-email", metaVar = "EMAIL", usage = "email addresses to add to the account")
-  private List<String> addEmails = new ArrayList<String>();
+  private List<String> addEmails = new ArrayList<>();
 
   @Option(name = "--delete-email", metaVar = "EMAIL", usage = "email addresses to delete from the account")
-  private List<String> deleteEmails = new ArrayList<String>();
+  private List<String> deleteEmails = new ArrayList<>();
 
   @Option(name = "--add-ssh-key", metaVar = "-|KEY", usage = "public keys to add to the account")
-  private List<String> addSshKeys = new ArrayList<String>();
+  private List<String> addSshKeys = new ArrayList<>();
 
   @Option(name = "--delete-ssh-key", metaVar = "-|KEY", usage = "public keys to delete from the account")
-  private List<String> deleteSshKeys = new ArrayList<String>();
+  private List<String> deleteSshKeys = new ArrayList<>();
 
   @Option(name = "--http-password", metaVar = "PASSWORD", usage = "password for HTTP authentication for the account")
   private String httpPassword;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
index e0c2a97..cc72e96 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
@@ -16,10 +16,10 @@
 
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.api.projects.ProjectState;
+import com.google.gerrit.extensions.common.InheritableBoolean;
+import com.google.gerrit.extensions.common.SubmitType;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
-import com.google.gerrit.reviewdb.client.Project.State;
-import com.google.gerrit.reviewdb.client.Project.SubmitType;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.project.ProjectCache;
@@ -106,7 +106,7 @@
   }
 
   @Option(name = "--project-state", aliases = {"--ps"}, usage = "project's visibility state")
-  private State state;
+  private ProjectState state;
 
   @Option(name = "--max-object-size-limit", usage = "max Git object size for this project")
   private String maxObjectSizeLimit;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
index 41d3e97..8ce935c 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
@@ -56,7 +56,7 @@
   private ProjectControl projectControl;
 
   @Option(name = "--add", aliases = {"-a"}, metaVar = "REVIEWER", usage = "user or group that should be added as reviewer")
-  private List<String> toAdd = new ArrayList<String>();
+  private List<String> toAdd = new ArrayList<>();
 
   @Option(name = "--remove", aliases = {"-r"}, metaVar = "REVIEWER", usage = "user that should be removed from the reviewer list")
   void optionRemove(Account.Id who) {
@@ -92,8 +92,8 @@
   @Inject
   private ChangesCollection changesCollection;
 
-  private Set<Account.Id> toRemove = new HashSet<Account.Id>();
-  private Set<Change.Id> changes = new HashSet<Change.Id>();
+  private Set<Account.Id> toRemove = new HashSet<>();
+  private Set<Change.Id> changes = new HashSet<>();
 
   @Override
   protected void run() throws UnloggedFailure {
@@ -161,7 +161,7 @@
 
   private Set<Change.Id> parseChangeId(String idstr)
       throws UnloggedFailure, OrmException {
-    Set<Change.Id> matched = new HashSet<Change.Id>(4);
+    Set<Change.Id> matched = new HashSet<>(4);
     boolean isCommit = idstr.matches("^([0-9a-fA-F]{4," + RevId.LEN + "})$");
 
     // By newer style changeKey?
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java
index 397120f..1f3a49a 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java
@@ -16,20 +16,25 @@
 
 import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
 
-import com.google.common.cache.Cache;
-import com.google.common.cache.CacheStats;
-import com.google.common.collect.Maps;
+import com.google.common.base.Strings;
 import com.google.gerrit.common.Version;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.events.LifecycleListener;
-import com.google.gerrit.extensions.registration.DynamicMap;
-import com.google.gerrit.server.cache.h2.H2CacheImpl;
-import com.google.gerrit.server.config.SitePath;
-import com.google.gerrit.server.git.WorkQueue;
-import com.google.gerrit.server.git.WorkQueue.Task;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.ConfigResource;
+import com.google.gerrit.server.config.GetSummary;
+import com.google.gerrit.server.config.GetSummary.JvmSummaryInfo;
+import com.google.gerrit.server.config.GetSummary.MemSummaryInfo;
+import com.google.gerrit.server.config.GetSummary.SummaryInfo;
+import com.google.gerrit.server.config.GetSummary.TaskSummaryInfo;
+import com.google.gerrit.server.config.GetSummary.ThreadSummaryInfo;
+import com.google.gerrit.server.config.ListCaches;
+import com.google.gerrit.server.config.ListCaches.CacheInfo;
+import com.google.gerrit.server.config.ListCaches.CacheType;
 import com.google.gerrit.server.util.TimeUtil;
 import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
 import com.google.gerrit.sshd.SshDaemon;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -38,27 +43,20 @@
 import org.apache.sshd.common.io.IoSession;
 import org.apache.sshd.common.io.mina.MinaSession;
 import org.apache.sshd.server.Environment;
-import org.eclipse.jgit.internal.storage.file.WindowCacheStatAccessor;
 import org.kohsuke.args4j.Option;
 
-import java.io.File;
 import java.io.IOException;
-import java.lang.management.ManagementFactory;
-import java.lang.management.OperatingSystemMXBean;
-import java.lang.management.RuntimeMXBean;
-import java.net.InetAddress;
-import java.net.UnknownHostException;
 import java.text.SimpleDateFormat;
 import java.util.Collection;
 import java.util.Date;
 import java.util.Map;
-import java.util.SortedMap;
+import java.util.Map.Entry;
 
 /** Show the current cache states. */
 @RequiresCapability(GlobalCapability.VIEW_CACHES)
 @CommandMetaData(name = "show-caches", description = "Display current cache statistics",
   runsAt = MASTER_OR_SLAVE)
-final class ShowCaches extends CacheCommand {
+final class ShowCaches extends SshCommand {
   private static volatile long serverStarted;
 
   static class StartupListener implements LifecycleListener {
@@ -78,15 +76,20 @@
   @Option(name = "--show-jvm", usage = "show details about the JVM")
   private boolean showJVM;
 
-  @Inject
-  private WorkQueue workQueue;
+  @Option(name = "--show-threads", usage = "show detailed thread counts")
+  private boolean showThreads;
 
   @Inject
   private SshDaemon daemon;
 
   @Inject
-  @SitePath
-  private File sitePath;
+  private Provider<ListCaches> listCaches;
+
+  @Inject
+  private Provider<GetSummary> getSummary;
+
+  @Inject
+  private Provider<CurrentUser> self;
 
   @Option(name = "--width", aliases = {"-w"}, metaVar = "COLS", usage = "width of output table")
   private int columns = 80;
@@ -106,7 +109,7 @@
   }
 
   @Override
-  protected void run() {
+  protected void run() throws UnloggedFailure {
     nw = columns - 50;
     Date now = new Date();
     stdout.format(
@@ -145,128 +148,131 @@
     }
     stdout.print("+---------------------+---------+---------+\n");
 
-    Map<String, H2CacheImpl<?, ?>> disks = Maps.newTreeMap();
-    printMemoryCaches(disks, sortedCoreCaches());
-    printMemoryCaches(disks, sortedPluginCaches());
-    for (Map.Entry<String, H2CacheImpl<?, ?>> entry : disks.entrySet()) {
-      H2CacheImpl<?, ?> cache = entry.getValue();
-      CacheStats stat = cache.stats();
-      H2CacheImpl.DiskStats disk = cache.diskStats();
-      stdout.print(String.format(
-          "D %-"+nw+"s|%6s %6s %7s| %7s |%4s %4s|\n",
-          entry.getKey(),
-          count(cache.size()),
-          count(disk.size()),
-          bytes(disk.space()),
-          duration(stat.averageLoadPenalty()),
-          percent(stat.hitCount(), stat.requestCount()),
-          percent(disk.hitCount(), disk.requestCount())));
-    }
+    Collection<CacheInfo> caches = getCaches();
+    printMemoryCoreCaches(caches);
+    printMemoryPluginCaches(caches);
+    printDiskCaches(caches);
     stdout.print('\n');
 
-    if (gc) {
-      System.gc();
-      System.runFinalization();
-      System.gc();
-    }
+    if (self.get().getCapabilities().canAdministrateServer()) {
+      sshSummary();
 
-    sshSummary();
-    taskSummary();
-    memSummary();
+      SummaryInfo summary =
+          getSummary.get().setGc(gc).setJvm(showJVM).apply(new ConfigResource());
+      taskSummary(summary.taskSummary);
+      memSummary(summary.memSummary);
+      threadSummary(summary.threadSummary);
 
-    if (showJVM) {
-      jvmSummary();
+      if (showJVM && summary.jvmSummary != null) {
+        jvmSummary(summary.jvmSummary);
+      }
     }
 
     stdout.flush();
   }
 
-  private void printMemoryCaches(
-      Map<String, H2CacheImpl<?, ?>> disks,
-      Map<String, Cache<?,?>> caches) {
-    for (Map.Entry<String, Cache<?,?>> entry : caches.entrySet()) {
-      Cache<?,?> cache = entry.getValue();
-      if (cache instanceof H2CacheImpl) {
-        disks.put(entry.getKey(), (H2CacheImpl<?,?>)cache);
-        continue;
-      }
-      CacheStats stat = cache.stats();
-      stdout.print(String.format(
-          "  %-"+nw+"s|%6s %6s %7s| %7s |%4s %4s|\n",
-          entry.getKey(),
-          count(cache.size()),
-          "",
-          "",
-          duration(stat.averageLoadPenalty()),
-          percent(stat.hitCount(), stat.requestCount()),
-          ""));
+  private Collection<CacheInfo> getCaches() {
+    @SuppressWarnings("unchecked")
+    Map<String, CacheInfo> caches =
+        (Map<String, CacheInfo>) listCaches.get().apply(new ConfigResource());
+    for (Map.Entry<String, CacheInfo> entry : caches.entrySet()) {
+      CacheInfo cache = entry.getValue();
+      cache.name = entry.getKey();
     }
+    return caches.values();
   }
 
-  private Map<String, Cache<?, ?>> sortedCoreCaches() {
-    SortedMap<String, Cache<?, ?>> m = Maps.newTreeMap();
-    for (Map.Entry<String, Provider<Cache<?, ?>>> entry :
-        cacheMap.byPlugin("gerrit").entrySet()) {
-      m.put(cacheNameOf("gerrit", entry.getKey()), entry.getValue().get());
-    }
-    return m;
-  }
-
-  private Map<String, Cache<?, ?>> sortedPluginCaches() {
-    SortedMap<String, Cache<?, ?>> m = Maps.newTreeMap();
-    for (DynamicMap.Entry<Cache<?, ?>> e : cacheMap) {
-      if (!"gerrit".equals(e.getPluginName())) {
-        m.put(cacheNameOf(e.getPluginName(), e.getExportName()),
-            e.getProvider().get());
+  private void printMemoryCoreCaches(Collection<CacheInfo> caches) {
+    for (CacheInfo cache : caches) {
+      if (!cache.name.contains("-") && CacheType.MEM.equals(cache.type)) {
+        printCache(cache);
       }
     }
-    return m;
   }
 
-  private void memSummary() {
-    final Runtime r = Runtime.getRuntime();
-    final long mMax = r.maxMemory();
-    final long mFree = r.freeMemory();
-    final long mTotal = r.totalMemory();
-    final long mInuse = mTotal - mFree;
+  private void printMemoryPluginCaches(Collection<CacheInfo> caches) {
+    for (CacheInfo cache : caches) {
+      if (cache.name.contains("-") && CacheType.MEM.equals(cache.type)) {
+        printCache(cache);
+      }
+    }
+  }
 
-    final int jgitOpen = WindowCacheStatAccessor.getOpenFiles();
-    final long jgitBytes = WindowCacheStatAccessor.getOpenBytes();
+  private void printDiskCaches(Collection<CacheInfo> caches) {
+    for (CacheInfo cache : caches) {
+      if (CacheType.DISK.equals(cache.type)) {
+        printCache(cache);
+      }
+    }
+  }
 
+  private void printCache(CacheInfo cache) {
+    stdout.print(String.format(
+        "%1s %-"+nw+"s|%6s %6s %7s| %7s |%4s %4s|\n",
+        CacheType.DISK.equals(cache.type) ? "D" : "",
+        cache.name,
+        nullToEmpty(cache.entries.mem),
+        nullToEmpty(cache.entries.disk),
+        Strings.nullToEmpty(cache.entries.space),
+        Strings.nullToEmpty(cache.averageGet),
+        formatAsPercent(cache.hitRatio.mem),
+        formatAsPercent(cache.hitRatio.disk)
+      ));
+  }
+
+  private static String nullToEmpty(Long l) {
+    return l != null ? String.valueOf(l) : "";
+  }
+
+  private static String formatAsPercent(Integer i) {
+    return i != null ? String.valueOf(i) + "%" : "";
+  }
+
+  private void memSummary(MemSummaryInfo memSummary) {
     stdout.format("Mem: %s total = %s used + %s free + %s buffers\n",
-        bytes(mTotal),
-        bytes(mInuse - jgitBytes),
-        bytes(mFree),
-        bytes(jgitBytes));
-    stdout.format("     %s max\n", bytes(mMax));
-    stdout.format("    %8d open files, %8d cpus available, %8d threads\n",
-        jgitOpen,
-        r.availableProcessors(),
-        ManagementFactory.getThreadMXBean().getThreadCount());
+        memSummary.total,
+        memSummary.used,
+        memSummary.free,
+        memSummary.buffers);
+    stdout.format("     %s max\n", memSummary.max);
+    stdout.format("    %8d open files\n",
+        nullToZero(memSummary.openFiles));
     stdout.print('\n');
   }
 
-  private void taskSummary() {
-    Collection<Task<?>> pending = workQueue.getTasks();
-    int tasksTotal = pending.size();
-    int tasksRunning = 0, tasksReady = 0, tasksSleeping = 0;
-    for (Task<?> task : pending) {
-      switch (task.getState()) {
-        case RUNNING: tasksRunning++; break;
-        case READY: tasksReady++; break;
-        case SLEEPING: tasksSleeping++; break;
-        case CANCELLED:
-        case DONE:
-        case OTHER:
-          break;
+  private void threadSummary(ThreadSummaryInfo threadSummary) {
+    stdout.format("Threads: %d CPUs available, %d threads\n",
+        threadSummary.cpus, threadSummary.threads);
+
+    if (showThreads) {
+      stdout.print(String.format("  %22s", ""));
+      for (Thread.State s : Thread.State.values()) {
+        stdout.print(String.format(" %14s", s.name()));
+      }
+      stdout.print('\n');
+      for (Entry<String, Map<Thread.State, Integer>> e :
+          threadSummary.counts.entrySet()) {
+        stdout.print(String.format("  %-22s", e.getKey()));
+        for (Thread.State s : Thread.State.values()) {
+          stdout.print(String.format(" %14d", nullToZero(e.getValue().get(s))));
+        }
+        stdout.print('\n');
       }
     }
+    stdout.print('\n');
+  }
+
+  private void taskSummary(TaskSummaryInfo taskSummary) {
     stdout.format(
         "Tasks: %4d  total = %4d running +   %4d ready + %4d sleeping\n",
-        tasksTotal,
-        tasksRunning,
-        tasksReady,
-        tasksSleeping);
+        nullToZero(taskSummary.total),
+        nullToZero(taskSummary.running),
+        nullToZero(taskSummary.ready),
+        nullToZero(taskSummary.sleeping));
+  }
+
+  private static int nullToZero(Integer i) {
+    return i != null ? i : 0;
   }
 
   private void sshSummary() {
@@ -292,33 +298,20 @@
         uptime(now - oldest));
   }
 
-  private void jvmSummary() {
-    OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean();
-    RuntimeMXBean runtimeBean = ManagementFactory.getRuntimeMXBean();
+  private void jvmSummary(JvmSummaryInfo jvmSummary) {
     stdout.format("JVM: %s %s %s\n",
-        runtimeBean.getVmVendor(),
-        runtimeBean.getVmName(),
-        runtimeBean.getVmVersion());
+        jvmSummary.vmVendor,
+        jvmSummary.vmName,
+        jvmSummary.vmVersion);
     stdout.format("  on %s %s %s\n",
-        osBean.getName(),
-        osBean.getVersion(),
-        osBean.getArch());
-    try {
-      stdout.format("  running as %s on %s\n",
-          System.getProperty("user.name"),
-          InetAddress.getLocalHost().getHostName());
-    } catch (UnknownHostException e) {
-    }
-    stdout.format("  cwd  %s\n", path(new File(".").getAbsoluteFile().getParentFile()));
-    stdout.format("  site %s\n", path(sitePath));
-  }
-
-  private String path(File file) {
-    try {
-      return file.getCanonicalPath();
-    } catch (IOException err) {
-      return file.getAbsolutePath();
-    }
+        jvmSummary.osName,
+        jvmSummary.osVersion,
+        jvmSummary.osArch);
+    stdout.format("  running as %s on %s\n",
+        jvmSummary.user,
+        Strings.nullToEmpty(jvmSummary.host));
+    stdout.format("  cwd  %s\n", jvmSummary.currentWorkingDirectory);
+    stdout.format("  site %s\n", jvmSummary.site);
   }
 
   private String uptime(long uptimeMillis) {
@@ -343,54 +336,4 @@
     hr = (uptime - (days * 24 * 3600)) / 3600;
     return String.format("%4d days %2d hrs", days, hr);
   }
-
-  private String bytes(double value) {
-    value /= 1024;
-    String suffix = "k";
-
-    if (value > 1024) {
-      value /= 1024;
-      suffix = "m";
-    }
-    if (value > 1024) {
-      value /= 1024;
-      suffix = "g";
-    }
-    return String.format("%1$6.2f%2$s", value, suffix);
-  }
-
-  private String count(long cnt) {
-    if (cnt == 0) {
-      return "";
-    }
-    return String.format("%6d", cnt);
-  }
-
-  private String duration(double ns) {
-    if (ns < 0.5) {
-      return "";
-    }
-    String suffix = "ns";
-    if (ns >= 1000.0) {
-      ns /= 1000.0;
-      suffix = "us";
-    }
-    if (ns >= 1000.0) {
-      ns /= 1000.0;
-      suffix = "ms";
-    }
-    if (ns >= 1000.0) {
-      ns /= 1000.0;
-      suffix = "s ";
-    }
-    return String.format("%4.1f%s", ns, suffix);
-  }
-
-  private String percent(final long value, final long total) {
-    if (total <= 0) {
-      return "";
-    }
-    final long pcent = (100 * value) / total;
-    return String.format("%3d%%", (int) pcent);
-  }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java
index 17bea45..3d1ed3f 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java
@@ -30,7 +30,9 @@
 
 import org.apache.sshd.common.io.IoAcceptor;
 import org.apache.sshd.common.io.IoSession;
+import org.apache.sshd.common.io.mina.MinaAcceptor;
 import org.apache.sshd.common.io.mina.MinaSession;
+import org.apache.sshd.common.io.nio2.Nio2Acceptor;
 import org.apache.sshd.server.Environment;
 import org.apache.sshd.server.session.ServerSession;
 import org.kohsuke.args4j.Option;
@@ -84,7 +86,7 @@
     }
 
     final List<IoSession> list =
-        new ArrayList<IoSession>(acceptor.getManagedSessions().values());
+        new ArrayList<>(acceptor.getManagedSessions().values());
     Collections.sort(list, new Comparator<IoSession>() {
       @Override
       public int compare(IoSession arg0, IoSession arg1) {
@@ -132,6 +134,20 @@
           hostname(remoteAddress)));
     }
     stdout.print("--\n");
+    stdout.print("SSHD Backend: " + getBackend() + "\n");
+  }
+
+  private String getBackend() {
+    IoAcceptor acceptor = daemon.getIoAcceptor();
+    if (acceptor == null) {
+      return "";
+    } else if (acceptor instanceof MinaAcceptor) {
+      return "mina";
+    } else if (acceptor instanceof Nio2Acceptor) {
+      return "nio2";
+    } else {
+      return "unknown";
+    }
   }
 
   private static String id(final SshSession sd) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
index 77d79b3..147d52a 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
@@ -17,15 +17,12 @@
 import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
 
 import com.google.common.base.Objects;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.git.TaskInfoFactory;
-import com.google.gerrit.server.git.WorkQueue;
-import com.google.gerrit.server.git.WorkQueue.ProjectTask;
+import com.google.gerrit.server.config.ConfigResource;
+import com.google.gerrit.server.config.ListTasks;
+import com.google.gerrit.server.config.ListTasks.TaskInfo;
 import com.google.gerrit.server.git.WorkQueue.Task;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.server.util.IdGenerator;
 import com.google.gerrit.server.util.TimeUtil;
 import com.google.gerrit.sshd.AdminHighPriorityCommand;
 import com.google.gerrit.sshd.CommandMetaData;
@@ -37,11 +34,8 @@
 
 import java.io.IOException;
 import java.text.SimpleDateFormat;
-import java.util.Collections;
-import java.util.Comparator;
 import java.util.Date;
 import java.util.List;
-import java.util.concurrent.TimeUnit;
 
 /** Display the current work queue. */
 @AdminHighPriorityCommand
@@ -52,10 +46,7 @@
   private boolean wide;
 
   @Inject
-  private WorkQueue workQueue;
-
-  @Inject
-  private ProjectCache projectCache;
+  private ListTasks listTasks;
 
   @Inject
   private IdentifiedUser currentUser;
@@ -64,7 +55,7 @@
   private int taskNameWidth;
 
   @Override
-  public void start(final Environment env) throws IOException {
+  public void start(Environment env) throws IOException {
     String s = env.getEnv().get(Environment.ENV_COLUMNS);
     if (s != null && !s.isEmpty()) {
       try {
@@ -77,125 +68,59 @@
   }
 
   @Override
-  protected void run() {
+  protected void run() throws UnloggedFailure {
     taskNameWidth = wide ? Integer.MAX_VALUE : columns - 8 - 12 - 12 - 4 - 4;
-    final List<QueueTaskInfo> pending = getSortedTaskInfoList();
-
     stdout.print(String.format("%-8s %-12s %-12s %-4s %s\n", //
         "Task", "State", "StartTime", "", "Command"));
     stdout.print("----------------------------------------------"
         + "--------------------------------\n");
 
-    int numberOfPendingTasks = 0;
-    final long now = TimeUtil.nowMs();
-    final boolean viewAll = currentUser.getCapabilities().canViewQueue();
-
-    for (final QueueTaskInfo taskInfo : pending) {
-      final long delay = taskInfo.delayMillis;
-      final Task.State state = taskInfo.state;
-
-      final String start;
-      switch (state) {
-        case DONE:
-        case CANCELLED:
-        case RUNNING:
-        case READY:
-          start = format(state);
-          break;
-        default:
-          start = time(now, delay);
-          break;
-      }
-
-      boolean regularUserCanSee = false;
-      boolean hasCustomizedPrint = true;
-
-      // If the user is not administrator, check if has rights to see
-      // the Task
-      Project.NameKey projectName = null;
-      String remoteName = null;
-
-      if (!viewAll) {
-        projectName = taskInfo.getProjectNameKey();
-        remoteName = taskInfo.getRemoteName();
-        hasCustomizedPrint = taskInfo.hasCustomizedPrint();
-
-        ProjectState e = null;
-        if (projectName != null) {
-          e = projectCache.get(projectName);
+    try {
+      List<TaskInfo> tasks = listTasks.apply(new ConfigResource());
+      long now = TimeUtil.nowMs();
+      boolean viewAll = currentUser.getCapabilities().canViewQueue();
+      for (TaskInfo task : tasks) {
+        String start;
+        switch (task.state) {
+          case DONE:
+          case CANCELLED:
+          case RUNNING:
+          case READY:
+            start = format(task.state);
+            break;
+          default:
+            start = time(now, task.delay);
+            break;
         }
 
-        regularUserCanSee = e != null && e.controlFor(currentUser).isVisible();
+        // Shows information about tasks depending on the user rights
+        if (viewAll || task.projectName == null) {
+          String command = task.command.length() < taskNameWidth
+              ? task.command
+              : task.command.substring(0, taskNameWidth);
 
-        if (regularUserCanSee) {
-          numberOfPendingTasks++;
+          stdout.print(String.format("%8s %-12s %-12s %-4s %s\n",
+              task.id, start, startTime(task.startTime), "", command));
+        } else {
+          String remoteName = task.remoteName != null
+              ? task.remoteName + "/" + task.projectName
+              : task.projectName;
+
+          stdout.print(String.format("%8s %-12s %-4s %s\n",
+              task.id, start, startTime(task.startTime),
+              Objects.firstNonNull(remoteName, "n/a")));
         }
       }
-
-      String startTime = startTime(taskInfo.getStartTime());
-
-      // Shows information about tasks depending on the user rights
-      if (viewAll || (!hasCustomizedPrint && regularUserCanSee)) {
-        stdout.print(String.format("%8s %-12s %-12s %-4s %s\n", //
-            id(taskInfo.getTaskId()), start, startTime, "",
-            taskInfo.getTaskString(taskNameWidth)));
-      } else if (regularUserCanSee) {
-        if (projectName != null) {
-          if (remoteName == null) {
-            remoteName = projectName.get();
-          } else {
-            remoteName = remoteName + "/" + projectName.get();
-          }
-        }
-
-        stdout.print(String.format("%8s %-12s %-4s %s\n",
-            id(taskInfo.getTaskId()), start, startTime,
-            Objects.firstNonNull(remoteName, "n/a")));
-      }
+      stdout.print("----------------------------------------------"
+          + "--------------------------------\n");
+      stdout.print("  " + tasks.size() + " tasks\n");
+    } catch (AuthException e) {
+      throw die(e);
     }
-    stdout.print("----------------------------------------------"
-        + "--------------------------------\n");
-
-    if (viewAll) {
-      numberOfPendingTasks = pending.size();
-    }
-
-    stdout.print("  " + numberOfPendingTasks + " tasks\n");
   }
 
-  private List<QueueTaskInfo> getSortedTaskInfoList() {
-    final List<QueueTaskInfo> taskInfos =
-        workQueue.getTaskInfos(new TaskInfoFactory<QueueTaskInfo>() {
-          @Override
-          public QueueTaskInfo getTaskInfo(Task<?> task) {
-            return new QueueTaskInfo(task);
-          }
-        });
-    Collections.sort(taskInfos, new Comparator<QueueTaskInfo>() {
-      @Override
-      public int compare(QueueTaskInfo a, QueueTaskInfo b) {
-        if (a.state != b.state) {
-          return a.state.ordinal() - b.state.ordinal();
-        }
-
-        int cmp = Long.signum(a.delayMillis - b.delayMillis);
-        if (cmp != 0) {
-          return cmp;
-        }
-
-        return a.getTaskString(taskNameWidth)
-            .compareTo(b.getTaskString(taskNameWidth));
-      }
-    });
-    return taskInfos;
-  }
-
-  private static String id(final int id) {
-    return IdGenerator.format(id);
-  }
-
-  private static String time(final long now, final long delay) {
-    final Date when = new Date(now + delay);
+  private static String time(long now, long delay) {
+    Date when = new Date(now + delay);
     return format(when, delay);
   }
 
@@ -203,14 +128,14 @@
     return format(when, TimeUtil.nowMs() - when.getTime());
   }
 
-  private static String format(final Date when, final long timeFromNow) {
+  private static String format(Date when, long timeFromNow) {
     if (timeFromNow < 24 * 60 * 60 * 1000L) {
       return new SimpleDateFormat("HH:mm:ss.SSS").format(when);
     }
     return new SimpleDateFormat("MMM-dd HH:mm").format(when);
   }
 
-  private static String format(final Task.State state) {
+  private static String format(Task.State state) {
     switch (state) {
       case DONE:
         return "....... done";
@@ -226,50 +151,4 @@
         return state.toString();
     }
   }
-
-  private static class QueueTaskInfo {
-    private final long delayMillis;
-    private final Task.State state;
-    private final Task<?> task;
-
-    QueueTaskInfo(Task<?> task) {
-      this.task = task;
-      this.delayMillis = task.getDelay(TimeUnit.MILLISECONDS);
-      this.state = task.getState();
-    }
-
-    String getRemoteName() {
-      if (task instanceof ProjectTask) {
-        return ((ProjectTask<?>) task).getRemoteName();
-      }
-      return null;
-    }
-
-    Project.NameKey getProjectNameKey() {
-      if (task instanceof ProjectTask<?>) {
-        return ((ProjectTask<?>) task).getProjectNameKey();
-      }
-      return null;
-    }
-
-    boolean hasCustomizedPrint() {
-      if (task instanceof ProjectTask<?>) {
-        return ((ProjectTask<?>) task).hasCustomizedPrint();
-      }
-      return false;
-    }
-
-    int getTaskId() {
-      return task.getTaskId();
-    }
-
-    Date getStartTime() {
-      return task.getStartTime();
-    }
-
-    String getTaskString(int maxLength) {
-      String s = task.toString();
-      return s.length() < maxLength ? s : s.substring(0, maxLength);
-    }
-  }
 }
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 c55c7ed..6da858a 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
@@ -59,7 +59,7 @@
 
   /** Queue of events to stream to the connected user. */
   private final LinkedBlockingQueue<ChangeEvent> queue =
-      new LinkedBlockingQueue<ChangeEvent>(MAX_EVENTS);
+      new LinkedBlockingQueue<>(MAX_EVENTS);
 
   private final Gson gson = new Gson();
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java
index fc127ec..34f7107 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java
@@ -14,18 +14,26 @@
 
 package com.google.gerrit.sshd.commands;
 
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.git.ChangeCache;
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.git.TransferConfig;
 import com.google.gerrit.server.git.VisibleRefFilter;
+import com.google.gerrit.server.git.validators.UploadValidationException;
+import com.google.gerrit.server.git.validators.UploadValidators;
 import com.google.gerrit.sshd.AbstractGitCommand;
+import com.google.gerrit.sshd.SshSession;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
+import org.eclipse.jgit.transport.PreUploadHook;
+import org.eclipse.jgit.transport.PreUploadHookChain;
 import org.eclipse.jgit.transport.UploadPack;
 
 import java.io.IOException;
+import java.util.List;
 
 /** Publishes Git repositories over SSH using the Git upload-pack protocol. */
 final class Upload extends AbstractGitCommand {
@@ -41,6 +49,15 @@
   @Inject
   private ChangeCache changeCache;
 
+  @Inject
+  private DynamicSet<PreUploadHook> preUploadHooks;
+
+  @Inject
+  private UploadValidators.Factory uploadValidatorsFactory;
+
+  @Inject
+  private SshSession session;
+
   @Override
   protected void runImpl() throws IOException, Failure {
     if (!projectControl.canRunUploadPack()) {
@@ -54,6 +71,21 @@
     }
     up.setPackConfig(config.getPackConfig());
     up.setTimeout(config.getTimeout());
-    up.upload(in, out, err);
+
+    List<PreUploadHook> allPreUploadHooks = Lists.newArrayList(preUploadHooks);
+    allPreUploadHooks.add(uploadValidatorsFactory.create(project, repo,
+        session.getRemoteAddressAsString()));
+    up.setPreUploadHook(PreUploadHookChain.newChain(allPreUploadHooks));
+    try {
+      up.upload(in, out, err);
+    } catch (UploadValidationException e) {
+      // UploadValidationException is used by the UploadValidators to
+      // stop the uploadPack. We do not want this exception to go beyond this
+      // point otherwise it would print a stacktrace in the logs and return an
+      // internal server error to the client.
+      if (!e.isOutput()) {
+        up.sendMessage(e.getMessage());
+      }
+    }
   }
 }
diff --git a/gerrit-sshd/src/test/java/com/google/gerrit/sshd/commands/ProjectConfigParamParserTest.java b/gerrit-sshd/src/test/java/com/google/gerrit/sshd/commands/ProjectConfigParamParserTest.java
index a22bae2..4f48eea 100644
--- a/gerrit-sshd/src/test/java/com/google/gerrit/sshd/commands/ProjectConfigParamParserTest.java
+++ b/gerrit-sshd/src/test/java/com/google/gerrit/sshd/commands/ProjectConfigParamParserTest.java
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.sshd.commands;
 
-import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
 
-import com.google.gerrit.server.project.PutConfig.ConfigValue;
+import com.google.gerrit.extensions.api.projects.ProjectInput.ConfigValue;
 
 import org.junit.Before;
 import org.junit.Test;
diff --git a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
index 7fc6d6f..e330834 100644
--- a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
+++ b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
@@ -54,7 +54,6 @@
 import org.kohsuke.args4j.spi.Setter;
 import org.kohsuke.args4j.spi.FieldSetter;
 
-
 import java.io.StringWriter;
 import java.io.Writer;
 import java.lang.annotation.Annotation;
@@ -131,7 +130,7 @@
     out.write(name);
 
     char next = '?';
-    List<NamedOptionDef> booleans = new ArrayList<NamedOptionDef>();
+    List<NamedOptionDef> booleans = new ArrayList<>();
     for (@SuppressWarnings("rawtypes") OptionHandler handler : parser.options) {
       if (handler.option instanceof NamedOptionDef) {
         NamedOptionDef n = (NamedOptionDef) handler.option;
@@ -443,4 +442,8 @@
       return false;
     }
   }
+
+  public CmdLineException reject(String message) {
+    return new CmdLineException(parser, message);
+  }
 }
diff --git a/gerrit-war/BUCK b/gerrit-war/BUCK
index cde128e..f557edc 100644
--- a/gerrit-war/BUCK
+++ b/gerrit-war/BUCK
@@ -1,6 +1,6 @@
 include_defs('//tools/git.defs')
 
-java_library2(
+java_library(
   name = 'init',
   srcs = glob(['src/main/java/**/*.java']),
   deps = [
@@ -8,6 +8,7 @@
     '//gerrit-extension-api:api',
     '//gerrit-httpd:httpd',
     '//gerrit-lucene:lucene',
+    '//gerrit-oauth:oauth',
     '//gerrit-openid:openid',
     '//gerrit-pgm:init-api',
     '//gerrit-pgm:init-base',
@@ -23,7 +24,7 @@
     '//lib/log:api',
     '//lib/jgit:jgit',
   ],
-  compile_deps = ['//lib:servlet-api-3_1'],
+  provided_deps = ['//lib:servlet-api-3_1'],
   visibility = [
     '//:',
     '//gerrit-gwtdebug:gwtdebug',
@@ -33,7 +34,7 @@
 
 genrule(
   name = 'webapp_assets',
-  cmd = 'cd $SRCDIR/src/main/webapp; zip -qr $OUT .',
+  cmd = 'cd src/main/webapp; zip -qr $OUT .',
   srcs = glob(['src/main/webapp/**/*']),
   deps = [],
   out = 'webapp_assets.zip',
@@ -42,15 +43,14 @@
 
 genrule(
   name = 'log4j-config__jar',
-  cmd = 'jar cf $OUT -C $SRCDIR/src/main/resources .',
+  cmd = 'jar cf $OUT -C src/main/resources .',
   srcs = ['src/main/resources/log4j.properties'],
   out = 'log4j-config.jar',
 )
 
 prebuilt_jar(
   name = 'log4j-config',
-  binary_jar = genfile('log4j-config.jar'),
-  deps = [':log4j-config__jar'],
+  binary_jar = ':log4j-config__jar',
   visibility = [
     '//:',
     '//tools/eclipse:classpath',
@@ -59,8 +59,7 @@
 
 prebuilt_jar(
   name = 'version',
-  binary_jar = genfile('version.jar'),
-  deps = [':gen_version'],
+  binary_jar = ':gen_version',
   visibility = ['//:'],
 )
 
diff --git a/gerrit-war/pom.xml b/gerrit-war/pom.xml
index 4fde449..e01d57c 100644
--- a/gerrit-war/pom.xml
+++ b/gerrit-war/pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-war</artifactId>
-  <version>2.9.5</version>
+  <version>2.10.8</version>
   <packaging>war</packaging>
   <name>Gerrit Code Review - WAR</name>
   <description>Gerrit WAR</description>
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
index 78d5fff..f59401c 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
@@ -19,6 +19,7 @@
 
 import com.google.common.base.Splitter;
 import com.google.gerrit.common.ChangeHookRunner;
+import com.google.gerrit.httpd.auth.oauth.OAuthModule;
 import com.google.gerrit.httpd.auth.openid.OpenIdModule;
 import com.google.gerrit.httpd.plugins.HttpPluginModule;
 import com.google.gerrit.lifecycle.LifecycleManager;
@@ -35,15 +36,17 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.GerritServerConfigModule;
 import com.google.gerrit.server.config.MasterNodeStartup;
+import com.google.gerrit.server.config.RestCacheAdminModule;
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.contact.HttpContactStoreConnection;
+import com.google.gerrit.server.git.GarbageCollectionRunner;
 import com.google.gerrit.server.git.LocalDiskRepositoryManager;
 import com.google.gerrit.server.git.ReceiveCommitsExecutorModule;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.index.IndexModule;
 import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
 import com.google.gerrit.server.mail.SmtpEmailSender;
-import com.google.gerrit.server.patch.IntraLineWorkerPool;
+import com.google.gerrit.server.patch.DiffExecutorModule;
 import com.google.gerrit.server.plugins.PluginGuiceEnvironment;
 import com.google.gerrit.server.plugins.PluginRestApiModule;
 import com.google.gerrit.server.schema.DataSourceModule;
@@ -201,7 +204,7 @@
   }
 
   private Injector createDbInjector() {
-    final List<Module> modules = new ArrayList<Module>();
+    final List<Module> modules = new ArrayList<>();
     if (sitePath != null) {
       Module sitePathModule = new AbstractModule() {
         @Override
@@ -249,7 +252,7 @@
   }
 
   private Injector createCfgInjector() {
-    final List<Module> modules = new ArrayList<Module>();
+    final List<Module> modules = new ArrayList<>();
     if (sitePath == null) {
       // If we didn't get the site path from the system property
       // we need to get it from the database, as that's our old
@@ -272,18 +275,19 @@
   }
 
   private Injector createSysInjector() {
-    final List<Module> modules = new ArrayList<Module>();
+    final List<Module> modules = new ArrayList<>();
     modules.add(new WorkQueue.Module());
     modules.add(new ChangeHookRunner.Module());
     modules.add(new ReceiveCommitsExecutorModule());
     modules.add(new MergeabilityChecksExecutorModule());
-    modules.add(new IntraLineWorkerPool.Module());
+    modules.add(new DiffExecutorModule());
     modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
     modules.add(new InternalAccountDirectory.Module());
     modules.add(new DefaultCacheFactory.Module());
     modules.add(new SmtpEmailSender.Module());
     modules.add(new SignedTokenEmailTokenVerifier.Module());
     modules.add(new PluginRestApiModule());
+    modules.add(new RestCacheAdminModule());
     AbstractModule changeIndexModule;
     switch (IndexModule.getIndexType(cfgInjector)) {
       case LUCENE:
@@ -310,11 +314,12 @@
         bind(GerritUiOptions.class).toInstance(new GerritUiOptions(false));
       }
     });
+    modules.add(GarbageCollectionRunner.module());
     return cfgInjector.createChildInjector(modules);
   }
 
   private Injector createSshInjector() {
-    final List<Module> modules = new ArrayList<Module>();
+    final List<Module> modules = new ArrayList<>();
     modules.add(sysInjector.getInstance(SshModule.class));
     modules.add(new SshHostKeyModule());
     modules.add(new DefaultCommandModule(false));
@@ -322,7 +327,7 @@
   }
 
   private Injector createWebInjector() {
-    final List<Module> modules = new ArrayList<Module>();
+    final List<Module> modules = new ArrayList<>();
     modules.add(RequestContextFilter.module());
     modules.add(AllRequestFilter.module());
     modules.add(sysInjector.getInstance(GitOverHttpModule.class));
@@ -332,13 +337,15 @@
     } else {
       modules.add(new NoSshModule());
     }
-    modules.add(CacheBasedWebSession.module());
+    modules.add(H2CacheBasedWebSession.module());
     modules.add(HttpContactStoreConnection.module());
     modules.add(new HttpPluginModule());
 
     AuthConfig authConfig = cfgInjector.getInstance(AuthConfig.class);
     if (authConfig.getAuthType() == AuthType.OPENID) {
       modules.add(new OpenIdModule());
+    } else if (authConfig.getAuthType() == AuthType.OAUTH) {
+      modules.add(new OAuthModule());
     }
 
     return sysInjector.createChildInjector(modules);
diff --git a/gerrit-war/src/main/resources/log4j.properties b/gerrit-war/src/main/resources/log4j.properties
index cb14916..ef64f3b 100644
--- a/gerrit-war/src/main/resources/log4j.properties
+++ b/gerrit-war/src/main/resources/log4j.properties
@@ -12,14 +12,12 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 #
-log4j.rootCategory=DEBUG, stderr
+log4j.rootCategory=INFO, stderr
 log4j.appender.stderr=org.apache.log4j.ConsoleAppender
 log4j.appender.stderr.target=System.err
 log4j.appender.stderr.layout=org.apache.log4j.PatternLayout
 log4j.appender.stderr.layout.ConversionPattern=[%d] %-5p %c %x: %m%n
 
-log4j.logger.com.google.gerrit=INFO
-
 # Silence non-critical messages from MINA SSHD.
 #
 log4j.logger.org.apache.mina=WARN
@@ -28,10 +26,6 @@
 log4j.logger.org.apache.sshd.common.keyprovider.FileKeyPairProvider=INFO
 log4j.logger.com.google.gerrit.sshd.GerritServerSession=WARN
 
-# Silence non-critical messages from Jetty.
-#
-log4j.logger.org.eclipse.jetty=INFO
-
 # Silence non-critical messages from mime-util.
 #
 log4j.logger.eu.medsea.mimeutil=WARN
diff --git a/lib/BUCK b/lib/BUCK
index 18e27b8..42170ee 100644
--- a/lib/BUCK
+++ b/lib/BUCK
@@ -12,6 +12,7 @@
 define_license(name = 'clippy')
 define_license(name = 'codemirror')
 define_license(name = 'diffy')
+define_license(name = 'freebie_application_icon_set')
 define_license(name = 'h2')
 define_license(name = 'jgit')
 define_license(name = 'jsch')
@@ -50,51 +51,15 @@
 
 maven_jar(
   name = 'guava',
-  id = 'com.google.guava:guava:16.0',
-  sha1 = 'aca09d2e5e8416bf91550e72281958e35460be52',
+  id = 'com.google.guava:guava:17.0',
+  sha1 = '9c6ef172e8de35fd8d4d8783e4821e57cdef7445',
   license = 'Apache2.0',
 )
 
 maven_jar(
-  name = 'asm3',
-  id = 'asm:asm:3.2',
-  sha1 = '9bc1511dec6adf302991ced13303e4140fdf9ab7',
-  license = 'ow2',
-  attach_source = False,
-)
-
-maven_jar(
-  name = 'ow2-asm',
-  id = 'org.ow2.asm:asm:4.1',
-  sha1 = 'ad568238ee36a820bd6c6806807e8a14ea34684d',
-  license = 'ow2',
-)
-
-maven_jar(
-  name = 'ow2-asm-analysis',
-  id = 'org.ow2.asm:asm-analysis:4.1',
-  sha1 = '73401033069e4714f57b60aeae02f97210aaa64e',
-  license = 'ow2',
-)
-
-maven_jar(
-  name = 'ow2-asm-tree',
-  id = 'org.ow2.asm:asm-tree:4.1',
-  sha1 = '51085abcc4cb6c6e1cb5551e6f999eb8e31c5b2d',
-  license = 'ow2',
-)
-
-maven_jar(
-  name = 'ow2-asm-util',
-  id = 'org.ow2.asm:asm-util:4.1',
-  sha1 = '6344065cb0f94e2b930a95e6656e040ebc11df08',
-  license = 'ow2',
-)
-
-maven_jar(
   name = 'velocity',
-  id = 'org.apache.velocity:velocity:1.6.4',
-  sha1 = 'fcc58693dd8fc83d714fba149789be37cc19b66d',
+  id = 'org.apache.velocity:velocity:1.7',
+  sha1 = '2ceb567b8f3f21118ecdec129fe1271dbc09aa7a',
   license = 'Apache2.0',
   deps = [
     '//lib/commons:collections',
@@ -106,15 +71,15 @@
 
 maven_jar(
   name = 'jsch',
-  id = 'com.jcraft:jsch:0.1.50',
-  sha1 = 'fae4a0b1f2a96cb8f58f38da2650814c991cea01',
+  id = 'com.jcraft:jsch:0.1.51',
+  sha1 = '6ceee2696b07cc320d0e1aaea82c7b40768aca0f',
   license = 'jsch',
 )
 
 maven_jar(
   name = 'servlet-api-3_1',
-  id = 'org.apache.tomcat:tomcat-servlet-api:8.0.0-RC10',
-  sha1 = '975935b6203073938dfeeb28e4effc3b094c4fc4',
+  id = 'org.apache.tomcat:tomcat-servlet-api:8.0.5',
+  sha1 = '9ef01afc25481b82aa8f3615db536869f2dc961e',
   license = 'Apache2.0',
   exclude = ['META-INF/NOTICE', 'META-INF/LICENSE'],
 )
@@ -172,9 +137,9 @@
   license = 'Apache2.0',
   deps = [
     ':parboiled-core',
-    ':ow2-asm-tree',
-    ':ow2-asm-analysis',
-    ':ow2-asm-util',
+    '//lib/ow2:ow2-asm-tree',
+    '//lib/ow2:ow2-asm-analysis',
+    '//lib/ow2:ow2-asm-util',
   ],
   attach_source = False,
   visibility = [],
@@ -221,35 +186,6 @@
 )
 
 maven_jar(
-  name = 'easymock',
-  id = 'org.easymock:easymock:3.2',
-  sha1 = '00c82f7fa3ef377d8954b1db25123944b5af2ba4',
-  license = 'DO_NOT_DISTRIBUTE',
-  deps = [
-    ':cglib-2_2',
-    ':objenesis',
-  ],
-)
-
-maven_jar(
-  name = 'cglib-2_2',
-  id = 'cglib:cglib-nodep:2.2.2',
-  sha1 = '00d456bb230c70c0b95c76fb28e429d42f275941',
-  license = 'DO_NOT_DISTRIBUTE',
-  visibility = ['//lib:easymock'],
-  attach_source = False,
-)
-
-maven_jar(
-  name = 'objenesis',
-  id = 'org.objenesis:objenesis:1.2',
-  sha1 = 'bfcb0539a071a4c5a30690388903ac48c0667f2a',
-  license = 'DO_NOT_DISTRIBUTE',
-  visibility = ['//lib:easymock'],
-  attach_source = False,
-)
-
-maven_jar(
   name = 'tukaani-xz',
   id = 'org.tukaani:xz:1.4',
   sha1 = '18a9a2ce6abf32ea1b5fd31dae5210ad93f4e5e3',
@@ -257,3 +193,12 @@
   attach_source = False,
   visibility = ['//lib/jgit:jgit-archive'],
 )
+
+maven_jar(
+  name = 'javassist-3.17.1-GA',
+  # The GWT version is still at 3.16.1-GA, so those do not match
+  id = 'org.javassist:javassist:3.17.1-GA',
+  sha1 = '30c30512115866b6e0123f1913bc7735b9f76d08',
+  license = 'DO_NOT_DISTRIBUTE',
+)
+
diff --git a/lib/LICENSE-freebie_application_icon_set b/lib/LICENSE-freebie_application_icon_set
new file mode 100644
index 0000000..753ab1f
--- /dev/null
+++ b/lib/LICENSE-freebie_application_icon_set
@@ -0,0 +1 @@
+link:http://creativecommons.org/licenses/by/3.0/us/[CC-BY 3.0] (c) Matt Gentile, link:http://tympanus.net/codrops/2012/10/02/freebie-application-icon-set-png-psd-csh/[Freebie: Application Icon Set]
diff --git a/lib/asciidoctor/java/AsciiDoctor.java b/lib/asciidoctor/java/AsciiDoctor.java
index 933b929..9e48641 100644
--- a/lib/asciidoctor/java/AsciiDoctor.java
+++ b/lib/asciidoctor/java/AsciiDoctor.java
@@ -58,10 +58,10 @@
 
   @Option(name = "-a", usage =
       "a list of attributes, in the form key or key=value pair")
-  private List<String> attributes = new ArrayList<String>();
+  private List<String> attributes = new ArrayList<>();
 
   @Argument(usage = "input files")
-  private List<String> inputFiles = new ArrayList<String>();
+  private List<String> inputFiles = new ArrayList<>();
 
   public static String mapInFileToOutFile(
       String inFile, String inExt, String outExt) {
@@ -97,7 +97,7 @@
   }
 
   private Map<String, Object> getAttributes() {
-    Map<String, Object> attributeValues = new HashMap<String, Object>();
+    Map<String, Object> attributeValues = new HashMap<>();
 
     for (String attribute : attributes) {
       int equalsIndex = attribute.indexOf('=');
diff --git a/lib/asciidoctor/java/DocIndexer.java b/lib/asciidoctor/java/DocIndexer.java
index 0cb785c..96f3eb5 100644
--- a/lib/asciidoctor/java/DocIndexer.java
+++ b/lib/asciidoctor/java/DocIndexer.java
@@ -51,7 +51,7 @@
 import java.util.zip.ZipOutputStream;
 
 public class DocIndexer {
-  private static final Version LUCENE_VERSION = Version.LUCENE_46;
+  private static final Version LUCENE_VERSION = Version.LUCENE_48;
   private static final Pattern SECTION_HEADER = Pattern.compile("^=+ (.*)");
 
   @Option(name = "-o", usage = "output JAR file")
@@ -67,7 +67,7 @@
   private String outExt = ".html";
 
   @Argument(usage = "input files")
-  private List<String> inputFiles = new ArrayList<String>();
+  private List<String> inputFiles = new ArrayList<>();
 
   private void invoke(String... parameters) throws IOException {
     CmdLineParser parser = new CmdLineParser(this);
@@ -104,6 +104,9 @@
     IndexWriter iwriter = new IndexWriter(directory, config);
     for (String inputFile : inputFiles) {
       File file = new File(inputFile);
+      if (file.length() == 0) {
+        continue;
+      }
 
       BufferedReader titleReader = new BufferedReader(
           new InputStreamReader(new FileInputStream(file), "UTF-8"));
diff --git a/lib/bouncycastle/BUCK b/lib/bouncycastle/BUCK
index 99f960e..d1ec48d 100644
--- a/lib/bouncycastle/BUCK
+++ b/lib/bouncycastle/BUCK
@@ -2,19 +2,19 @@
 
 # This version must match the version that also appears in
 # gerrit-pgm/src/main/resources/com/google/gerrit/pgm/libraries.config
-VERSION = '1.49'
+VERSION = '1.51'
 
 maven_jar(
   name = 'bcprov',
   id = 'org.bouncycastle:bcprov-jdk15on:' + VERSION,
-  sha1 = 'f5155f04330459104b79923274db5060c1057b99',
+  sha1 = '9ab8afcc2842d5ef06eb775a0a2b12783b99aa80',
   license = 'DO_NOT_DISTRIBUTE', #'bouncycastle'
 )
 
 maven_jar(
   name = 'bcpg',
   id = 'org.bouncycastle:bcpg-jdk15on:' + VERSION,
-  sha1 = '081d84be5b125e1997ab0e2244d1a2276b5de76c',
+  sha1 = 'b5fa4c280dfbf8bf7c260bc1e78044c7a1de5133',
   license = 'DO_NOT_DISTRIBUTE', #'bouncycastle'
   deps = [':bcprov'],
 )
@@ -22,7 +22,7 @@
 maven_jar(
   name = 'bcpkix',
   id = 'org.bouncycastle:bcpkix-jdk15on:' + VERSION,
-  sha1 = '924cc7ad2f589630c97b918f044296ebf1bb6855',
+  sha1 = '6c8c1f61bf27a09f9b1a8abc201523669bba9597',
   license = 'DO_NOT_DISTRIBUTE', #'bouncycastle'
   deps = [':bcprov'],
 )
diff --git a/lib/codemirror/BUCK b/lib/codemirror/BUCK
index 1ba2d92..e8539c6 100644
--- a/lib/codemirror/BUCK
+++ b/lib/codemirror/BUCK
@@ -1,5 +1,6 @@
 include_defs('//lib/maven.defs')
 include_defs('//lib/codemirror/cm3.defs')
+include_defs('//lib/codemirror/closure.defs')
 
 VERSION = '28a638a984'
 SHA1 = '68f8f136092a5965778186fb401a33be34cf73ed'
@@ -8,46 +9,52 @@
 ZIP = 'codemirror-%s.zip' % VERSION
 TOP = 'codemirror-%s' % VERSION
 
+CLOSURE_COMPILER_ARGS = [
+  '--compilation_level SIMPLE_OPTIMIZATIONS',
+  '--warning_level QUIET'
+]
+
 genrule(
   name = 'css',
   cmd = ';'.join([
       ':>$OUT',
       "echo '/** @license' >>$OUT",
-      'unzip -p $SRCDIR/%s %s/LICENSE >>$OUT' % (ZIP, TOP),
+      'unzip -p $(location :zip) %s/LICENSE >>$OUT' % TOP,
       "echo '*/' >>$OUT",
     ] +
-    ['unzip -p $SRCDIR/%s %s/%s >>$OUT' % (ZIP, TOP, n)
+    ['unzip -p $(location :zip) %s/%s >>$OUT' % (TOP, n)
      for n in CM3_CSS + CM3_THEMES]
   ),
-  srcs = [genfile(ZIP)],
-  deps = [':download'],
+  deps = [':zip'],
   out = 'cm3.css',
 )
 
-# TODO(sop) Minify with Closure JavaScript compiler.
 genrule(
-  name = 'js',
+  name = 'cm3-verbose',
   cmd = ';'.join([
       ':>$OUT',
       "echo '/** @license' >>$OUT",
-      'unzip -p $SRCDIR/%s %s/LICENSE >>$OUT' % (ZIP, TOP),
+      'unzip -p $(location :zip) %s/LICENSE >>$OUT' % TOP,
       "echo '*/' >>$OUT",
     ] +
-    ['unzip -p $SRCDIR/%s %s/%s >>$OUT' % (ZIP, TOP, n)
+    ['unzip -p $(location :zip) %s/%s >>$OUT' % (TOP, n)
      for n in CM3_JS]
   ),
-  srcs = [genfile(ZIP)],
-  deps = [':download'],
-  out = 'cm3.js',
+  deps = [':zip'],
+  out = 'cm3-verbose.js',
+)
+
+js_minify(
+  name = 'js',
+  generated = [':cm3-verbose'],
+  compiler_args = CLOSURE_COMPILER_ARGS,
+  out = 'cm3.js'
 )
 
 prebuilt_jar(
   name = 'codemirror',
-  binary_jar = genfile('codemirror.jar'),
-  deps = [
-    ':jar',
-    '//lib:LICENSE-codemirror',
-  ],
+  binary_jar = ':jar',
+  deps = ['//lib:LICENSE-codemirror'],
   visibility = ['PUBLIC'],
 )
 
@@ -55,35 +62,28 @@
   name = 'jar',
   cmd = ';'.join([
     'cd $TMP',
-    'unzip -q $SRCDIR/%s %s' % (
-      ZIP,
-      ' '.join(['%s/mode/%s' % (TOP, n) for n in CM3_MODES])),
+    'unzip -q $(location :zip) %s' %
+    ' '.join(['%s/mode/%s' % (TOP, n) for n in CM3_MODES]),
+    ';'.join(['$(exe :js_minifier) ' +
+    ' '.join(CLOSURE_COMPILER_ARGS) +
+    ' --js_output_file %s/mode/%s.min --js %s/mode/%s'
+    % (TOP, n, TOP, n) for n in CM3_MODES]),
+    ';'.join(['mv %s/mode/%s.min %s/mode/%s' % (TOP, n, TOP, n) for n in CM3_MODES]),
     'mkdir net',
     'mv %s net/codemirror' % TOP,
     'mkdir net/codemirror/lib',
-    'mv $SRCDIR/cm3.css net/codemirror/lib',
-    'mv $SRCDIR/cm3.js net/codemirror/lib',
+    'cp $(location :css) net/codemirror/lib',
+    'cp $(location :js) net/codemirror/lib',
     'zip -qr $OUT *'
   ]),
-  srcs = [
-    genfile(ZIP),
-    genfile('cm3.css'),
-    genfile('cm3.js'),
-  ],
-  deps = [
-    ':download',
-    ':css',
-    ':js',
-  ],
   out = 'codemirror.jar',
 )
 
 genrule(
-  name = 'download',
+  name = 'zip',
   cmd = '$(exe //tools:download_file)' +
     ' -o $OUT' +
     ' -u ' + URL +
     ' -v ' + SHA1,
-  deps = ['//tools:download_file'],
-  out = 'codemirror-' + VERSION + '.zip',
+  out = ZIP,
 )
diff --git a/lib/codemirror/closure.defs b/lib/codemirror/closure.defs
new file mode 100644
index 0000000..e602b9f
--- /dev/null
+++ b/lib/codemirror/closure.defs
@@ -0,0 +1,50 @@
+# https://code.google.com/p/closure-compiler/wiki/BinaryDownloads?tm=2
+CLOSURE_VERSION = '20140407'
+CLOSURE_COMPILER_URL = 'http://dl.google.com/closure-compiler/compiler-%s.zip' % CLOSURE_VERSION
+COMPILER = 'compiler.jar'
+CLOSURE_COMPILER_SHA1 = 'eeb02bfd45eb4a080b66dd423eaee4bdd1d674e9'
+
+def js_minify(
+    name,
+    out,
+    compiler_args = [],
+    srcs = [],
+    generated = []):
+  cmd = ['$(exe :js_minifier) --js_output_file $OUT'] + compiler_args
+  if srcs:
+    cmd.append('$SRCS')
+  if generated:
+    cmd.extend(['$(location %s)' % n for n in generated])
+
+  genrule(
+    name = name,
+    cmd = ' '.join(cmd),
+    srcs = srcs,
+    out = out,
+)
+
+java_binary(
+  name = 'js_minifier',
+  main_class = 'com.google.javascript.jscomp.CommandLineRunner',
+  deps = [':compiler-jar']
+)
+
+prebuilt_jar(
+  name = 'compiler-jar',
+  binary_jar = ':compiler',
+)
+
+genrule(
+  name = 'compiler',
+  cmd = 'unzip -p $(location :closure-compiler-zip) %s >$OUT' % COMPILER,
+  out = COMPILER,
+)
+
+genrule(
+  name = 'closure-compiler-zip',
+  cmd = '$(exe //tools:download_file)' +
+    ' -o $OUT' +
+    ' -u ' + CLOSURE_COMPILER_URL +
+    ' -v ' + CLOSURE_COMPILER_SHA1,
+  out = 'closure-compiler.zip',
+)
diff --git a/lib/commons/BUCK b/lib/commons/BUCK
index ab8a036..85e404f 100644
--- a/lib/commons/BUCK
+++ b/lib/commons/BUCK
@@ -15,10 +15,6 @@
   license = 'Apache2.0',
   exclude = ['META-INF/LICENSE.txt', 'META-INF/NOTICE.txt'],
   attach_source = False,
-  visibility = [
-    '//lib:velocity',
-    '//lib/solr:zookeeper',
-  ],
 )
 
 maven_jar(
@@ -93,9 +89,9 @@
 
 maven_jar(
   name = 'httpclient',
-  id = 'org.apache.httpcomponents:httpclient:4.2.5',
-  bin_sha1 = '666e26e76f2e87d84e4f16acb546481ae1b8e9a6',
-  src_sha1 = '55d345272944d7e8dace47925336a3764ee0e24b',
+  id = 'org.apache.httpcomponents:httpclient:4.3.4',
+  bin_sha1 = 'a9a1fef2faefed639ee0d0fba5b3b8e4eb2ff2d8',
+  src_sha1 = '7a14aafed8c5e2c4e360a2c1abd1602efa768b1f',
   license = 'Apache2.0',
   deps = [
     ':codec',
@@ -106,16 +102,16 @@
 
 maven_jar(
   name = 'httpcore',
-  id = 'org.apache.httpcomponents:httpcore:4.2.4',
-  bin_sha1 = '3b7f38df6de5dd8b500e602ae8c2dd5ee446f883',
-  src_sha1 = 'c3ffe3a73348645042fb0b06303b6a3de194494e',
+  id = 'org.apache.httpcomponents:httpcore:4.3.2',
+  bin_sha1 = '31fbbff1ddbf98f3aa7377c94d33b0447c646b6e',
+  src_sha1 = '4809f38359edeea9487f747e09aa58ec8d3a54c5',
   license = 'Apache2.0',
 )
 
 maven_jar(
   name = 'httpmime',
-  id = 'org.apache.httpcomponents:httpmime:4.2.5',
-  bin_sha1 = '412b9914d0adec6d5716df1ada8acbc4f6f2dd37',
-  src_sha1 = 'c07ce7f6b153284a9ebaf58532c2442200cf3aa2',
+  id = 'org.apache.httpcomponents:httpmime:4.3.4',
+  bin_sha1 = '54ffde537682aea984c22fbcf0106f21397c5f9b',
+  src_sha1 = '0651e21152b0963661068f948d84ed08c18094f8',
   license = 'Apache2.0',
 )
diff --git a/lib/easymock/BUCK b/lib/easymock/BUCK
new file mode 100644
index 0000000..5ab30d2
--- /dev/null
+++ b/lib/easymock/BUCK
@@ -0,0 +1,30 @@
+include_defs('//lib/maven.defs')
+
+maven_jar(
+  name = 'easymock',
+  id = 'org.easymock:easymock:3.2',
+  sha1 = '00c82f7fa3ef377d8954b1db25123944b5af2ba4',
+  license = 'DO_NOT_DISTRIBUTE',
+  deps = [
+    ':cglib-2_2',
+    ':objenesis',
+  ],
+)
+
+maven_jar(
+  name = 'cglib-2_2',
+  id = 'cglib:cglib-nodep:2.2.2',
+  sha1 = '00d456bb230c70c0b95c76fb28e429d42f275941',
+  license = 'DO_NOT_DISTRIBUTE',
+  attach_source = False,
+)
+
+maven_jar(
+  name = 'objenesis',
+  id = 'org.objenesis:objenesis:1.2',
+  sha1 = 'bfcb0539a071a4c5a30690388903ac48c0667f2a',
+  license = 'DO_NOT_DISTRIBUTE',
+  visibility = ['//lib/powermock:powermock-reflect'],
+  attach_source = False,
+)
+
diff --git a/lib/guice/BUCK b/lib/guice/BUCK
index 946bf9d..703573e 100644
--- a/lib/guice/BUCK
+++ b/lib/guice/BUCK
@@ -1,6 +1,6 @@
 include_defs('//lib/maven.defs')
 
-VERSION = '4.0-beta'
+VERSION = '4.0-beta5'
 EXCLUDE = [
   'META-INF/DEPENDENCIES',
   'META-INF/LICENSE',
@@ -19,7 +19,7 @@
 maven_jar(
   name = 'guice_library',
   id = 'com.google.inject:guice:' + VERSION,
-  sha1 = 'a82be989679df08b66d48b42659a3ca2daaf1d5b',
+  sha1 = 'fdf5df843620978a6f2929fd56f719a20d713c2b',
   license = 'Apache2.0',
   deps = [':aopalliance'],
   exclude_java_sources = True,
@@ -33,7 +33,7 @@
 maven_jar(
   name = 'guice-assistedinject',
   id = 'com.google.inject.extensions:guice-assistedinject:' + VERSION,
-  sha1 = 'abd6511011a9e4b64e2ebb60caac2e1cd6cd19a1',
+  sha1 = '820f10e0650cd9ed2591f398937df50f330b147d',
   license = 'Apache2.0',
   deps = [':guice'],
   exclude = EXCLUDE,
@@ -42,7 +42,7 @@
 maven_jar(
   name = 'guice-servlet',
   id = 'com.google.inject.extensions:guice-servlet:' + VERSION,
-  sha1 = '46b44984f254c0bf92d0c972fad1a70292ada28e',
+  sha1 = '852af296c8a06aac968d17491fd8c1eab1ec8b10',
   license = 'Apache2.0',
   deps = [':guice'],
   exclude = EXCLUDE,
diff --git a/lib/gwt/BUCK b/lib/gwt/BUCK
index 537c6d8..8d2b718 100644
--- a/lib/gwt/BUCK
+++ b/lib/gwt/BUCK
@@ -1,11 +1,11 @@
 include_defs('//lib/maven.defs')
 
-VERSION = '2.6.0'
+VERSION = '2.6.1'
 
 maven_jar(
   name = 'user',
   id = 'com.google.gwt:gwt-user:' + VERSION,
-  sha1 = '8237d1bd37127188107388c8724be44a0e9f73ca',
+  sha1 = 'c078b1b8cc0281214b0eb458d2c283d039374fad',
   license = 'Apache2.0',
   attach_source = False,
 )
@@ -13,7 +13,7 @@
 maven_jar(
   name = 'dev',
   id = 'com.google.gwt:gwt-dev:' + VERSION,
-  sha1 = 'af3d9ad2fb8be30dc87fdcd6d9a373b2ab675802',
+  sha1 = 'db237e4be0aa1fe43425d2c51ab5485dba211ddd',
   license = 'Apache2.0',
   deps = [
     ':javax-validation',
@@ -32,12 +32,6 @@
   visibility = [],
 )
 
-python_binary(
-  name = 'compiler',
-  main = 'compiler.py',
-  visibility = ['PUBLIC'],
-)
-
 maven_jar(
   name = 'gwt-test-utils',
   id = 'com.googlecode.gwt-test-utils:gwt-test-utils:0.47',
diff --git a/lib/gwt/compiler.py b/lib/gwt/compiler.py
deleted file mode 100755
index f7b478c..0000000
--- a/lib/gwt/compiler.py
+++ /dev/null
@@ -1,65 +0,0 @@
-#!/usr/bin/python
-# 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.
-
-from __future__ import print_function
-
-from multiprocessing import cpu_count
-from os import makedirs, mkdir, path
-from subprocess import Popen, PIPE
-from sys import argv, stderr
-
-cp, opt, end = [], [], False
-module, TMP, outzip = argv[1], argv[2], argv[3]
-
-for a in argv[4:]:
-  if end:
-    if a.endswith('.jar'):
-      cp.append(path.expandvars(a))
-  elif a == '--':
-    end = True
-  else:
-    opt.append(a)
-
-if not outzip.endswith('.zip'):
-  print("%s must end with .zip" % outzip, file=stderr)
-  exit(1)
-
-for d in ['deploy', 'unit_cache', 'work']:
-  mkdir(path.join(TMP, d))
-if not path.exists(path.dirname(outzip)):
-  makedirs(path.dirname(outzip))
-
-cmd = [
-  'java', '-Xmx512m',
-  '-Djava.io.tmpdir=' + TMP,
-  '-Dgwt.normalizeTimestamps=true',
-  '-Dgwt.persistentunitcachedir=' + path.join(TMP, 'unit_cache'),
-  '-classpath', ':'.join(cp),
-  'com.google.gwt.dev.Compiler',
-  '-deploy', path.join(TMP, 'deploy'),
-  '-workDir', path.join(TMP, 'work'),
-  '-war', outzip,
-  '-localWorkers', str(cpu_count()),
-] + opt + [module]
-
-try:
-  gwt = Popen(cmd, stdout=PIPE, stderr=PIPE)
-  out, err = gwt.communicate()
-  if gwt.returncode != 0:
-    print(out + err, file=stderr)
-    exit(gwt.returncode)
-except KeyboardInterrupt:
-  print("Interrupted by user", file=stderr)
-  exit(1)
diff --git a/lib/jetty/BUCK b/lib/jetty/BUCK
index 9637598..13e9774 100644
--- a/lib/jetty/BUCK
+++ b/lib/jetty/BUCK
@@ -1,12 +1,12 @@
 include_defs('//lib/maven.defs')
 
-VERSION = '9.1.0.v20131115'
+VERSION = '9.2.1.v20140609'
 EXCLUDE = ['about.html']
 
 maven_jar(
   name = 'servlet',
   id = 'org.eclipse.jetty:jetty-servlet:' + VERSION,
-  sha1 = 'bf92dbb4426e5d14973726861accda38d93baa64',
+  sha1 = 'f2327faaf09a3f306babc209f9a7ae01b1528464',
   license = 'Apache2.0',
   deps = [
     ':security',
@@ -18,7 +18,7 @@
 maven_jar(
   name = 'security',
   id = 'org.eclipse.jetty:jetty-security:' + VERSION,
-  sha1 = '0ec8b3000720c746ba03d96a2172c88124509c57',
+  sha1 = '8ac8cc9e5c66eb6022cbe80f4e22d4e42dc5e643',
   license = 'Apache2.0',
   deps = [':server'],
   exclude = EXCLUDE,
@@ -28,7 +28,7 @@
 maven_jar(
   name = 'webapp',
   id = 'org.eclipse.jetty:jetty-webapp:' + VERSION,
-  sha1 = '3d98b3197fbe453a8df27c106f12363587439ee3',
+  sha1 = '906e0f4ba7a0cebb8af61513c8511981ba2ccf6e',
   license = 'Apache2.0',
   deps = [':xml'],
   exclude = EXCLUDE,
@@ -41,7 +41,7 @@
 maven_jar(
   name = 'xml',
   id = 'org.eclipse.jetty:jetty-xml:' + VERSION,
-  sha1 = '67e8618447a7740b7a95d74d1b6b4b5c8c1024f5',
+  sha1 = '0d589789eb98d31160d11413b6323b9ea4569046',
   license = 'Apache2.0',
   exclude = EXCLUDE,
   visibility = [],
@@ -50,7 +50,19 @@
 maven_jar(
   name = 'server',
   id = 'org.eclipse.jetty:jetty-server:' + VERSION,
-  sha1 = 'c64cb3ab62ff32fcd8b838369a426c688d901103',
+  sha1 = 'd02c51c4f8eec3174b09b6e978feaaf05c3dc4ea',
+  license = 'Apache2.0',
+  exported_deps = [
+    ':continuation',
+    ':http',
+  ],
+  exclude = EXCLUDE,
+)
+
+maven_jar(
+  name = 'jmx',
+  id = 'org.eclipse.jetty:jetty-jmx:' + VERSION,
+  sha1 = '1258d5ac618b120026da8a82283e6cb8ff4638a6',
   license = 'Apache2.0',
   exported_deps = [
     ':continuation',
@@ -62,7 +74,7 @@
 maven_jar(
   name = 'continuation',
   id = 'org.eclipse.jetty:jetty-continuation:' + VERSION,
-  sha1 = '5751f7ea38488dd32180bd3273f7f8591928aee3',
+  sha1 = 'e5bf20cdcd9c2878677f3c0f43baea2725f8c59e',
   license = 'Apache2.0',
   exclude = EXCLUDE,
 )
@@ -70,7 +82,7 @@
 maven_jar(
   name = 'http',
   id = 'org.eclipse.jetty:jetty-http:' + VERSION,
-  sha1 = '1ee35683e75298b3fe246befd1cd88b6e6087427',
+  sha1 = 'a132617cb898afc9d4ce5d586e11ad90b9831fff',
   license = 'Apache2.0',
   exported_deps = [':io'],
   exclude = EXCLUDE,
@@ -79,7 +91,7 @@
 maven_jar(
   name = 'io',
   id = 'org.eclipse.jetty:jetty-io:' + VERSION,
-  sha1 = '99e4632a0760f5fb2a110d25df992faf959700d6',
+  sha1 = '8465fe92159632e9f0a1bfe6951dba8163ac0b12',
   license = 'Apache2.0',
   exported_deps = [':util'],
   exclude = EXCLUDE,
@@ -89,7 +101,7 @@
 maven_jar(
   name = 'util',
   id = 'org.eclipse.jetty:jetty-util:' + VERSION,
-  sha1 = '440fc44218366a7b58739aef4402b4927e135b9c',
+  sha1 = '4ae7ac5d3cfcb21bc288dd3f4ec3ba2823442f0d',
   license = 'Apache2.0',
   exclude = EXCLUDE,
   visibility = [],
diff --git a/lib/jgit/BUCK b/lib/jgit/BUCK
index 9a94085..8af81c7 100644
--- a/lib/jgit/BUCK
+++ b/lib/jgit/BUCK
@@ -9,9 +9,9 @@
   bin_sha1 = '1dac8dd7deb4ec72939fe30cd6fd57c22fd4a403',
   src_sha1 = '6c5fe5a2bd6b12571d15984916463f2f28223a93',
   license = 'jgit',
+  repository = REPO,
   unsign = True,
   deps = [':ewah'],
-  repository = REPO,
   exclude = [
     'META-INF/eclipse.inf',
     'about.html',
@@ -24,8 +24,8 @@
   id = 'org.eclipse.jgit:org.eclipse.jgit.http.server:' + VERS,
   sha1 = '6498fa4f4bd5db11d3069952540b68a9aef024c2',
   license = 'jgit',
-  deps = [':jgit'],
   repository = REPO,
+  deps = [':jgit'],
   unsign = True,
   exclude = [
     'about.html',
@@ -38,11 +38,11 @@
   id = 'org.eclipse.jgit:org.eclipse.jgit.archive:' + VERS,
   sha1 = 'd64327d788ae43d79eb4e42d2432646c7b485789',
   license = 'jgit',
+  repository = REPO,
   deps = [':jgit',
     '//lib/commons:compress',
     '//lib:tukaani-xz',
   ],
-  repository = REPO,
   unsign = True,
   exclude = [
     'about.html',
@@ -55,9 +55,9 @@
   id = 'org.eclipse.jgit:org.eclipse.jgit.junit:' + VERS,
   sha1 = '50c36e367e7df961c1acc9308b7d52ea21a73d91',
   license = 'DO_NOT_DISTRIBUTE',
+  repository = REPO,
   unsign = True,
   deps = [':jgit'],
-  repository = REPO,
 )
 
 maven_jar(
@@ -69,17 +69,15 @@
 
 prebuilt_jar(
   name = 'Edit',
-  binary_jar = genfile('edit-src.jar'),
-  deps = [':jgit_edit_src'],
+  binary_jar = ':jgit_edit_src',
   visibility = ['PUBLIC'],
 )
 
 genrule(
   name = 'jgit_edit_src',
-  cmd = 'unzip -qd $TMP $SRCS org/eclipse/jgit/diff/Edit.java;' +
+  cmd = 'unzip -qd $TMP $(location :jgit_src) ' +
+    'org/eclipse/jgit/diff/Edit.java;' +
     'cd $TMP;' +
     'zip -Dq $OUT org/eclipse/jgit/diff/Edit.java',
-  srcs = [genfile('jgit/org.eclipse.jgit-%s-src.jar' % VERS)],
   out = 'edit-src.jar',
-  deps = [':jgit_src']
 )
diff --git a/lib/log/BUCK b/lib/log/BUCK
index 2659fcd..cadc7e7 100644
--- a/lib/log/BUCK
+++ b/lib/log/BUCK
@@ -1,31 +1,33 @@
 include_defs('//lib/maven.defs')
 
+VER = '1.7.7'
+
 maven_jar(
   name = 'api',
-  id = 'org.slf4j:slf4j-api:1.6.1',
-  sha1 = '6f3b8a24bf970f17289b234284c94f43eb42f0e4',
+  id = 'org.slf4j:slf4j-api:' + VER,
+  sha1 = '2b8019b6249bb05d81d3a3094e468753e2b21311',
   license = 'slf4j',
 )
 
 maven_jar(
   name = 'impl_log4j',
-  id = 'org.slf4j:slf4j-log4j12:1.6.1',
-  sha1 = 'bd245d6746cdd4e6203e976e21d597a46f115802',
+  id = 'org.slf4j:slf4j-log4j12:' + VER,
+  sha1 = '58f588119ffd1702c77ccab6acb54bfb41bed8bd',
   license = 'slf4j',
   deps = [':log4j'],
 )
 
 maven_jar(
-  name = 'log4j',
-  id = 'log4j:log4j:1.2.16',
-  sha1 = '7999a63bfccbc7c247a9aea10d83d4272bd492c6',
-  license = 'Apache2.0',
-  exclude = ['META-INF/LICENSE', 'META-INF/NOTICE'],
+  name = 'jcl-over-slf4j',
+  id = 'org.slf4j:jcl-over-slf4j:' + VER,
+  sha1 = '56003dcd0a31deea6391b9e2ef2f2dc90b205a92',
+  license = 'slf4j',
 )
 
 maven_jar(
-  name = 'jcl-over-slf4j',
-  id = 'org.slf4j:jcl-over-slf4j:1.6.1',
-  sha1 = '99c61095a14dfc9e47a086068033c286bf236475',
-  license = 'slf4j',
+  name = 'log4j',
+  id = 'log4j:log4j:1.2.17',
+  sha1 = '5af35056b4d257e4b64b9e8069c0746e8b08629f',
+  license = 'Apache2.0',
+  exclude = ['META-INF/LICENSE', 'META-INF/NOTICE'],
 )
diff --git a/lib/lucene/BUCK b/lib/lucene/BUCK
index 450a88a..9ccc5aa 100644
--- a/lib/lucene/BUCK
+++ b/lib/lucene/BUCK
@@ -1,12 +1,11 @@
 include_defs('//lib/maven.defs')
 
-VERSION = '4.6.0'
+VERSION = '4.8.1'
 
 maven_jar(
   name = 'core',
   id = 'org.apache.lucene:lucene-core:' + VERSION,
-  bin_sha1 = 'f1d974facaea30a3a0c1752a24097af5a7d40e60',
-  src_sha1 = '19d4eb5def4bc2517a00b50f7a875b7ce33988a7',
+  sha1 = 'a549eef6316a2c38d4cda932be809107deeaf8a7',
   license = 'Apache2.0',
   exclude = [
     'META-INF/LICENSE.txt',
@@ -17,8 +16,7 @@
 maven_jar(
   name = 'analyzers-common',
   id = 'org.apache.lucene:lucene-analyzers-common:' + VERSION,
-  bin_sha1 = '25dda6706bcb7a741f25f57cdbec6c2f36adc557',
-  src_sha1 = '04866d0e36e3ef708d099014752ad4fef61d4243',
+  sha1 = '6e3731524351c83cd21022a23bee5e87f0575555',
   license = 'Apache2.0',
   exclude = [
     'META-INF/LICENSE.txt',
@@ -29,7 +27,6 @@
 maven_jar(
   name = 'query-parser',
   id = 'org.apache.lucene:lucene-queryparser:' + VERSION,
-  bin_sha1 = 'ef35f1eb55e50725777162e376e7b5222f45f7fa',
-  src_sha1 = 'e99e3b298e83461c03ef6eb66ab9798a4b712dc6',
+  sha1 = 'f3e105d74137906fdeb2c7bc4dd68c08564778f9',
   license = 'Apache2.0',
 )
diff --git a/lib/maven.defs b/lib/maven.defs
index 1b36fdb..2ea61f3 100644
--- a/lib/maven.defs
+++ b/lib/maven.defs
@@ -16,7 +16,6 @@
 ECLIPSE = 'ECLIPSE:'
 GERRIT = 'GERRIT:'
 GERRIT_API = 'GERRIT_API:'
-ECLIPSE = 'ECLIPSE:'
 MAVEN_CENTRAL = 'MAVEN_CENTRAL:'
 MAVEN_LOCAL = 'MAVEN_LOCAL:'
 
@@ -42,7 +41,8 @@
     sha1 = '', bin_sha1 = '', src_sha1 = '',
     repository = MAVEN_CENTRAL,
     attach_source = True,
-    visibility = ['PUBLIC']):
+    visibility = ['PUBLIC'],
+    local_license = False):
   from os import path
 
   parts = id.split(':')
@@ -50,6 +50,12 @@
     raise NameError('expected id="groupId:artifactId:version"')
   group, artifact, version = parts
 
+  # SNAPSHOT artifacts are handled differently on Google storage bucket:
+  # 'SNAPSHOT' is discarded from the directory name. However on other
+  # Maven repositories, most notable local repository located in
+  # ~/.m2/repository (and is supported through MAVEN_LOCAL repository)
+  # it must be preserved, otherwise the artifact wouldn't be found.
+  # Atm the SNAPSHOT part is only discarded for Google storage bucket.
   if 'SNAPSHOT' in version and repository.startswith(GERRIT):
     file_version = version.replace('-SNAPSHOT', '')
     version = version.split('-SNAPSHOT')[0] + '-SNAPSHOT'
@@ -81,43 +87,44 @@
     cmd.append('--unsign')
 
   genrule(
-    name = name + '__download_bin',
+    name = '%s__download_bin' % name,
     cmd = ' '.join(cmd),
-    deps = ['//tools:download_file'],
     out = binjar,
   )
-  license = ['//lib:LICENSE-' + license]
+  license = ':LICENSE-' + license
+  if not local_license:
+    license = '//lib' + license
+  license = [license]
 
   if src_sha1 or attach_source:
     cmd = ['$(exe //tools:download_file)', '-o', '$OUT', '-u', srcurl]
     if src_sha1:
       cmd.extend(['-v', src_sha1])
     genrule(
-      name = name + '__download_src',
+      name = '%s__download_src' % name,
       cmd = ' '.join(cmd),
-      deps = ['//tools:download_file'],
       out = srcjar,
     )
     prebuilt_jar(
-      name = name + '_src',
-      binary_jar = genfile(srcjar),
-      deps = license + [':' + name + '__download_src'],
+      name = '%s_src' % name,
+      binary_jar = ':%s__download_src' % name,
+      deps = license,
       visibility = visibility,
     )
   else:
     srcjar = None
     genrule(
-      name = name + '__download_src',
+      name = '%s__download_src' % name,
       cmd = ':>$OUT',
-      out = '__' + name + '__no_src',
+      out = '__%s__no_src' % name,
     )
 
   if exported_deps:
     prebuilt_jar(
-      name = name + '__jar',
-      deps = deps + license + [':' + name + '__download_bin'],
-      binary_jar = genfile(binjar),
-      source_jar = genfile(srcjar) if srcjar else None,
+      name = '%s__jar' % name,
+      deps = deps + license,
+      binary_jar = ':%s__download_bin' % name,
+      source_jar = ':%s__download_src' % name if srcjar else None,
     )
     java_library(
       name = name,
@@ -127,9 +134,9 @@
   else:
     prebuilt_jar(
       name = name,
-      deps = deps + license + [':' + name + '__download_bin'],
-      binary_jar = genfile(binjar),
-      source_jar = genfile(srcjar) if srcjar else None,
+      deps = deps + license,
+      binary_jar = ':%s__download_bin' % name,
+      source_jar = ':%s__download_src' % name if srcjar else None,
       visibility = visibility,
     )
 
@@ -142,18 +149,17 @@
   binjar = name + '.jar'
   srcjar = name + '-src.jar'
   genrule(
-    name = name + '__local_bin',
+    name = '%s__local_bin' % name,
     cmd = 'ln -s %s $OUT' % jar,
     out = binjar)
   if src:
     genrule(
-      name = name + '__local_src',
+      name = '%s__local_src' % name,
       cmd = 'ln -s %s $OUT' % src,
       out = srcjar)
     prebuilt_jar(
-      name = name + '_src',
-      deps = [':' + name + '__local_src'],
-      binary_jar = genfile(srcjar),
+      name = '%s_src' % name,
+      binary_jar = ':%s__local_src' % name,
       visibility = visibility,
     )
   else:
@@ -161,8 +167,8 @@
 
   prebuilt_jar(
     name = name,
-    deps = deps + [':' + name + '__local_bin'],
-    binary_jar = genfile(binjar),
-    source_jar = genfile(srcjar) if srcjar else None,
+    deps = deps,
+    binary_jar = ':%s__local_bin' % name,
+    source_jar = ':%s__local_src' % name if srcjar else None,
     visibility = visibility,
   )
diff --git a/lib/mina/BUCK b/lib/mina/BUCK
index d866807..0c9b41e 100644
--- a/lib/mina/BUCK
+++ b/lib/mina/BUCK
@@ -8,18 +8,17 @@
 
 maven_jar(
   name = 'sshd',
-  id = 'org.apache.sshd:sshd-core:0.9.0-4-g5967cfd',
-  sha1 = '449ec11c4417b295dbf1661585a50c6ec7d9a452',
+  id = 'org.apache.sshd:sshd-core:0.14.0',
+  sha1 = 'cb12fa1b1b07fb5ce3aa4f99b189743897bd4fca',
   license = 'Apache2.0',
   deps = [':core'],
   exclude = EXCLUDE,
-  repository = GERRIT,
 )
 
 maven_jar(
   name = 'core',
-  id = 'org.apache.mina:mina-core:2.0.7',
-  sha1 = 'c878e2aa82de748474a624ec3933e4604e446dec',
+  id = 'org.apache.mina:mina-core:2.0.8',
+  sha1 = 'd6ff69fa049aeaecdf0c04cafbb1ab53b7487883',
   license = 'Apache2.0',
   exclude = EXCLUDE,
 )
diff --git a/lib/ow2/BUCK b/lib/ow2/BUCK
new file mode 100644
index 0000000..81d1618
--- /dev/null
+++ b/lib/ow2/BUCK
@@ -0,0 +1,32 @@
+include_defs('//lib/maven.defs')
+
+VERSION = '4.1'
+
+maven_jar(
+  name = 'ow2-asm',
+  id = 'org.ow2.asm:asm:' + VERSION,
+  sha1 = 'ad568238ee36a820bd6c6806807e8a14ea34684d',
+  license = 'ow2',
+)
+
+maven_jar(
+  name = 'ow2-asm-analysis',
+  id = 'org.ow2.asm:asm-analysis:' + VERSION,
+  sha1 = '73401033069e4714f57b60aeae02f97210aaa64e',
+  license = 'ow2',
+)
+
+maven_jar(
+  name = 'ow2-asm-tree',
+  id = 'org.ow2.asm:asm-tree:' + VERSION,
+  sha1 = '51085abcc4cb6c6e1cb5551e6f999eb8e31c5b2d',
+  license = 'ow2',
+)
+
+maven_jar(
+  name = 'ow2-asm-util',
+  id = 'org.ow2.asm:asm-util:' + VERSION,
+  sha1 = '6344065cb0f94e2b930a95e6656e040ebc11df08',
+  license = 'ow2',
+)
+
diff --git a/lib/powermock/BUCK b/lib/powermock/BUCK
new file mode 100644
index 0000000..3a2eeac
--- /dev/null
+++ b/lib/powermock/BUCK
@@ -0,0 +1,72 @@
+include_defs('//lib/maven.defs')
+
+VERSION = '1.5'
+
+maven_jar(
+  name = 'powermock-module-junit4',
+  id = 'org.powermock:powermock-module-junit4:' + VERSION,
+  sha1 = '9f6f8d0485249171f9d870e2b269048fa8cad43b',
+  license = 'DO_NOT_DISTRIBUTE',
+  deps = [
+    ':powermock-module-junit4-common',
+    '//lib:junit',
+  ],
+)
+
+maven_jar(
+  name = 'powermock-module-junit4-common',
+  id = 'org.powermock:powermock-module-junit4-common:' + VERSION,
+  sha1 = '43db4720ff57af42a1bd5c73fb5cdfebeebd564c',
+  license = 'DO_NOT_DISTRIBUTE',
+  deps = [
+    ':powermock-reflect',
+    '//lib:junit',
+  ],
+)
+
+maven_jar(
+  name = 'powermock-reflect',
+  id = 'org.powermock:powermock-reflect:' + VERSION,
+  sha1 = '8df1548eeabb8492ba97d4f3eb84ae4d5f69215e',
+  license = 'DO_NOT_DISTRIBUTE',
+  deps = [
+    '//lib:junit',
+    '//lib/easymock:objenesis',
+  ],
+)
+
+maven_jar(
+  name = 'powermock-api-easymock',
+  id = 'org.powermock:powermock-api-easymock:' + VERSION,
+  sha1 = 'a485b570b9debb46b53459a8e866a40343b2cfe2',
+  license = 'DO_NOT_DISTRIBUTE',
+  deps = [
+    ':powermock-api-support',
+    '//lib/easymock:easymock',
+  ],
+)
+
+maven_jar(
+  name = 'powermock-api-support',
+  id = 'org.powermock:powermock-api-support:' + VERSION,
+  sha1 = '7c1b2e4555cfa333aec201c4612345c092820a38',
+  license = 'DO_NOT_DISTRIBUTE',
+  deps = [
+    ':powermock-core',
+    ':powermock-reflect',
+    '//lib:junit',
+  ],
+)
+
+maven_jar(
+  name = 'powermock-core',
+  id = 'org.powermock:powermock-core:' + VERSION,
+  sha1 = '4415337ff3fdb7ceb484f11fd08e39711e408976',
+  license = 'DO_NOT_DISTRIBUTE',
+  deps = [
+    ':powermock-reflect',
+    '//lib:javassist-3.17.1-GA',
+    '//lib:junit',
+  ],
+)
+
diff --git a/lib/prolog/prolog.defs b/lib/prolog/prolog.defs
index b91e2de..d0636f5 100644
--- a/lib/prolog/prolog.defs
+++ b/lib/prolog/prolog.defs
@@ -19,20 +19,16 @@
     visibility = []):
   genrule(
     name = name + '__pl2j',
-    cmd = 'cd $SRCDIR;$(exe //lib/prolog:compiler)' +
+    cmd = '$(exe //lib/prolog:compiler)' +
       ' $TMP $OUT ' +
       ' '.join(srcs),
     srcs = srcs,
-    deps = ['//lib/prolog:compiler'],
     out = name + '.src.zip',
   )
   java_library(
     name = name + '__lib',
-    srcs = [genfile(name + '.src.zip')],
-    deps = [
-      ':' + name + '__pl2j',
-      '//lib/prolog:prolog-cafe',
-    ] + deps,
+    srcs = [':' + name + '__pl2j'],
+    deps = ['//lib/prolog:prolog-cafe'] + deps,
   )
   genrule(
     name = name + '__ln',
@@ -42,7 +38,6 @@
   )
   prebuilt_jar(
     name = name,
-    binary_jar = genfile(name + '.jar'),
-    deps = [':%s__ln' % name],
+    binary_jar = ':%s__ln' % name,
     visibility = visibility,
   )
diff --git a/plugins/BUCK b/plugins/BUCK
index 480cd4c..134f275 100644
--- a/plugins/BUCK
+++ b/plugins/BUCK
@@ -28,11 +28,11 @@
     ';'.join(['echo >&2 plugins/'+n+' is required.' for n in NEED]) +
     (';echo >&2;exit 1;' if NEED else '') +
     'mkdir -p $TMP/WEB-INF/plugins;' +
-    'for s in $SRCS;do ln -s $s $TMP/WEB-INF/plugins;done;' +
+    'for s in ' +
+    ' '.join(['$(location //%s/%s:%s)' % (BASE, n, n) for n in HAVE]) +
+    ';do ln -s $s $TMP/WEB-INF/plugins;done;' +
     'cd $TMP;' +
     'zip -qr $OUT .',
-  srcs = [genfile('%s/%s.jar' % (n, n)) for n in HAVE],
-  deps = ['//%s/%s:%s' % (BASE, n, n) for n in HAVE],
   out = 'core.zip',
   visibility = ['//:release'],
 )
diff --git a/plugins/commit-message-length-validator b/plugins/commit-message-length-validator
index c4ac20c..a93b856 160000
--- a/plugins/commit-message-length-validator
+++ b/plugins/commit-message-length-validator
@@ -1 +1 @@
-Subproject commit c4ac20cd951f3efa85a04a272e84c54fac7de346
+Subproject commit a93b85656a68b6a71fe7cf0bb0cc4ed8143657bf
diff --git a/plugins/cookbook-plugin b/plugins/cookbook-plugin
index dda1b78..5ac8e34 160000
--- a/plugins/cookbook-plugin
+++ b/plugins/cookbook-plugin
@@ -1 +1 @@
-Subproject commit dda1b787dba031597b83db1eb3c1b57565059b68
+Subproject commit 5ac8e3475cc284bd1c2159cfded076c959462845
diff --git a/plugins/replication b/plugins/replication
index 7c25cd5..16db002 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 7c25cd575572410ef90a53d1af853fde7d011bf8
+Subproject commit 16db002a0bfe6559a6395dabf155b7a8bfadc968
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index 0fc4b12..4f5831f 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit 0fc4b128d290b20c4ef962d30529faae6e89a169
+Subproject commit 4f5831f1c319753ee4ef16c35778f49397985af9
diff --git a/tools/buck.defs b/tools/buck.defs
deleted file mode 100644
index 8bcacc3..0000000
--- a/tools/buck.defs
+++ /dev/null
@@ -1,65 +0,0 @@
-# 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.
-
-original_java_library = java_library
-def java_library(
-    name,
-    srcs=[],
-    resources=[],
-    source='7',
-    target='7',
-    proguard_config=None,
-    deps=[],
-    exported_deps=[],
-    visibility=[],
-    ):
-  original_java_library(
-    name=name,
-    srcs=srcs,
-    resources=resources,
-    source=source,
-    target=target,
-    proguard_config=proguard_config,
-    deps=deps,
-    exported_deps=exported_deps,
-    visibility=visibility,
-  )
-
-original_java_test = java_test
-def java_test(
-    name,
-    srcs=[],
-    labels=[],
-    resources=[],
-    source='7',
-    target='7',
-    vm_args=[],
-    source_under_test=[],
-    contacts=[],
-    deps=[],
-    visibility=[],
-    ):
-  original_java_test(
-    name=name,
-    srcs=srcs,
-    labels=labels,
-    resources=resources,
-    source=source,
-    target=target,
-    vm_args=vm_args,
-    source_under_test=source_under_test,
-    contacts=contacts,
-    deps=deps,
-    visibility=visibility,
-  )
diff --git a/tools/build.defs b/tools/build.defs
index af39c2a..8b858cd 100644
--- a/tools/build.defs
+++ b/tools/build.defs
@@ -14,12 +14,8 @@
 
 # These definitions support building a runnable version of Gerrit.
 
-DOCS_SRC = genfile('Documentation/html.zip')
+DOCS_HTML = '//Documentation:html'
 DOCS_LIB = '//Documentation:index_lib'
-DOCS_DEP = [
-  '//Documentation:html',
-  '//Documentation:index_lib',
-]
 LIBS = [
   '//gerrit-war:log4j-config',
   '//gerrit-war:init',
@@ -33,7 +29,7 @@
   deps = []
   for n in os.listdir('plugins'):
     if os.path.exists(os.path.join('plugins', n, 'BUCK')):
-      deps.append('//plugins/%s:%s__plugin__compile' % (n, n))
+      deps.append('//plugins/%s:%s__plugin' % (n, n))
   return deps
 
 def war(
@@ -50,30 +46,19 @@
   for l in pgmlibs:
     cmd.extend(['--pgmlib', l])
 
-  src = []
   dep = []
   if docs:
-    src.append(DOCS_SRC)
-    dep.extend(DOCS_DEP)
+    cmd.append('$(location %s)' % DOCS_HTML)
+    dep.append(DOCS_LIB)
     cmd.extend(['--lib', DOCS_LIB])
   if context:
-    root = get_base_path()
-    if root:
-      root = '/'.join(['..' for _ in root.split('/')]) + '/'
-    for r in context:
-      dep.append(r[:r.rindex('.')])
-      if r.startswith('//'):
-        r = root + r[2:]
-      r = r.replace(':', '/')
-      src.append(genfile(r))
-  if src:
-    cmd.append('$SRCS')
+    for t in context:
+      cmd.append('$(location %s)' % t)
 
   genrule(
     name = name,
     cmd = ' '.join(cmd),
-    srcs = src,
-    deps = libs + pgmlibs + dep + ['//tools:pack_war'],
+    deps = libs + pgmlibs + dep,
     out = name + '.war',
     visibility = visibility,
   )
@@ -84,9 +69,9 @@
     libs = LIBS + ['//gerrit-war:version'],
     pgmlibs = PGMLIBS,
     context = [
-      '//gerrit-main:main_bin.jar',
-      '//gerrit-war:webapp_assets.zip',
-      '//gerrit-gwtui:' + ui + '.zip',
+      '//gerrit-main:main_bin',
+      '//gerrit-war:webapp_assets',
+      '//gerrit-gwtui:' + ui,
     ] + context,
     docs = docs,
     visibility = visibility,
diff --git a/tools/default.defs b/tools/default.defs
index be31b85..27efa11 100644
--- a/tools/default.defs
+++ b/tools/default.defs
@@ -14,108 +14,36 @@
 
 # Rule definitions loaded by default into every BUCK file.
 
-include_defs('//tools/buck.defs')
 include_defs('//tools/gwt-constants.defs')
+import copy
 
 def genantlr(
     name,
     srcs,
     out):
-  tmp = name + '.src.zip'
   genrule(
     name = name,
     srcs = srcs,
     cmd = '$(exe //lib/antlr:antlr-tool) -o $TMP $SRCS;' +
       'cd $TMP;' +
       'zip -qr $OUT .',
-    deps = ['//lib/antlr:antlr-tool'],
     out = out,
   )
 
-def gwt_module(
-    name,
-    srcs,
-    gwtxml = None,
-    resources = [],
-    deps = [],
-    compile_deps = [],
-    visibility = []):
-  if gwtxml:
-    resources = resources + [gwtxml]
-  java_library(
-    name = name,
-    deps = deps + compile_deps,
-    resources = srcs + resources,
-    visibility = visibility,
-  )
-  java_library(
-    name = name + '_lib',
-    srcs = srcs,
-    deps = [':' + name] + [d + '_lib' for d in deps] + compile_deps,
-    visibility = visibility,
-  )
-
-def gwt_application(
-    name,
-    module_target,
-    compiler_opts = [],
-    compiler_jvm_flags = [],
-    deps = [],
-    visibility = []):
-  cmd = ['$(exe //lib/gwt:compiler)', module_target, '$TMP', '$OUT']
-  cmd += compiler_opts + ['--', '$DEPS']
-  genrule(
-    name = name,
-    cmd = ' '.join(cmd),
-    deps = [
-      '//lib/gwt:compiler',
-      '//lib/gwt:dev',
-    ] + deps,
-    out = '%s.zip' % name,
-    visibility = visibility,
-  )
-
-# Compiles a Java library with additional compile-time dependencies
-# that do not show up as transitive dependencies to java_library()
-# or java_binary() rule that depends on this library.
-def java_library2(
-    name,
-    srcs = [],
-    resources = [],
-    deps = [],
-    compile_deps = [],
-    visibility = []):
-  c = name + '__compile'
-  t = name + '__link'
-  j = 'lib__%s__output/%s.jar' % (c, c)
-  o = 'lib__%s__output/%s.jar' % (name, name)
-  java_library(
-    name = c,
-    srcs = srcs,
-    resources = resources,
-    deps = deps + compile_deps,
-    visibility = ['//tools/eclipse:classpath'],
-  )
-  # Break the dependency chain by passing the newly built
-  # JAR to consumers through a prebuilt_jar().
-  genrule(
-    name = t,
-    cmd = 'mkdir -p $(dirname $OUT);ln -s $SRCS $OUT',
-    srcs = [genfile(j)],
-    deps = [':' + c],
-    out = o,
-  )
-  prebuilt_jar(
-    name = name,
-    binary_jar = genfile(o),
-    deps = deps + [':' + t],
-    visibility = visibility,
-  )
+def gwt_module(gwt_xml=None, **kwargs):
+  kw = copy.deepcopy(kwargs)
+  if 'resources' not in kw:
+    kw['resources'] = []
+  if gwt_xml:
+    kw['resources'] += [gwt_xml]
+  if 'srcs' in kw:
+    kw['resources'] += kw['srcs']
+  java_library(**kw)
 
 def gerrit_extension(
     name,
     deps = [],
-    compile_deps = [],
+    provided_deps = [],
     srcs = [],
     resources = [],
     manifest_file = None,
@@ -124,7 +52,7 @@
   gerrit_plugin(
     name = name,
     deps = deps,
-    compile_deps = compile_deps,
+    provided_deps = provided_deps,
     srcs = srcs,
     resources = resources,
     manifest_file = manifest_file,
@@ -136,7 +64,7 @@
 def gerrit_plugin(
     name,
     deps = [],
-    compile_deps = [],
+    provided_deps = [],
     srcs = [],
     resources = [],
     gwt_module = None,
@@ -144,6 +72,7 @@
     manifest_entries = [],
     type = 'plugin',
     visibility = ['PUBLIC']):
+  from multiprocessing import cpu_count
   mf_cmd = 'v=$(git describe HEAD);'
   if manifest_file:
     mf_src = [manifest_file]
@@ -168,18 +97,18 @@
   if gwt_module:
     gwt_deps = GWT_PLUGIN_DEPS
     static_jars = [':%s-static-jar' % name]
-  java_library2(
+  java_library(
     name = name + '__plugin',
     srcs = srcs,
     resources = resources,
     deps = deps,
-    compile_deps = ['//gerrit-%s-api:lib' % type] + compile_deps + gwt_deps,
+    provided_deps = ['//gerrit-%s-api:lib' % type] + provided_deps + gwt_deps,
+    visibility = ['PUBLIC'],
   )
   if gwt_module:
     prebuilt_jar(
       name = '%s-static-jar' % name,
-      binary_jar = genfile('%s-static.zip' % name),
-      deps = [':%s-static' % name],
+      binary_jar = ':%s-static' % name,
     )
     genrule(
       name = '%s-static' % name,
@@ -189,20 +118,24 @@
         ';cd $TMP' +
         ';zip -qr $OUT .',
       out = '%s-static.zip' % name,
-      deps = [':%s__gwt_application' % name]
     )
-    gwt_application(
+    gwt_binary(
       name = name + '__gwt_application',
-      module_target = gwt_module,
-      compiler_opts = GWT_COMPILER_OPTS,
-      deps = [':%s__plugin' % name] + gwt_deps,
+      modules = [gwt_module],
+      deps = gwt_deps,
+      module_deps = [':%s__plugin' % name],
+      local_workers = cpu_count(),
+      strict = True,
+      experimental_args = GWT_COMPILER_ARGS,
+      vm_args = GWT_JVM_ARGS,
     )
+
   java_binary(
     name = name,
-    manifest_file = genfile('MANIFEST.MF'),
+    manifest_file = ':%s__manifest' % name,
+    merge_manifests = False,
     deps = [
       ':%s__plugin' % name,
-      ':%s__manifest' % name,
     ] + static_jars,
     visibility = visibility,
   )
@@ -225,11 +158,17 @@
     paths,
     srcs = [],
     deps = [],
-    visibility = []
+    visibility = [],
+    do_it_wrong = False,
   ):
+  if do_it_wrong:
+    sourcepath = paths
+  else:
+    sourcepath = ['$SRCDIR/' + n for n in paths]
   genrule(
     name = name,
     cmd = ' '.join([
+      'while ! test -f .buckconfig; do cd ..; done;',
       'javadoc',
       '-quiet',
       '-protected',
@@ -240,13 +179,12 @@
       '-link http://docs.oracle.com/javase/7/docs/api',
       '-subpackages ' + pkg,
       '-sourcepath ',
-      ':'.join([n for n in paths]),
+      ':'.join(sourcepath),
       ' -classpath ',
       ':'.join(['$(location %s)' % n for n in deps]),
       '-d $TMP',
     ]) + ';jar cf $OUT -C $TMP .',
     srcs = srcs,
-    deps = deps,
     out = name + '.jar',
     visibility = visibility,
 )
diff --git a/tools/download_file.py b/tools/download_file.py
index 3e6fca9..88ab41a 100755
--- a/tools/download_file.py
+++ b/tools/download_file.py
@@ -28,6 +28,7 @@
 CACHE_DIR = path.join(GERRIT_HOME, 'buck-cache')
 LOCAL_PROPERTIES = 'local.properties'
 
+
 def hashfile(p):
   d = sha1()
   with open(p, 'rb') as f:
@@ -38,6 +39,7 @@
       d.update(b)
   return d.hexdigest()
 
+
 def safe_mkdirs(d):
   if path.isdir(d):
     return
@@ -47,6 +49,7 @@
     if not path.isdir(d):
       raise err
 
+
 def download_properties(root_dir):
   """ Get the download properties.
 
@@ -73,14 +76,16 @@
       pass
   return p
 
+
 def cache_entry(args):
   if args.v:
     h = args.v
   else:
-    h = sha1(args.u).hexdigest()
+    h = sha1(args.u.encode('utf-8')).hexdigest()
   name = '%s-%s' % (path.basename(args.o), h)
   return path.join(CACHE_DIR, name)
 
+
 opts = OptionParser()
 opts.add_option('-o', help='local output file')
 opts.add_option('-u', help='URL to download')
@@ -110,7 +115,7 @@
 
   print('Download %s' % src_url, file=stderr)
   try:
-    check_call(['curl', '--proxy-anyauth', '-sfo', cache_ent, src_url])
+    check_call(['curl', '--proxy-anyauth', '-ksfo', cache_ent, src_url])
   except OSError as err:
     print('could not invoke curl: %s\nis curl installed?' % err, file=stderr)
     exit(1)
@@ -137,30 +142,24 @@
   exclude += args.x
 if args.exclude_java_sources:
   try:
-    zf = ZipFile(cache_ent, 'r')
-    try:
+    with ZipFile(cache_ent, 'r') as zf:
       for n in zf.namelist():
         if n.endswith('.java'):
           exclude.append(n)
-    finally:
-      zf.close()
   except (BadZipfile, LargeZipFile) as err:
-    print('error opening %s: %s'  % (cache_ent, err), file=stderr)
+    print('error opening %s: %s' % (cache_ent, err), file=stderr)
     exit(1)
 
 if args.unsign:
   try:
-    zf = ZipFile(cache_ent, 'r')
-    try:
+    with ZipFile(cache_ent, 'r') as zf:
       for n in zf.namelist():
         if (n.endswith('.RSA')
             or n.endswith('.SF')
             or n.endswith('.LIST')):
           exclude.append(n)
-    finally:
-      zf.close()
   except (BadZipfile, LargeZipFile) as err:
-    print('error opening %s: %s'  % (cache_ent, err), file=stderr)
+    print('error opening %s: %s' % (cache_ent, err), file=stderr)
     exit(1)
 
 safe_mkdirs(path.dirname(args.o))
diff --git a/tools/eclipse/BUCK b/tools/eclipse/BUCK
index 81889f2..4e76b029 100644
--- a/tools/eclipse/BUCK
+++ b/tools/eclipse/BUCK
@@ -10,10 +10,14 @@
     '//gerrit-httpd:httpd_tests',
     '//gerrit-main:main_lib',
     '//gerrit-patch-jgit:jgit_patch_tests',
-    '//gerrit-plugin-gwtui:gwtui-api',
-    '//gerrit-server:server__compile',
+    '//gerrit-plugin-gwtui:gwtui-api-lib',
+    '//gerrit-server:server',
+    '//gerrit-server:server_tests',
     '//lib/asciidoctor:asciidoc_lib',
     '//lib/asciidoctor:doc_indexer_lib',
+    '//lib/bouncycastle:bcprov',
+    '//lib/bouncycastle:bcpg',
+    '//lib/bouncycastle:bcpkix',
     '//lib/jetty:webapp',
     '//lib/prolog:compiler_lib',
     '//Documentation:index_lib',
diff --git a/tools/eclipse/gerrit_gwt_debug.launch b/tools/eclipse/gerrit_gwt_debug.launch
index 945050d..c09997f 100644
--- a/tools/eclipse/gerrit_gwt_debug.launch
+++ b/tools/eclipse/gerrit_gwt_debug.launch
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <launchConfiguration type="org.eclipse.jdt.launching.localJavaApplication">
 <listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
-<listEntry value="/gerrit/buck-out/gen/lib/gwt/dev/gwt-dev-2.6.0.jar"/>
+<listEntry value="/gerrit/buck-out/gen/lib/gwt/dev/gwt-dev-2.6.1.jar"/>
 </listAttribute>
 <listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
 <listEntry value="1"/>
diff --git a/tools/eclipse/project.py b/tools/eclipse/project.py
index 50644eb..2008316 100755
--- a/tools/eclipse/project.py
+++ b/tools/eclipse/project.py
@@ -32,7 +32,7 @@
 ])
 
 ROOT = path.abspath(__file__)
-for _ in range(0, 3):
+while not path.exists(path.join(ROOT, '.buckconfig')):
   ROOT = path.dirname(ROOT)
 
 opts = OptionParser()
diff --git a/tools/gwt-constants.defs b/tools/gwt-constants.defs
index b5f6292..cc09d3e 100644
--- a/tools/gwt-constants.defs
+++ b/tools/gwt-constants.defs
@@ -1,13 +1,11 @@
-GWT_COMPILER_OPTS = [
-  '-strict',
-  '-style', 'OBF',
-  '-optimize', '9',
+GWT_JVM_ARGS = ['-Xmx512m']
+
+GWT_COMPILER_ARGS = [
   '-XdisableClassMetadata',
   '-XdisableCastChecking',
 ]
 
 GWT_PLUGIN_DEPS = [
-  '//gerrit-gwtui-common:client',
   '//gerrit-plugin-gwtui:gwtui-api-lib',
   '//lib/gwt:user',
 ]
diff --git a/tools/maven/mvn.py b/tools/maven/mvn.py
index 9e36b48..cc10816 100644
--- a/tools/maven/mvn.py
+++ b/tools/maven/mvn.py
@@ -16,9 +16,8 @@
 from __future__ import print_function
 from optparse import OptionParser
 from os import path, environ
-
+from subprocess import check_output
 from sys import stderr
-from tools.util import check_output
 
 opts = OptionParser()
 opts.add_option('--repository', help='maven repository id')
@@ -34,7 +33,7 @@
   exit(1)
 
 root = path.abspath(__file__)
-for _ in range(0, 3):
+while not path.exists(path.join(root, '.buckconfig')):
   root = path.dirname(root)
 
 if 'install' == args.a:
diff --git a/tools/pack_war.py b/tools/pack_war.py
index f794c5c..ba39856 100755
--- a/tools/pack_war.py
+++ b/tools/pack_war.py
@@ -15,10 +15,9 @@
 
 from __future__ import print_function
 from optparse import OptionParser
-from os import makedirs, path, symlink
-from subprocess import check_call
+from os import getcwd, chdir, makedirs, path, symlink
+from subprocess import check_call, check_output
 import sys
-from util import check_output
 
 opts = OptionParser()
 opts.add_option('-o', help='path to write WAR to')
@@ -31,8 +30,11 @@
 root = war[:war.index('buck-out')]
 jars = set()
 
+
 def link_jars(libs, directory):
   makedirs(directory)
+  while not path.isfile('.buckconfig'):
+    chdir('..')
   try:
     cp = check_output(['buck', 'audit', 'classpath'] + libs)
   except Exception as e:
@@ -53,7 +55,7 @@
 try:
   for s in ctx:
     check_call(['unzip', '-q', '-d', war, s])
-  check_call(['zip', '-9qr', args.o, '.'], cwd = war)
+  check_call(['zip', '-9qr', args.o, '.'], cwd=war)
 except KeyboardInterrupt:
   print('Interrupted by user', file=sys.stderr)
   exit(1)
diff --git a/tools/util.py b/tools/util.py
index fb49664..0960c54 100644
--- a/tools/util.py
+++ b/tools/util.py
@@ -14,23 +14,16 @@
 
 from os import path
 
-try:
-  from subprocess import check_output
-except ImportError:
-  from subprocess import Popen, PIPE
-  def check_output(*cmd):
-    return Popen(*cmd, stdout=PIPE).communicate()[0]
-
 REPO_ROOTS = {
   'ATLASSIAN': 'https://maven.atlassian.com/content/repositories/atlassian-3rdparty',
   'ECLIPSE': 'https://repo.eclipse.org/content/groups/releases',
   'GERRIT': 'http://gerrit-maven.storage.googleapis.com',
   'GERRIT_API': 'https://gerrit-api.commondatastorage.googleapis.com/release',
-  'ECLIPSE': 'https://repo.eclipse.org/content/groups/releases',
   'MAVEN_CENTRAL': 'http://repo1.maven.org/maven2',
   'MAVEN_LOCAL': 'file://' + path.expanduser('~/.m2/repository'),
 }
 
+
 def resolve_url(url, redirects):
   """ Resolve URL of a Maven artifact.