Merge "Upgrade javassist to 3.20.0-GA"
diff --git a/.buckconfig b/.buckconfig
index 61c28e4..3d5e9d8 100644
--- a/.buckconfig
+++ b/.buckconfig
@@ -1,9 +1,5 @@
 [alias]
   api = //:api
-  api_deploy = //tools/maven:api_deploy
-  api_install = //tools/maven:api_install
-  war_deploy = //tools/maven:war_deploy
-  war_install = //tools/maven:war_install
   chrome = //:chrome
   docs = //Documentation:searchfree
   firefox = //:firefox
@@ -12,6 +8,7 @@
   headless = //:headless
   polygerrit = //:polygerrit
   release = //:release
+  releasenotes = //ReleaseNotes:html
   safari = //:safari
   soyc = //gerrit-gwtui:ui_soyc
   soyc_r = //gerrit-gwtui:ui_soyc_r
@@ -34,7 +31,3 @@
 
 [test]
   excluded_labels = manual
-
-[repositories]
-  jgit = lib/jgit
-
diff --git a/.buckversion b/.buckversion
index 561a769..f5fe016 100644
--- a/.buckversion
+++ b/.buckversion
@@ -1 +1 @@
-ca8d6cbac373a690f543c5159eec0116e76187a9
+e64a2e2ada022f81e42be750b774024469551398
diff --git a/.editorconfig b/.editorconfig
index 1f149cf..cb18523 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -9,3 +9,4 @@
 charset = utf-8
 indent_style = space
 indent_size = 2
+continuation_indent_size = 4
diff --git a/.gitignore b/.gitignore
index 96b2a5c..e7fe393 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,7 @@
 /test_site
 /.idea
 *.iml
+*.eml
 *.sublime-*
 /gerrit-package-plugins
 /.buckconfig.local
@@ -24,3 +25,4 @@
 *.swp
 *.asc
 /bin/
+*~
diff --git a/Documentation/BUCK b/Documentation/BUCK
index 62c0e07..48ca579 100644
--- a/Documentation/BUCK
+++ b/Documentation/BUCK
@@ -14,7 +14,7 @@
 genasciidoc(
   name = 'html',
   out = 'html.zip',
-  docdir = DOC_DIR,
+  directory = DOC_DIR,
   srcs = SRCS + [':licenses.txt'],
   attributes = documentation_attributes(git_describe()),
   backend = 'html5',
@@ -24,7 +24,7 @@
 genasciidoc(
   name = 'searchfree',
   out = 'searchfree.zip',
-  docdir = DOC_DIR,
+  directory = DOC_DIR,
   srcs = SRCS + [':licenses.txt'],
   attributes = documentation_attributes(git_describe()),
   backend = 'html5',
@@ -49,13 +49,6 @@
   out = 'js_licenses.txt',
 )
 
-genrule(
-  name = 'doc.css',
-  srcs = ['doc.css.in'],
-  cmd = 'cp $SRCS $OUT',
-  out = 'doc.css',
-)
-
 python_binary(
   name = 'gen_licenses',
   main = 'gen_licenses.py',
@@ -64,6 +57,7 @@
 python_binary(
   name = 'replace_macros',
   main = 'replace_macros.py',
+  visibility = ['//ReleaseNotes:'],
 )
 
 genrule(
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index 57e7bf7..0123724 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -99,21 +99,14 @@
 [[administrators]]
 === Administrators
 
-This is the Gerrit "root" identity. The capability
-link:access-control.html#capability_administrateServer['Administrate Server']
-is assigned to this predefined group on Gerrit site creation.
+This is a predefined group, created on Gerrit site initialization, that
+has the capability link:access-control.html#capability_administrateServer[
+'Administrate Server'] assigned.
 
-Users in the 'Administrators' group can perform any action under
-the Admin menu, to any group or project, without further validation
-or any other access controls.  In most installations only those
-users who have direct filesystem and database access would be
-placed into this group.
-
-Membership in the 'Administrators' group does not imply any other
-access rights.  Administrators do not automatically get code review
-approval or submit rights in projects.  This is a feature designed
-to permit administrative users to otherwise access Gerrit as any
-other normal user would, without needing two different accounts.
+It is a normal Gerrit group without magic. This means if you remove
+the 'Administrate Server' capability from it, its members are no longer
+Gerrit administrators, despite the group name. The group may also be
+renamed.
 
 
 [[non-interactive_users]]
@@ -1175,10 +1168,19 @@
 === Administrate Server
 
 This is in effect the owner and administrator role of the Gerrit
-instance.  Any members of a group granted this capability will be
+instance. Any members of a group granted this capability will be
 able to grant any access right to any group. They will also have all
 capabilities granted to them automatically.
 
+In most installations only those users who have direct filesystem and
+database access should be granted this capability.
+
+This capability does not imply any other access rights. Users that have
+this capability do not automatically get code review approval or submit
+rights in projects. This is a feature designed to permit administrative
+users to otherwise access Gerrit as any other normal user would,
+without needing two different accounts.
+
 
 [[capability_batchChangesLimit]]
 === Batch Changes Limit
diff --git a/Documentation/asciidoc.defs b/Documentation/asciidoc.defs
index 2caf725..4b17071 100644
--- a/Documentation/asciidoc.defs
+++ b/Documentation/asciidoc.defs
@@ -35,7 +35,7 @@
   for attribute in attributes:
     asciidoc.extend(['-a', attribute])
   asciidoc.append('$SRCS')
-  newsrcs = [":doc.css"]
+  newsrcs = []
   for src in srcs:
     fn = src
     # We have two cases: regular source files and generated files.
@@ -52,15 +52,13 @@
 
     genrule(
       name = ex,
-      cmd = '$(exe :replace_macros) --suffix="%s"' % EXPN +
+      cmd = '$(exe //Documentation:replace_macros) --suffix="%s"' % EXPN +
         ' -s ' + passed_src + ' -o $OUT' +
         (' --searchbox' if searchbox else ' --no-searchbox'),
       srcs = srcs,
       out = ex,
     )
 
-    # The new AsciiDoctor requires both the css file and include files are under
-    # the same directory. Luckily Buck allows us to use :target as SRCS now.
     newsrcs.append(':%s' % ex)
 
   genrule(
@@ -74,41 +72,42 @@
 def genasciidoc(
     name,
     out,
-    docdir,
+    directory,
     srcs = [],
     attributes = [],
     backend = None,
     searchbox = True,
+    resources = True,
     visibility = []):
   SUFFIX = '_htmlonly'
 
   genasciidoc_htmlonly(
-    name = name + SUFFIX,
+    name = name + SUFFIX if resources else name,
     srcs = srcs,
     attributes = attributes,
     backend = backend,
     searchbox = searchbox,
-    out = name + SUFFIX + '.zip',
+    out = (name + SUFFIX + '.zip') if resources else (name + '.zip'),
   )
 
-  genrule(
-    name = name,
-    cmd = 'cd $TMP;' +
-      'mkdir -p %s/images;' % docdir +
-      'unzip -q $(location %s) -d %s/;'
-      % (':' + name + SUFFIX, docdir) +
-      'for s in $SRCS;do ln -s $s %s;done;' % docdir +
-      'mv %s/*.{jpg,png} %s/images;' % (docdir, docdir) +
-      'cp $(location %s) LICENSES.txt;' % ':licenses.txt' +
-      'zip -qr $OUT *',
-    srcs = glob([
-        'images/*.jpg',
-        'images/*.png',
-      ]) + [
-        ':doc.css',
-        '//gerrit-prettify:prettify.min.css',
-        '//gerrit-prettify:prettify.min.js',
-      ],
-    out = out,
-    visibility = visibility,
-  )
+  if resources:
+    genrule(
+      name = name,
+      cmd = 'cd $TMP;' +
+        'mkdir -p %s/images;' % directory +
+        'unzip -q $(location %s) -d %s/;'
+        % (':' + name + SUFFIX, directory) +
+        'for s in $SRCS;do ln -s $s %s/;done;' % directory +
+        'mv %s/*.{jpg,png} %s/images;' % (directory, directory) +
+        'cp $(location %s) LICENSES.txt;' % ':licenses.txt' +
+        'zip -qr $OUT *',
+      srcs = glob([
+          'images/*.jpg',
+          'images/*.png',
+        ]) + [
+          '//gerrit-prettify:prettify.min.css',
+          '//gerrit-prettify:prettify.min.js',
+        ],
+      out = out,
+      visibility = visibility,
+    )
diff --git a/Documentation/cmd-index-activate.txt b/Documentation/cmd-index-activate.txt
index 0135cfe..6cb7781 100644
--- a/Documentation/cmd-index-activate.txt
+++ b/Documentation/cmd-index-activate.txt
@@ -5,7 +5,7 @@
 
 == SYNOPSIS
 --
-'ssh' -p @SSH_PORT@ @SSH_HOST@ 'gerrit index activate'
+'ssh' -p <port> <host> 'gerrit index activate <index>'
 --
 
 == DESCRIPTION
@@ -18,6 +18,9 @@
 This command allows to activate the latest index even if there were some
 failures.
 
+The <index> argument controls which secondary index is activated. Currently, the
+only supported value is "changes".
+
 == ACCESS
 Caller must be a member of the privileged 'Administrators' group.
 
diff --git a/Documentation/cmd-index-start.txt b/Documentation/cmd-index-start.txt
index 4148b24..0a481e5 100644
--- a/Documentation/cmd-index-start.txt
+++ b/Documentation/cmd-index-start.txt
@@ -5,7 +5,7 @@
 
 == SYNOPSIS
 --
-'ssh' -p @SSH_PORT@ @SSH_HOST@ 'gerrit index start'
+'ssh' -p <port> <host> 'gerrit index start <index>'
 --
 
 == DESCRIPTION
@@ -19,6 +19,9 @@
 Gerrit. This command will not start the indexer if it is already running or if
 the active index is the latest.
 
+The <index> argument controls which secondary index is started. Currently, the
+only supported value is "changes".
+
 == ACCESS
 Caller must be a member of the privileged 'Administrators' group.
 
diff --git a/Documentation/cmd-query.txt b/Documentation/cmd-query.txt
index 0ff59d4..090781b 100644
--- a/Documentation/cmd-query.txt
+++ b/Documentation/cmd-query.txt
@@ -54,15 +54,17 @@
 
 --current-patch-set::
 	Include information about the current patch set in the results.
+	Note that the information will only be included when the current
+	patch set is visible to the caller.
 
 --patch-sets::
-	Include information about all patch sets.  If combined with
-	the --current-patch-set flag then the current patch set
-	information will be output twice, once in each field.
+	Include information about all patch sets visible to the caller.
+        If combined with the --current-patch-set flag then the current patch
+	set information will be output twice, once in each field.
 
 --all-approvals::
-	Include information about all patch sets along with the
-	approval information for each patch set.  If combined with
+	Include information about all patch sets visible to the caller along
+	with the approval information for each patch set.  If combined with
 	the --current-patch-set flag then the current patch set
 	information will be output twice, once in each field.
 
@@ -76,7 +78,7 @@
 --comments::
 	Include comments for all changes. If combined with the
 	--patch-sets flag then all inline/file comments are included for
-	each patch set.
+	each patch set that is visible to the caller.
 
 --commit-message::
 	Include the full commit message in the change description.
diff --git a/Documentation/cmd-review.txt b/Documentation/cmd-review.txt
index 0590337..c3d8651 100644
--- a/Documentation/cmd-review.txt
+++ b/Documentation/cmd-review.txt
@@ -19,6 +19,7 @@
   [--delete]
   [--verified <N>] [--code-review <N>]
   [--label Label-Name=<N>]
+  [--tag TAG]
   {COMMIT | CHANGEID,PATCHSET}...
 --
 
@@ -134,6 +135,15 @@
 	permitted for the user, or the vote is on an outdated or closed patch set,
 	return an error instead of silently discarding the vote.
 
+--tag::
+-t::
+  Apply a 'TAG' to the change message, votes, and inline comments. The 'TAG'
+  can represent an external system like CI that does automated verification
+  of the change. Comments with specific 'TAG' values can be filtered out in
+  the web UI.
+  NOTE: To apply different tags on on different votes/comments multiple
+  invocations of the SSH command are required.
+
 == ACCESS
 Any user who has configured an SSH key.
 
diff --git a/Documentation/cmd-stream-events.txt b/Documentation/cmd-stream-events.txt
index ee645c1..028bd58 100644
--- a/Documentation/cmd-stream-events.txt
+++ b/Documentation/cmd-stream-events.txt
@@ -181,23 +181,6 @@
 eventCreatedOn:: Time in seconds since the UNIX epoch when this event was
 created.
 
-=== Merge Failed
-
-Sent when a change has failed to be merged into the git repository.
-
-type:: "merge-failed"
-
-change:: link:json.html#change[change attribute]
-
-patchSet:: link:json.html#patchSet[patchSet attribute]
-
-submitter:: link:json.html#account[account attribute]
-
-reason:: Reason that the merge failed.
-
-eventCreatedOn:: Time in seconds since the UNIX epoch when this event was
-created.
-
 === Patchset Created
 
 Sent when a new change has been uploaded, or a new patch set has been uploaded
@@ -247,6 +230,27 @@
 eventCreatedOn:: Time in seconds since the UNIX epoch when this event was
 created.
 
+=== Reviewer Deleted
+
+Sent when a reviewer (with a vote) is removed from a change.
+
+type:: "reviewer-deleted"
+
+change:: link:json.html#change[change attribute]
+
+patchSet:: link:json.html#patchSet[patchSet attribute]
+
+reviewer:: link:json.html#account[account attribute]
+
+author:: link:json.html#account[account attribute]
+
+approvals:: All link:json.html#approval[approval attributes] removed.
+
+comment:: Review comment cover message.
+
+eventCreatedOn:: Time in seconds since the UNIX epoch when this event was
+created.
+
 === Topic Changed
 
 Sent when the topic of a change has been changed.
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 59830cc..34cff05 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -915,6 +915,12 @@
 +
 Default is 30 seconds.
 
+[[change.allowBlame]]change.allowBlame::
++
+Allow blame on side by side diff. If set to false, blame cannot be used.
++
+Default is true.
+
 [[change.allowDrafts]]change.allowDrafts::
 +
 Allow drafts workflow. If set to false, drafts cannot be created,
@@ -922,6 +928,21 @@
 +
 Default is true.
 
+[[change.cacheAutomerge]]change.cacheAutomerge::
++
+When reviewing diff commits, the left-hand side shows the output of the
+result of JGit's automatic merge algorithm. This option controls whether
+this output is cached in the change repository, or if only the diff is
+cached in the persistent `diff` cache.
++
+If true, automerge results are stored in the repository under
+`refs/cache-automerge/*`; the results of diffing the change against its
+automerge base are stored in the diff cache. If false, no extra data is
+stored in the repository, only the diff cache. This can result in slight
+performance improvements by reducing the number of refs in the repo.
++
+Default is true.
+
 [[change.submitLabel]]change.submitLabel::
 +
 Label name for the submit button.
@@ -955,7 +976,7 @@
 Default is "Submit all ${topicSize} changes of the same topic (${submitSize}
 changes including ancestors and other changes related by topic)".
 
-[[change.submitWholeTopic]]change.submitWholeTopic (*Experimental*)::
+[[change.submitWholeTopic]]change.submitWholeTopic::
 +
 Determines if the submit button submits the whole topic instead of
 just the current change.
@@ -1086,25 +1107,6 @@
 +
 Default is 300 seconds (5 minutes).
 
-[[changeMerge.threadPoolSize]]changeMerge.threadPoolSize::
-+
-_Deprecated:_ Formerly used to control thread pool size for background
-mergeability checks. These checks were moved to the indexing threadpool,
-so this value is now used for
-link:#index.batchThreads[index.batchThreads], only if that value is not
-provided.
-+
-This option may be removed in a future version.
-
-[[changeMerge.interactiveThreadPoolSize]]changeMerge.interactiveThreadPoolSize::
-+
-_Deprecated:_ Formerly used to control thread pool size for interactive
-mergeability checks. These checks were moved to the indexing threadpool,
-so this value is now used for link:#index.threads[index.threads], only
-if that value is not provided.
-+
-This option may be removed in a future version.
-
 [[commentlink]]
 === Section commentlink
 
@@ -2061,11 +2063,6 @@
 Optional filename for the project created hook, if not specified then
 `project-created` will be used.
 
-[[hooks.mergeFailedHook]]hooks.mergeFailedHook::
-+
-Optional filename for the merge failed hook, if not specified then
-`merge-failed` will be used.
-
 [[hooks.patchsetCreatedHook]]hooks.patchsetCreatedHook::
 +
 Optional filename for the patchset created hook, if not specified then
@@ -2400,9 +2397,8 @@
 +
 Number of threads to use for indexing in normal interactive operations.
 +
-Defaults to 1 if not set, or set to a negative value (unless
-link:#changeMerge.interactiveThreadPoolSize[changeMerge.interactiveThreadPoolSize]
-is iset).
+If not set or set to a negative value, defaults to 1 plus half of the number of
+logical CPUs as returned by the JVM.
 
 [[index.batchThreads]]index.batchThreads::
 +
@@ -2410,8 +2406,7 @@
 online schema upgrades.
 +
 If not set or set to a negative value, defaults to the number of logical
-CPUs as returned by the JVM (unless
-link:#changeMerge.threadPoolSize[changeMerge.threadPoolSize] is set).
+CPUs as returned by the JVM.
 
 [[index.onlineUpgrade]]index.onlineUpgrade::
 +
diff --git a/Documentation/config-hooks.txt b/Documentation/config-hooks.txt
index 5b1f5e3..1d92b49 100644
--- a/Documentation/config-hooks.txt
+++ b/Documentation/config-hooks.txt
@@ -78,14 +78,6 @@
   change-merged --change <change id> --change-url <change url> --change-owner <change owner> --project <project name> --branch <branch> --topic <topic> --submitter <submitter> --commit <sha1> --newrev <sha1>
 ====
 
-=== merge-failed
-
-Called whenever a change has failed to merge.
-
-====
-  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
 
 Called whenever a change has been abandoned.
@@ -126,6 +118,14 @@
   reviewer-added --change <change id> --change-url <change url> --change-owner <change owner> --project <project name> --branch <branch> --reviewer <reviewer>
 ====
 
+=== reviewer-deleted
+
+Called whenever a reviewer (with a vote) is removed from a change.
+
+====
+  reviewer-deleted --change <change id> --change-url <change url> --change-owner <change owner> --project <project name> --branch <branch> --reviewer <reviewer> [--<approval category id> <score> --<approval category id> <score> ...]
+====
+
 === topic-changed
 
 Called whenever a change's topic is changed from the Web UI or via the REST API.
diff --git a/Documentation/config-mail.txt b/Documentation/config-mail.txt
index a97cdf2..7aead9a 100644
--- a/Documentation/config-mail.txt
+++ b/Documentation/config-mail.txt
@@ -62,6 +62,12 @@
 to removing votes on changes.  It is a `ChangeEmail`: see `ChangeSubject.vm`
 and `ChangeFooter.vm`.
 
+=== DeleteReviewer.vm
+
+The `DeleteReiewer.vm` template will determine the contents of the email related
+to a user removing a reviewer (with a vote) from a change.  It is a
+`ChangeEmail`: see `ChangeSubject.vm` and `ChangeFooter.vm`.
+
 === Footer.vm
 
 The `Footer.vm` template will determine the contents of the footer text
diff --git a/Documentation/config-plugins.txt b/Documentation/config-plugins.txt
index 86570bd..415cb76 100644
--- a/Documentation/config-plugins.txt
+++ b/Documentation/config-plugins.txt
@@ -118,14 +118,12 @@
 these plugins.
 
 The Gerrit Project doesn't provide binaries for these plugins, but
-there are some public services that offer the download of pre-built
+there is one public service that offers the download of pre-built
 plugin jars:
 
 * link:https://gerrit-ci.gerritforge.com[CI Server from GerritForge]
-* link:http://builds.quelltextlich.at/gerrit/nightly/index.html[
-  CI Server from Quelltextlich]
 
-The following list gives an overview about available plugins, but the
+The following list gives an overview of available plugins, but the
 list may not be complete. You may discover more plugins on
 link:https://gerrit-review.googlesource.com/#/admin/projects/?filter=plugins%252F[
 gerrit-review].
diff --git a/Documentation/config.defs b/Documentation/config.defs
index 380080f..7f814d3 100644
--- a/Documentation/config.defs
+++ b/Documentation/config.defs
@@ -15,7 +15,7 @@
     'tilde="&#126;"',
     'last-update-label!',
     'source-highlighter=prettify',
-    'stylesheet=doc.css',
+    'stylesheet=DEFAULT',
     'linkcss=true',
     'prettifydir=.',
     'revnumber="%s"' % revision,
diff --git a/Documentation/dev-buck.txt b/Documentation/dev-buck.txt
index d0b2ddc..e36f8a6 100644
--- a/Documentation/dev-buck.txt
+++ b/Documentation/dev-buck.txt
@@ -105,6 +105,9 @@
   buck build gerrit
 ----
 
+*Note: PolyGerrit UI may require additional tools (such as npm). Please read
+the polygerrit-ui/README.md for more info.
+
 The output executable WAR will be placed in:
 
 ----
@@ -158,13 +161,13 @@
 Install {extension,plugin,gwt}-api to the local maven repository:
 
 ----
-  buck build api_install
+  sh tools/maven/api.sh install
 ----
 
 Install gerrit.war to the local maven repository:
 
 ----
-  buck build war_install
+  sh tools/maven/api.sh war_install
 ----
 
 === Plugins
@@ -610,7 +613,7 @@
 The following tests should be executed, when Buck version is upgraded:
 
 * buck build release
-* buck build api_install
+* tools/maven/api.sh install
 * buck test
 * buck build gerrit, change some sources in gerrit-server project,
   repeat buck build gerrit and verify that gerrit.war was updated
@@ -639,30 +642,6 @@
 link:#buck-daemon[Using Buck daemon] section above how to temporarily
 disable `buckd`.
 
-=== Re-triggering rule execution
-
-There is no way to re-trigger custom rules with side effects, like
-`api_{deploy|install}`. This is a `genrule()` that depends on Java sources
-and is deploying the Plugin API through custom Python script to the local or
-remote Maven repositories. When for some reasons the deployment was undone,
-there is no supported way to re-trigger the execution of `api_{deploy|install}`
-targets. That's because `--no-cache` option will ignore the `Buck` cache, but
-there is no way to ignore `buck-out` directory. To overcome this Buck's design
-limitation new `tools/maven/api.py` script was added, that always re-triggers
-installation or deployment of Plugin API to local or Central Maven repository.
-
-```
-  tools/maven/api.py {deploy|install}
-```
-
-Dry run mode is also supported:
-
-```
-  tools/maven/api.py -n {deploy|install}
-```
-
-With this script the deployment would re-trigger on every invocation.
-
 == Troubleshooting Buck
 
 In some cases problems with Buck itself need to be investigated. See for example
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 501f986..dbaa3c1 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -94,7 +94,6 @@
   Implementation-Title: Example plugin showing examples
   Implementation-Version: 1.0
   Implementation-Vendor: Example, Inc.
-  Implementation-URL: http://example.com/opensource/plugin-foo/
 ====
 
 === ApiType
@@ -1804,7 +1803,7 @@
 
   @Inject
   public MyMenu(@PluginName String name) {
-    menuEntries = Lists.newArrayList();
+    menuEntries = new ArrayList<>();
     menuEntries.add(new MenuEntry("My Menu", Collections.singletonList(
       new MenuItem("My Screen", "#/x/" + name + "/my-screen", ""))));
   }
diff --git a/Documentation/dev-release.txt b/Documentation/dev-release.txt
index 2a6cb1c..5ede117 100644
--- a/Documentation/dev-release.txt
+++ b/Documentation/dev-release.txt
@@ -154,11 +154,11 @@
 [[build-gerrit]]
 === Build Gerrit
 
-* Build the Gerrit WAR and API JARs
+* Build the Gerrit WAR, API JARs and documentation
 +
 ----
   buck clean
-  buck build --no-cache release
+  buck build --no-cache release docs
   buck build api_install
 ----
 
@@ -191,26 +191,19 @@
 * Push the WAR to Maven Central:
 +
 ----
-  buck build war_deploy
+  sh tools/maven/api.sh war_deploy
 ----
 
 * Push the plugin artifacts to Maven Central:
 +
 ----
-  buck build api_deploy
-----
-+
-For troubleshooting, the environment variable `VERBOSE` can be set. This
-prints out the commands that are executed by the Buck build process:
-+
-----
-  VERBOSE=1 buck build api_deploy
+  sh tools/maven/api.sh deploy
 ----
 +
 If no artifacts are uploaded, clean the `buck-out` folder and retry:
 +
 ----
-  rm -rf buck-out
+  buck clean ; rm -rf buck-out
 ----
 
 * Push the plugin Maven archetypes to Maven Central:
@@ -342,11 +335,14 @@
 * Build the release notes:
 +
 ----
-  make -C ReleaseNotes
+  buck build releasenotes
 ----
 
-* Extract the documentation files from the zip file generated during
-the release build: `buck-out/gen/Documentation/html/html.zip`.
+* Extract the release notes files from the zip file generated from the previous
+step: `buck-out/gen/ReleaseNotes/html/html.zip`.
+
+* Extract the documentation files from the zip file generated from
+`buck build docs`: `buck-out/gen/Documentation/searchfree/searchfree.zip`.
 
 * Upload the files manually via web browser to the appropriate folder
 in the
diff --git a/Documentation/dev-stars.txt b/Documentation/dev-stars.txt
new file mode 100644
index 0000000..1a98438
--- /dev/null
+++ b/Documentation/dev-stars.txt
@@ -0,0 +1,73 @@
+= Gerrit Code Review - Stars
+
+== Description
+
+Changes can be starred with labels that behave like private hashtags.
+Any label can be applied to a change, but these labels are only visible
+to the user for which the labels have been set.
+
+Stars allow users to categorize changes by self-defined criteria and
+then build link:user-dashboards.html[dashboards] for them by making use
+of the link:#query-stars[star query operators].
+
+[[star-api]]
+== Star API
+
+The link:rest-api-accounts.html#star-endpoints[star REST API] supports:
+
+* link:rest-api-accounts.html#get-stars[
+  get star labels from a change]
+* link:rest-api-accounts.html#set-stars[
+  update star labels on a change]
+* link:rest-api-accounts.html#get-starred-changes[
+  list changes that are starred by any label]
+
+Star labels are also included in
+link:rest-api-changes.html#change-info[ChangeInfo] entities that are
+returned by the link:rest-api-changes.html[changes REST API].
+
+There are link:rest-api-accounts.html#default-star-endpoints[
+additional REST endpoints] for the link:#default-star[default star].
+
+Only the link:#default-star[default star] is shown in the WebUi and
+can be updated from there. Other stars do not show up in the WebUi.
+
+[[default-star]]
+== Default Star
+
+If the default star is set by a user, this user is automatically
+notified by email whenever updates are made to that change.
+
+The default star is the star that is shown in the WebUI and which can
+be updated from there.
+
+The default star is represented by the special star label 'star'.
+
+[[query-stars]]
+== Query Stars
+
+There are several query operators to find changes with stars:
+
+* link:user-search.html#star[star:<LABEL>]:
+  Matches any change that was starred by the current user with the
+  label `<LABEL>`.
+* link:user-search.html#has-stars[has:stars]:
+  Matches any change that was starred by the current user with any
+  label.
+* link:user-search.html#is-starred[is:starred] /
+  link:user-search.html#has-star[has:star]:
+  Matches any change that was starred by the current user with the
+  link:#default-star[default star].
+
+[[syntax]]
+== Syntax
+
+Star labels cannot contain whitespace characters. All other characters
+are allowed.
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/doc.css.in b/Documentation/doc.css.in
deleted file mode 100644
index e09e426..0000000
--- a/Documentation/doc.css.in
+++ /dev/null
@@ -1,80 +0,0 @@
-body {
-  margin: 1em auto;
-  width: 900px;
-}
-
-#toctitle {
-  margin-top: 0.5em;
-  font-weight: bold;
-}
-
-h1, h2, h3, h4, h5, h6, #toctitle {
-  color: #527bbd;
-  font-family: sans-serif;
-}
-
-h1, h2, h3 {
-  border-bottom: 2px solid silver;
-}
-
-h1 {
-  margin-top: 1.5em;
-}
-
-p {
-  margin: 0.5em 0 0.5em 0;
-}
-li p {
-  margin: 0.2em 0 0.2em 0;
-}
-
-#license > .content,
-.listingblock > .content {
-  border: 2px solid silver;
-  background: #ebebeb;
-  color: darkgreen;
-  padding: 2px;
-  overflow: auto;
-}
-
-#license > .content pre,
-.listingblock > .content pre {
-  background: none;
-  border: 0 solid silver;
-  padding: 0 0 0 0;
-}
-
-dl dt {
-  margin-top: 1em;
-}
-
-table.tableblock {
-  border-collapse: collapse;
-}
-
-table.tableblock,
-th.tableblock,
-td.tableblock {
-  border: 1px solid #EEE;
-}
-
-div.title {
-  color: #527bbd;
-  font-family: Arial,Helvetica,sans-serif;
-  font-weight: bold;
-  text-align: left;
-}
-
-.listingblock div.title {
-  margin-top: 1.0em;
-  margin-bottom: 0.5em;
-}
-
-div.admonitionblock {
-  margin-top: 1em;
-}
-
-div.admonitionblock td.content {
-  padding-left: 0.5em;
-  border-left: 3px solid #dddddd;
-}
diff --git a/Documentation/error-messages.txt b/Documentation/error-messages.txt
index 6ad7ac5..2632254 100644
--- a/Documentation/error-messages.txt
+++ b/Documentation/error-messages.txt
@@ -31,7 +31,7 @@
 * link:error-permission-denied.html[Permission denied (publickey)]
 * link:error-prohibited-by-gerrit.html[prohibited by Gerrit]
 * link:error-project-not-found.html[Project not found: ...]
-* link:error-squash-commits-first.html[squash commits first]
+* link:error-same-change-id-in-multiple-changes.html[same Change-Id in multiple changes]
 * link:error-upload-denied.html[Upload denied for project \'...']
 * link:error-not-allowed-to-upload-merges.html[you are not allowed to upload merges]
 
diff --git a/Documentation/error-squash-commits-first.txt b/Documentation/error-same-change-id-in-multiple-changes.txt
similarity index 94%
rename from Documentation/error-squash-commits-first.txt
rename to Documentation/error-same-change-id-in-multiple-changes.txt
index 4069d5b..b6aad69 100644
--- a/Documentation/error-squash-commits-first.txt
+++ b/Documentation/error-same-change-id-in-multiple-changes.txt
@@ -1,4 +1,4 @@
-= squash commits first
+= same Change-Id in multiple changes
 
 With this error message Gerrit rejects to push a commit if it
 contains the same Change-Id as a predecessor commit.
@@ -50,8 +50,10 @@
   Writing objects: 100% (6/6), 558 bytes, done.
   Total 6 (delta 0), reused 0 (delta 0)
   To ssh://JohnDoe@host:29418/myProject
-   ! [remote rejected] HEAD -> refs/for/master (squash commits first)
+  ! [remote rejected] HEAD -> refs/for/master (same Change-Id in multiple changes.
+  Squash the commits with the same Change-Id or ensure Change-Ids are unique for each commit)
   error: failed to push some refs to 'ssh://JohnDoe@host:29418/myProject'
+
 ----
 
 If it was the intention to rework a change and push a new patch
diff --git a/Documentation/images/link.png b/Documentation/images/link.png
deleted file mode 100644
index 25eacb7..0000000
--- a/Documentation/images/link.png
+++ /dev/null
Binary files differ
diff --git a/Documentation/index.txt b/Documentation/index.txt
index 2b7aa4c..1693c18 100644
--- a/Documentation/index.txt
+++ b/Documentation/index.txt
@@ -68,6 +68,7 @@
 .. link:dev-build-plugins.html[Building Gerrit plugins]
 .. link:js-api.html[JavaScript Plugin API]
 .. link:config-validation.html[Validation Interfaces]
+.. link:dev-stars.html[Starring Changes]
 . link:dev-design.html[System Design]
 . link:i18n-readme.html[i18n Support]
 
@@ -84,5 +85,9 @@
 * link:https://gerrit.googlesource.com/gerrit[Source Code]
 * link:https://www.gerritcodereview.com/about.md[A History of Gerrit Code Review]
 
+GERRIT
+------
+Part of link:https://www.gerritcodereview.com/[Gerrit Code Review]
+
 SEARCHBOX
 ---------
diff --git a/Documentation/pgm-init.txt b/Documentation/pgm-init.txt
index 9fe813b..22f9109 100644
--- a/Documentation/pgm-init.txt
+++ b/Documentation/pgm-init.txt
@@ -1,7 +1,7 @@
 = init
 
 == NAME
-init - Initialize a new Gerrit server installation
+init - Initialize/Upgrade a Gerrit server installation
 
 == SYNOPSIS
 --
@@ -9,9 +9,12 @@
 	-d <SITE_PATH>
 	[--batch]
 	[--no-auto-start]
+	[--skip-plugins]
 	[--list-plugins]
 	[--install-plugin=<PLUGIN_NAME>]
-        [--dev]
+	[--install-all-plugins]
+	[--secure-store-lib]
+	[--dev]
 	[--skip-all-downloads]
 	[--skip-download=<LIBRARY_NAME>]
 --
@@ -22,18 +25,19 @@
 into a newly created `$site_path`.
 
 If run in an existing `$site_path`, init will upgrade some resources
-as necessary.
+(e.g. DB schema, plugins) as necessary.
 
 == OPTIONS
 --batch::
-	Run in batch mode, skipping interactive prompts.  Reasonable
-	configuration defaults are chosen based on the whims of
-	the Gerrit developers.
+	Run in batch mode, skipping interactive prompts. For a fresh
+	install, reasonable configuration defaults are chosen based
+	on the whims of the Gerrit developers. On upgrades, the settings
+	in gerrit.config are respected.
 +
-If during a schema migration unused objects (e.g. tables, columns)
-are detected they are *not* automatically dropped, but only a list of
-SQL statements to drop these objects is provided. To drop the unused
-objects these SQL statements have to be executed manually.
+	If during a schema migration unused objects (e.g. tables, columns)
+	are detected they are *not* automatically dropped, but only a list of
+	SQL statements to drop these objects is provided. To drop the unused
+	objects these SQL statements have to be executed manually.
 
 --no-auto-start::
 	Don't automatically start the daemon after initializing a
@@ -46,13 +50,32 @@
 	Location of the gerrit.config file, and all other per-site
 	configuration data, supporting libraries and log files.
 
+--skip-plugins::
+	Entirely skip installation and initialization of plugins. This option
+	is needed when initializing a gerrit site without an archive. That
+	happens when running gerrit acceptance or integration tests in a
+	debugger, using classes. Supplying this option leads to ignoring the
+	--install-plugin and --install-all-plugins options, if supplied as well.
+
 --list-plugins::
 	Print names of plugins that can be installed during init process.
 
+--install-all-plugins::
+	Automatically install all plugins from gerrit.war without asking.
+	This option also works in batch mode. This option cannot be supplied
+	alongside --install-plugin.
+
+--secure-store-lib::
+	Path to the jar providing the chosen
+	link:dev-plugins.html#secure-store[SecureStore] implementation class.
+	This option is used the same way as the --new-secure-store-lib option
+	documented in link:pgm-SwitchSecureStore.html[SwitchSecureStore].
+
 --install-plugin::
 	Automatically install plugin with given name without asking.
-	This option may be supplied more than once to install multiple
-	plugins.
+	This option also works in batch mode. This option may be supplied
+	more than once to install multiple plugins. This option cannot be
+	supplied alongside --install-all-plugins.
 
 --dev::
 	Install in developer mode. Default configuration settings are
diff --git a/Documentation/project-configuration.txt b/Documentation/project-configuration.txt
index 6d7c6d0..2b0db70b 100644
--- a/Documentation/project-configuration.txt
+++ b/Documentation/project-configuration.txt
@@ -53,7 +53,9 @@
 
 The method Gerrit uses to submit a change to a project can be
 modified by any project owner through the project console, `Projects` >
-`List` > my/project.  The following methods are supported:
+`List` > my/project. In general, a submitted change is only merged if all
+its dependencies are also submitted, with exceptions documented below.
+The following submit types are supported:
 
 [[fast_forward_only]]
 * Fast Forward Only
@@ -96,10 +98,12 @@
 is also set to the submitter, while the author header retains the
 original patch set author.
 +
-Note that Gerrit ignores patch set dependencies when operating in
-cherry-pick mode. Submitters must remember to submit changes in
-the right order since inter-change dependencies will not be
-enforced for them.
+Note that Gerrit ignores dependencies between changes when using this
+submit type unless
+link:config-gerrit.html#change.submitWholeTopic[`change.submitWholeTopic`]
+is enabled and depending changes share the same topic. So generally
+submitters must remember to submit changes in the right order when using this
+submit type.
 
 [[rebase_if_necessary]]
 * Rebase If Necessary
diff --git a/Documentation/replace_macros.py b/Documentation/replace_macros.py
index fec4a58..a8b7699 100755
--- a/Documentation/replace_macros.py
+++ b/Documentation/replace_macros.py
@@ -60,9 +60,32 @@
 SEARCH_BOX = """
 
 ++++
-<div style="position:absolute; right:20px; top:20px;">
-<input type="text" id="docSearch" size="70" />
-<button type="button" id="searchBox">Search</button>
+<div style="
+  position:fixed;
+  top:0px;
+  right:0px;
+  text-align:
+  right;
+  padding-top:2px;
+  padding-right:0.5em;
+  padding-bottom:2px;">
+<input size="40"
+  style="line-height: 0.75em;font-size: 0.75em;"
+  id="docSearch"
+  type="text">
+<button style="
+  background:none!important;
+  border:none;
+  padding:0!important;
+  vertical-align:bottom;
+  font-family:'Open Sans','DejaVu Sans',sans-serif;
+  font-size:0.8em;
+  color:#1d4b8f;
+  text-decoration:none;"
+  type="button"
+  id="searchBox">
+  Search
+</button>
 <script type="text/javascript">
 var f = function() {
   window.location = '../#/Documentation/' +
@@ -148,11 +171,14 @@
           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);
+              + ' padding-left: 4px; padding-right: 4px;');
+          var span = document.createElement('span');
+          span.setAttribute('style', 'height: ' + element.offsetHeight + 'px;'
+              + ' display: inline-block; vertical-align: baseline;'
+              + ' font-size: 16px; text-decoration: none; color: grey;');
+          a.appendChild(span);
+          var link = document.createTextNode('🔗');
+          span.appendChild(link);
           element.insertBefore(a, element.firstChild);
 
           // remove the link icon when the mouse is moved away,
@@ -160,14 +186,16 @@
           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
+                && document.elementFromPoint(evt.clientX, evt.clientY) != span
+                && document.elementFromPoint(evt.clientX, evt.clientY) != link
                 && element.contains(a)) {
               element.removeChild(a);
             }
           }
           element.onmouseout = hide;
           a.onmouseout = hide;
-          img.onmouseout = hide;
+          span.onmouseout = hide;
+          link.onmouseout = hide;
         }
       }
     }
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index e6dec40..bb61c3d 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -641,6 +641,9 @@
 
 The SSH public key must be provided as raw content in the request body.
 
+Trying to add an SSH key that already exists succeeds, but no new SSH
+key is persisted.
+
 .Request
 ----
   POST /accounts/self/sshkeys HTTP/1.0
@@ -1105,11 +1108,13 @@
     "changes_per_page": 25,
     "show_site_header": true,
     "use_flash_clipboard": true,
+    "download_command": "CHECKOUT",
     "date_format": "STD",
     "time_format": "HHMM_12",
+    "diff_view": "SIDE_BY_SIDE",
     "size_bar_in_change_table": true,
     "review_category_strategy": "ABBREV",
-    "diff_view": "SIDE_BY_SIDE",
+    "mute_common_path_prefixes": true,
     "my": [
       {
         "url": "#/dashboard/self",
@@ -1159,11 +1164,13 @@
     "changes_per_page": 50,
     "show_site_header": true,
     "use_flash_clipboard": true,
+    "download_command": "CHECKOUT",
     "date_format": "STD",
     "time_format": "HHMM_12",
     "size_bar_in_change_table": true,
     "review_category_strategy": "NAME",
     "diff_view": "SIDE_BY_SIDE",
+    "mute_common_path_prefixes": true,
     "my": [
       {
         "url": "#/dashboard/self",
@@ -1207,11 +1214,13 @@
     "changes_per_page": 50,
     "show_site_header": true,
     "use_flash_clipboard": true,
+    "download_command": "CHECKOUT",
     "date_format": "STD",
     "time_format": "HHMM_12",
     "size_bar_in_change_table": true,
     "review_category_strategy": "NAME",
     "diff_view": "SIDE_BY_SIDE",
+    "mute_common_path_prefixes": true,
     "my": [
       {
         "url": "#/dashboard/self",
@@ -1291,7 +1300,7 @@
 
 .Request
 ----
-  GET /a/accounts/self/preferences.diff HTTP/1.0
+  PUT /a/accounts/self/preferences.diff HTTP/1.0
   Content-Type: application/json; charset=UTF-8
 
   {
@@ -1361,6 +1370,7 @@
     "key_map_type": "VIM",
     "tab_size": 4,
     "line_length": 80,
+    "indent_unit": 2,
     "cursor_blink_rate": 530,
     "hide_top_menu": true,
     "show_whitespace_errors": true,
@@ -1391,6 +1401,7 @@
     "key_map_type": "VIM",
     "tab_size": 4,
     "line_length": 80,
+    "indent_unit": 2,
     "cursor_blink_rate": 530,
     "hide_top_menu": true,
     "show_tabs": true,
@@ -1426,16 +1437,155 @@
   }
 ----
 
-[[get-starred-changes]]
-=== Get Starred Changes
+[[get-watched-projects]]
+=== Get Watched Projects
+--
+'GET /accounts/link:#account-id[\{account-id\}]/watched.projects'
+--
+
+Retrieves all projects a user is watching.
+
+.Request
+----
+  GET /a/accounts/self/watched.projects HTTP/1.0
+----
+
+As result the watched projects of the user are returned as a list of
+link:#project-watch-info[ProjectWatchInfo] entities.
+The result is sorted by project name in ascending order.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  [
+    {
+      "project": "Test Project 1",
+      "notify_new_changes": true,
+      "notify_new_patch_sets": true,
+      "notify_all_comments": true,
+    },
+    {
+      "project": "Test Project 2",
+      "filter": "branch:experimental",
+      "notify_all_comments": true,
+      "notify_submitted_changes": true,
+      "notify_abandoned_changes": true
+    }
+  ]
+----
+
+[[set-watched-projects]]
+=== Add/Update a List of Watched Project Entities
+--
+'POST /accounts/link:#account-id[\{account-id\}]/watched.projects'
+--
+
+Add new projects to watch or update existing watched projects.
+Projects that are already watched by a user will be updated with
+the provided configuration. All other projects in the request
+will be watched using the provided configuration. The posted body
+can contain link:#project-watch-info[ProjectWatchInfo] entities.
+Omitted boolean values will be set to false.
+
+.Request
+----
+  POST /a/accounts/self/watched.projects HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  [
+    {
+      "project": "Test Project 1",
+      "notify_new_changes": true,
+      "notify_new_patch_sets": true,
+      "notify_all_comments": true,
+    }
+  ]
+----
+
+As result the watched projects of the user are returned as a list of
+link:#project-watch-info[ProjectWatchInfo] entities.
+The result is sorted by project name in ascending order.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  [
+    {
+      "project": "Test Project 1",
+      "notify_new_changes": true,
+      "notify_new_patch_sets": true,
+      "notify_all_comments": true,
+    },
+    {
+      "project": "Test Project 2",
+      "notify_new_changes": true,
+      "notify_new_patch_sets": true,
+      "notify_all_comments": true,
+    }
+  ]
+----
+
+[[delete-watched-projects]]
+=== Delete Watched Projects
+--
+'POST /accounts/link:#account-id[\{account-id\}]/watched.projects:delete'
+--
+
+Projects posted to this endpoint will no longer be watched. The posted body
+can contain an array of project names as strings.
+
+.Request
+----
+  POST /a/accounts/self/watched.projects:delete HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  [
+    "Test Project 1"
+  ]
+----
+
+As result the watched projects of the user are returned as a list of
+link:#project-watch-info[ProjectWatchInfo] entities.
+The result is sorted by project name in ascending order.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  [
+    {
+      "project": "Test Project 2",
+      "notify_new_changes": true,
+      "notify_new_patch_sets": true,
+      "notify_all_comments": true,
+    }
+  ]
+----
+
+[[default-star-endpoints]]
+== Default Star Endpoints
+
+[[get-changes-with-default-star]]
+=== Get Changes With Default Star
 --
 'GET /accounts/link:#account-id[\{account-id\}]/starred.changes'
 --
 
-Gets the changes starred by the identified user account. This
-URL endpoint is functionally identical to the changes query
-`GET /changes/?q=is:starred`. The result is a list of
-link:rest-api-changes.html#change-info[ChangeInfo] entities.
+Gets the changes that were starred with the default star by the
+identified user account. This URL endpoint is functionally identical
+to the changes query `GET /changes/?q=is:starred`. The result is a list
+of link:rest-api-changes.html#change-info[ChangeInfo] entities.
 
 .Request
 ----
@@ -1460,6 +1610,9 @@
       "created": "2013-02-01 09:59:32.126000000",
       "updated": "2013-02-21 11:16:36.775000000",
       "starred": true,
+      "stars": [
+        "star"
+      ],
       "mergeable": true,
       "submittable": false,
       "insertions": 145,
@@ -1473,14 +1626,15 @@
 ----
 
 [[star-change]]
-=== Star Change
+=== Put Default Star On Change
 --
 'PUT /accounts/link:#account-id[\{account-id\}]/starred.changes/link:rest-api-changes.html#change-id[\{change-id\}]'
 --
 
-Star a change. Starred changes are returned for the search query
-`is:starred` or `starredby:USER` and automatically notify the user
-whenever updates are made to the change.
+Star a change with the default label. Changes starred with the default
+label are returned for the search query `is:starred` or `starredby:USER`
+and automatically notify the user whenever updates are made to the
+change.
 
 .Request
 ----
@@ -1493,12 +1647,12 @@
 ----
 
 [[unstar-change]]
-=== Unstar Change
+=== Remove Default Star From Change
 --
 'DELETE /accounts/link:#account-id[\{account-id\}]/starred.changes/link:rest-api-changes.html#change-id[\{change-id\}]'
 --
 
-Unstar a change. Removes the starred flag, stopping notifications.
+Remove the default star label from a change. This stops notifications.
 
 .Request
 ----
@@ -1510,6 +1664,131 @@
   HTTP/1.1 204 No Content
 ----
 
+[[star-endpoints]]
+== Star Endpoints
+
+[[get-starred-changes]]
+=== Get Starred Changes
+--
+'GET /accounts/link:#account-id[\{account-id\}]/stars.changes'
+--
+
+Gets the changes that were starred with any label by the identified
+user account. This URL endpoint is functionally identical to the
+changes query `GET /changes/?q=has:stars`. The result is a list of
+link:rest-api-changes.html#change-info[ChangeInfo] entities.
+
+.Request
+----
+  GET /a/accounts/self/stars.changes
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  [
+    {
+      "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
+      "project": "myProject",
+      "branch": "master",
+      "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9940",
+      "subject": "Implementing Feature X",
+      "status": "NEW",
+      "created": "2013-02-01 09:59:32.126000000",
+      "updated": "2013-02-21 11:16:36.775000000",
+      "stars": [
+        "ignore",
+        "risky"
+      ],
+      "mergeable": true,
+      "submittable": false,
+      "insertions": 145,
+      "deletions": 12,
+      "_number": 3965,
+      "owner": {
+        "name": "John Doe"
+      }
+    }
+  ]
+----
+
+[[get-stars]]
+=== Get Star Labels From Change
+--
+'GET /accounts/link:#account-id[\{account-id\}]/stars.changes/link:rest-api-changes.html#change-id[\{change-id\}]'
+--
+
+Get star labels from a change.
+
+.Request
+----
+  GET /a/accounts/self/stars.changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940 HTTP/1.0
+----
+
+As response the star labels that the user applied on the change are
+returned. The labels are lexicographically sorted.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  [
+    "blue",
+    "green",
+    "red"
+  ]
+----
+
+[[set-stars]]
+=== Update Star Labels On Change
+--
+'POST /accounts/link:#account-id[\{account-id\}]/stars.changes/link:rest-api-changes.html#change-id[\{change-id\}]'
+--
+
+Update star labels on a change. The star labels to be added/removed
+must be specified in the request body as link:#star-input[StarInput]
+entity. Starred changes are returned for the search query `has:stars`.
+
+.Request
+----
+  POST /a/accounts/self/stars.changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940 HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "add": [
+      "blue",
+      "red"
+    ],
+    "remove": [
+      "yellow"
+    ]
+  }
+----
+
+As response the star labels that the user applied on the change are
+returned. The labels are lexicographically sorted.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  [
+    "blue",
+    "green",
+    "red"
+  ]
+----
+
 [[ids]]
 == IDs
 
@@ -1693,8 +1972,8 @@
 |`context`                     ||
 The number of lines of context when viewing a patch.
 |`theme`                       ||
-The CodeMirror theme. Currently only a subset of light and dark
-CodeMirror themes are supported.
+The CodeMirror theme name in upper case, for example `DEFAULT`. All the themes
+from the CodeMirror release that Gerrit is using are available.
 |`expand_all_comments`         |not set if `false`|
 Whether all inline comments should be automatically expanded.
 |`ignore_whitespace`           ||
@@ -1807,16 +2086,17 @@
 |===========================================
 |Field Name                    ||Description
 |`theme`                       ||
-The CodeMirror theme. Currently only a subset of light and dark
-CodeMirror themes are supported. Light themes `DEFAULT`, `ECLIPSE`,
-`ELEGANT`, `NEAT`. Dark themes `MIDNIGHT`, `NIGHT`, `TWILIGHT`.
+The CodeMirror theme name in upper case, for example `DEFAULT`. All the themes
+from the CodeMirror release that Gerrit is using are available.
 |`key_map_type`                ||
 The CodeMirror key map. Currently only a subset of key maps are
-supported: `DEFAULT`, `EMACS`, `VIM`.
+supported: `DEFAULT`, `EMACS`, `SUBLIME`, `VIM`.
 |`tab_size`                    ||
 Number of spaces that should be used to display one tab.
 |`line_length`                 ||
 Number of characters that should be displayed per line.
+|`indent_unit`                 ||
+Number of spaces that should be used for auto-indent.
 |`cursor_blink_rate`           ||
 Half-period in milliseconds used for cursor blinking.
 Setting it to 0 disables cursor blinking.
@@ -1941,7 +2221,7 @@
 Whether the site header should be shown.
 |`use_flash_clipboard`          |not set if `false`|
 Whether to use the flash clipboard widget.
-|`download_scheme`              ||
+|`download_scheme`              |optional|
 The type of download URL the user prefers to use. May be any key from
 the `schemes` map in
 link:rest-api-config.html#download-info[DownloadInfo].
@@ -1978,7 +2258,7 @@
 |`url_aliases`                  |optional|
 A map of URL path pairs, where the first URL path is an alias for the
 second URL path.
-|`email_notifications`          ||
+|`email_strategy`               ||
 The type of email strategy to use. On `ENABLED`, the user will receive emails
 from Gerrit. On `CC_ON_OWN_COMMENTS` the user will also receive emails for
 their own comments. On `DISABLED` the user will not receive any email
@@ -2036,6 +2316,12 @@
 |`url_aliases`                  |optional|
 A map of URL path pairs, where the first URL path is an alias for the
 second URL path.
+|`email_strategy`               |optional|
+The type of email strategy to use. On `ENABLED`, the user will receive emails
+from Gerrit. On `CC_ON_OWN_COMMENTS` the user will also receive emails for
+their own comments. On `DISABLED` the user will not receive any email
+notifications from Gerrit.
+Allowed values are `ENABLED`, `CC_ON_OWN_COMMENTS`, `DISABLED`.
 |============================================
 
 [[query-limit-info]]
@@ -2066,6 +2352,18 @@
 |`valid`         ||Whether the SSH key is valid.
 |=============================
 
+[[stars-input]]
+=== StarsInput
+The `StarsInput` entity contains star labels that should be added to
+or removed from a change.
+
+[options="header",cols="1,^1,5"]
+|========================
+|Field Name ||Description
+|`add`      |optional|List of labels to add to the change.
+|`remove`   |optional|List of labels to remove from the change.
+|========================
+
 [[username-input]]
 === UsernameInput
 The `UsernameInput` entity contains information for setting the
@@ -2077,6 +2375,22 @@
 |`username` |The new username of the account.
 |=======================
 
+[[project-watch-info]]
+=== ProjectWatchInfo
+The `WatchedProjectsInfo` entity contains information about a project watch
+for a user.
+
+[options="header",cols="1,^1,5"]
+|=======================
+|Field Name                 |        |Description
+|`project`                  |        |The name of the project.
+|`filter`                   |optional|A filter string to be applied to the project.
+|`notify_new_changes`       |optional|Notify on new changes.
+|`notify_new_patch_sets`    |optional|Notify on new patch sets.
+|`notify_all_comments`      |optional|Notify on comments.
+|`notify_submitted_changes` |optional|Notify on submitted changes.
+|`notify_abandoned_changes` |optional|Notify on abandoned changes.
+|=======================
 
 GERRIT
 ------
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 51ba60f..b2cd1fe 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -1809,6 +1809,7 @@
        "subject":"Use an EventBus to manage star icons",
        "message":"Use an EventBus to manage star icons\n\nImage widgets that need to ..."
     },
+    "base_revision":"c35558e0925e6985c91f3a16921537d5e572b7a3"
   }
 ----
 
@@ -1952,6 +1953,9 @@
 If only the content type is required, callers should use HEAD to
 avoid downloading the encoded file contents.
 
+If the `base` parameter is set to true, the returned content is from the
+revision that the edit is based on.
+
 .Response
 ----
   HTTP/1.1 200 OK
@@ -2721,6 +2725,7 @@
   Content-Type: application/json; charset=UTF-8
 
   {
+    "tag": "jenkins",
     "message": "Some nits need to be fixed.",
     "labels": {
       "Code-Review": -1
@@ -3752,6 +3757,66 @@
 The `context` parameter can be specified to control the number of lines of surrounding context
 in the diff.  Valid values are `ALL` or number of lines.
 
+[[get-blame]]
+=== Get Blame
+--
+'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/files/link:#file-id[\{file-id\}]/blame'
+--
+
+Gets the blame of a file from a certain revision.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/files/gerrit-server%2Fsrc%2Fmain%2Fjava%2Fcom%2Fgoogle%2Fgerrit%2Fserver%2Fproject%2FRefControl.java/blame HTTP/1.0
+----
+
+As response a link:#blame-info[BlameInfo] entity is returned that describes the
+blame.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]
+  {
+    [
+      {
+        "author": "Joe Daw",
+        "id": "64e140b4de5883a4dd74d06c2b62ccd7ffd224a7",
+        "time": 1421441349,
+        "commit_msg": "RST test\n\nChange-Id: I11e9e24bd122253f4bb10c36dce825ac2410d646\n",
+        "ranges": [
+          {
+            "start": 1,
+            "end": 10
+          },
+          {
+            "start": 16,
+            "end": 296
+          }
+        ]
+      },
+      {
+        "author": "Jane Daw",
+        "id": "8d52621a0e2ac6adec73bd3a49f2371cd53137a7",
+        "time": 1421825421,
+        "commit_msg": "add banner\n\nChange-Id: I2eced9b2691015ae3c5138f4d0c4ca2b8fb15be9\n",
+        "ranges": [
+          {
+            "start": 13,
+            "end": 13
+          }
+        ]
+      }
+    ]
+  }
+----
+
+The `base` parameter can be specified to control the base patch set from which
+the blame should be generated.
+
 [[set-reviewed]]
 === Set Reviewed
 --
@@ -3970,6 +4035,27 @@
 permitted to vote on that label.
 |`date`        |optional|
 The time and date describing when the approval was made.
+|`tag`                 |optional|
+Value of the `tag` field from link:#review-input[ReviewInput] set
+while posting the review.
+NOTE: To apply different tags on on different votes/comments multiple
+invocations of the REST call are required.
+|===========================
+
+[[blame-info]]
+=== BlameInfo
+The `BlameInfo` entity stores the commit metadata with the row coordinates where
+it applies.
+
+[options="header",cols="1,6"]
+|===========================
+|Field Name | Description
+|`author`     | The author of the commit.
+|`id`         | The id of the commit.
+|`time`       | Commit time.
+|`commit_msg` | The commit message.
+|`ranges`     |
+The blame row coordinates as link:#range-info[RangeInfo] entities.
 |===========================
 
 [[change-edit-input]]
@@ -4027,7 +4113,10 @@
 The link:rest-api.html#timestamp[timestamp] of when the change was
 submitted.
 |`starred`            |not set if `false`|
-Whether the calling user has starred this change.
+Whether the calling user has starred this change with the default label.
+|`stars`              |optional|
+A list of star labels that are applied by the calling user to this
+change. The labels are lexicographically sorted.
 |`reviewed`           |not set if `false`|
 Whether the change was reviewed by the calling user.
 Only set if link:#reviewed[reviewed] is requested.
@@ -4132,6 +4221,11 @@
 |`date`            ||
 The link:rest-api.html#timestamp[timestamp] this message was posted.
 |`message`            ||The text left by the user.
+|`tag`                 |optional|
+Value of the `tag` field from link:#review-input[ReviewInput] set
+while posting the review.
+NOTE: To apply different tags on on different votes/comments multiple
+invocations of the REST call are required.
 |`_revision_number`    |optional|
 Which patchset (if any) generated this message.
 |==================================
@@ -4182,6 +4276,11 @@
 The author of the message as an
 link:rest-api-accounts.html#account-info[AccountInfo] entity. +
 Unset for draft comments, assumed to be the calling user.
+|`tag`                 |optional|
+Value of the `tag` field from link:#review-input[ReviewInput] set
+while posting the review.
+NOTE: To apply different tags on on different votes/comments multiple
+invocations of the REST call are required.
 |===========================
 
 [[comment-input]]
@@ -4220,6 +4319,10 @@
 The comment message. +
 If not set and an existing draft comment is updated, the existing draft
 comment is deleted.
+|`tag`         |optional, drafts only|
+Value of the `tag` field. Only allowed on link:#create-draft[draft comment] +
+inputs; for published comments, use the `tag` field in +
+link#review-input[ReviewInput]
 |===========================
 
 [[comment-range]]
@@ -4382,15 +4485,15 @@
 
 [options="header",cols="1,^1,5"]
 |===========================
-|Field Name    ||Description
-|`commit`      ||The commit of change edit as
+|Field Name     ||Description
+|`commit`       ||The commit of change edit as
 link:#commit-info[CommitInfo] entity.
-|`baseRevision`||The revision of the patch set change edit is based on.
-|`fetch`       ||
+|`base_revision`||The revision of the patch set the change edit is based on.
+|`fetch`        ||
 Information about how to fetch this patch set. The fetch information is
 provided as a map that maps the protocol name ("`git`", "`http`",
 "`ssh`") to link:#fetch-info[FetchInfo] entities.
-|`files`       |optional|
+|`files`        |optional|
 The files of the change edit as a map that maps the file names to
 link:#file-info[FileInfo] entities.
 |===========================
@@ -4579,9 +4682,9 @@
 
 [options="header",cols="1,^1,5"]
 |===========================
-|Field Name   ||Description
-|`destination`||Destination branch
-|`message`    |optional|
+|Field Name          ||Description
+|`destination_branch`||Destination branch
+|`message`           |optional|
 A message to be posted in this change's comments
 |===========================
 
@@ -4621,6 +4724,17 @@
 link:rest-api-accounts.html#gpg-key-info[GpgKeyInfo] entity.
 |===========================
 
+[[range-info]]
+=== RangeInfo
+The `RangeInfo` entity stores the coordinates of a range.
+
+[options="header",cols="1,6"]
+|===========================
+|Field Name   | Description
+|`start`      | First index.
+|`end`        | Last index.
+|===========================
+
 [[rebase-input]]
 === RebaseInput
 The `RebaseInput` entity contains information for changing parent when rebasing.
@@ -4715,6 +4829,11 @@
 |Field Name               ||Description
 |`message`                |optional|
 The message to be added as review comment.
+|`tag`                    |optional|
+Apply this tag to the review comment message, votes, and inline
+comments. Tags may be used by CI or other automated systems to
+distinguish them from human reviews. Comments with specific tag
+values can be filtered out in the web UI.
 |`labels`                 |optional|
 The votes that should be added to the revision as a map that maps the
 label names to the voting values.
@@ -4893,6 +5012,11 @@
 API]. Using this option requires
 link:access-control.html#category_submit_on_behalf_of[Submit (On Behalf Of)]
 permission on the branch.
+|`notify`|optional|
+Notify handling that defines to whom email notifications should be sent after
+the change is submitted. +
+Allowed values are `NONE`, `OWNER`, `OWNER_REVIEWERS` and `ALL`. +
+If not set, the default is `ALL`.
 |===========================
 
 [[submit-record]]
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index 3a64dc0..661abb0 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -1076,6 +1076,9 @@
 [options="header",cols="1,^1,5"]
 |=============================
 |Field Name           ||Description
+|`allow_blame`        |not set if `false`|
+link:config-gerrit.html#change.allowBlame[Whether blame on side by side diff is
+allowed].
 |`allow_drafts`       |not set if `false`|
 link:config-gerrit.html#change.allowDrafts[Whether draft workflow is
 allowed].
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 2701eb9..736b1b9 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -1651,7 +1651,7 @@
   }
 ----
 
-[[get-content]]
+[[get-content-from-commit]]
 === 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'
@@ -2472,6 +2472,61 @@
 The path to the `GerritSiteFooter.html` file.
 |=============================
 
+[[get-access]]
+=== List Access Rights for Project
+--
+'GET //projects/link:rest-api-projects.html#project-name[\{project-name\}]/access'
+--
+
+Lists the access rights for a single project.
+
+As result a link:#project-access-info[ProjectAccessInfo] entity is returned.
+
+.Request
+----
+  GET /projects/MyProject/access HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "revision": "61157ed63e14d261b6dca40650472a9b0bd88474",
+    "inherits_from": {
+      "id": "All-Projects",
+      "name": "All-Projects",
+      "description": "Access inherited by all other projects."
+    },
+    "local": {
+        "refs/*": {
+          "permissions": {
+            "read": {
+              "rules": {
+                "c2ce4749a32ceb82cd6adcce65b8216e12afb41c": {
+                  "action": "ALLOW",
+                  "force": false
+                },
+                "global:Anonymous-Users": {
+                  "action": "ALLOW",
+                  "force": false
+                }
+              }
+            }
+          }
+        }
+    },
+    "is_owner": true,
+    "owner_of": [
+      "refs/*"
+    ],
+    "can_upload": true,
+    "can_add": true,
+    "config_visible": true
+  }
+----
 
 GERRIT
 ------
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index b2b3614..b04898e 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -247,25 +247,43 @@
 Regular expression matching can be enabled by starting the string
 with `^`. In this mode `file:` is an alias of `path:` (see above).
 
+[[star]]
+star:'LABEL'::
++
+Matches any change that was starred by the current user with the label
+'LABEL'.
++
+E.g. if changes that are not interesting are marked with an `ignore`
+star, they could be filtered out by '-star:ignore'.
++
+'star:star' is the same as 'has:star' and 'is:starred'.
+
 [[has]]
 has:draft::
 +
 True if there is a draft comment saved by the current user.
 
+[[has-star]]
 has:star::
 +
-Same as 'is:starred', true if the change has been starred by the
-current user.
+Same as 'is:starred' and 'star:star', true if the change has been
+starred by the current user with the default label.
+
+[[has-stars]]
+has:stars::
++
+True if the change has been starred by the current user with any label.
 
 has:edit::
 +
 True if the change has inline edit created by the current user.
 
 [[is]]
+[[is-starred]]
 is:starred::
 +
 Same as 'has:star', true if the change has been starred by the
-current user.
+current user with the default label.
 
 is:watched::
 +
@@ -510,7 +528,7 @@
 
 starredby:'USER'::
 +
-Matches changes that have been starred by 'USER'.
+Matches changes that have been starred by 'USER' with the default label.
 The special case `starredby:self` applies to the caller.
 
 watchedby:'USER'::
diff --git a/Documentation/user-submodules.txt b/Documentation/user-submodules.txt
index a8a0262..8f75bf1 100644
--- a/Documentation/user-submodules.txt
+++ b/Documentation/user-submodules.txt
@@ -46,9 +46,9 @@
 With this feature, one could attach 'sub' inside of 'super' repository
 at path 'sub' by executing the following command when being inside
 'super':
-=====
+====
 git submodule add ssh://server/sub sub
-=====
+====
 
 Still considering the above example, after its execution notice that
 inside the local repository 'super' the 'sub' folder is considered a
diff --git a/ReleaseNotes/BUCK b/ReleaseNotes/BUCK
new file mode 100644
index 0000000..0f47808
--- /dev/null
+++ b/ReleaseNotes/BUCK
@@ -0,0 +1,19 @@
+include_defs('//Documentation/asciidoc.defs')
+include_defs('//ReleaseNotes/config.defs')
+
+DIR = 'ReleaseNotes'
+
+SRCS = glob(['*.txt'])
+
+
+genasciidoc(
+  name = 'html',
+  out = 'html.zip',
+  directory = DIR,
+  srcs = SRCS,
+  attributes = release_notes_attributes(),
+  backend = 'html5',
+  searchbox = False,
+  resources = False,
+  visibility = ['PUBLIC'],
+)
diff --git a/ReleaseNotes/Makefile b/ReleaseNotes/Makefile
deleted file mode 100644
index 3081600..0000000
--- a/ReleaseNotes/Makefile
+++ /dev/null
@@ -1,47 +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.
-
-ASCIIDOC       ?= asciidoc
-ASCIIDOC_EXTRA ?=
-
-DOC_HTML      := $(patsubst %.txt,%.html,$(wildcard ReleaseNotes*.txt))
-
-all: html
-
-html: index.html $(DOC_HTML)
-
-clean:
-	rm -f *.html
-
-index.html: index.txt
-	@echo FORMAT $@
-	@rm -f $@+ $@
-	@$(ASCIIDOC) --unsafe \
-		-a toc \
-		-b xhtml11 -f asciidoc.conf \
-		$(ASCIIDOC_EXTRA) -o $@+ $<
-	@mv $@+ $@
-
-$(DOC_HTML): %.html : %.txt
-	@echo FORMAT $@
-	@rm -f $@+ $@
-	@v=$$(echo $< | sed 's/^ReleaseNotes-//;s/.txt$$//;') && \
-	 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" \
-		-b xhtml11 -f asciidoc.conf \
-		$(ASCIIDOC_EXTRA) -o $@+ $<
-	@mv $@+ $@
diff --git a/ReleaseNotes/ReleaseNotes-2.0.10.txt b/ReleaseNotes/ReleaseNotes-2.0.10.txt
index 695be4f..33078d9 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.10.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.10.txt
@@ -1,13 +1,11 @@
-Release notes for Gerrit 2.0.10
-===============================
+= Release notes for Gerrit 2.0.10
 
 Gerrit 2.0.10 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
 
-New Features
-------------
+== New Features
 
 * GERRIT-129  Make the browser window title reflect the current scre...
 +
@@ -25,8 +23,7 @@
 +
 Minor enhancement to the way submitted emails are formatted.
 
-Bug Fixes
----------
+== Bug Fixes
 
 * GERRIT-91   Delay updating the UI until a Screen instance is fully...
 +
@@ -46,8 +43,7 @@
 * GERRIT-135  Enable Save button after paste in a comment editor
 * GERRIT-137  Error out if a user forgets to squash when replacing a...
 
-Other Changes
--------------
+== Other Changes
 * Start 2.0.10 development
 * Add missing super.onSign{In,Out} calls to ChangeScreen
 * Remove the now pointless sign in callback support
diff --git a/ReleaseNotes/ReleaseNotes-2.0.11.txt b/ReleaseNotes/ReleaseNotes-2.0.11.txt
index 62f2a18..5bd6ca0 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.11.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.11.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.0.11
-===============================
+= Release notes for Gerrit 2.0.11
 
 Gerrit 2.0.11 is now available in the usual location:
 
@@ -12,11 +11,9 @@
   java -jar gerrit.war --cat sql/upgrade009_010.sql | psql reviewdb
 ----
 
-Important Notes
----------------
+== Important Notes
 
-Cache directory
-~~~~~~~~~~~~~~~
+=== Cache directory
 
 Gerrit now prefers having a temporary directory to store a disk-based content cache.  This cache used to be in the PostgreSQL database, and was the primary reason for the rather large size of the Gerrit schema.  In 2.0.11 the cache has been moved to the local filesystem, and now has automatic expiration management to prevent it from growing too large.  As this is only a cache, making backups of this directory is not required.
 
@@ -30,13 +27,11 @@
 
 link:http://gerrit.googlecode.com/svn/documentation/2.0/config-gerrit.html[http://gerrit.googlecode.com/svn/documentation/2.0/config-gerrit.html]
 
-Protocol change
-~~~~~~~~~~~~~~~
+=== Protocol change
 
 The protocol between the browser based JavaScript and the server has changed.  After installing 2.0.11 users need to load the site page again to ensure they are running 2.0.11 or later.  Users can verify they have the new version by checking the version number in the footer in the lower right.  Users who don't load the new version (e.g. are using a stale tab from a week ago) will see errors when trying to view patches.
 
-New Features
-------------
+== New Features
 
 * GERRIT-8    Add 'Whole File' as a context preference in the user s...
 * GERRIT-9    Honor user's "Default Context" preference
@@ -65,8 +60,7 @@
 +
 Simple DWIMery: users can now do `repo upload --reviewer=who` to have the reviewer email automatically expand according to the email_format column in system_config, e.g. by expanding `who` to `who@example.com`.
 
-Bug Fixes
----------
+== Bug Fixes
 
 * GERRIT-81   Can't repack a repository while Gerrit is running
 +
@@ -80,8 +74,7 @@
 +
 Service users created by manually inserting into the accounts table didn't permit using their preferred_email in commits or tags; administrators had to also insert a dummy record into the account_external_ids table.  The dummy account_external_ids record is no longer necessary.
 
-Other Changes
--------------
+== Other Changes
 * Start 2.0.11 development
 * Include the 'Google Format' style we selected in our p...
 * Upgrade JGit to v0.4.0-310-g3da8761
diff --git a/ReleaseNotes/ReleaseNotes-2.0.12.txt b/ReleaseNotes/ReleaseNotes-2.0.12.txt
index eb28e2e..0e1df04 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.12.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.12.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.0.12
-===============================
+= Release notes for Gerrit 2.0.12
 
 Gerrit 2.0.12 is now available in the usual location:
 
@@ -12,21 +11,17 @@
   java -jar gerrit.war --cat sql/upgrade010_011.sql | psql reviewdb
 ----
 
-Important Notes
----------------
+== Important Notes
 
-Java 6 Required
-~~~~~~~~~~~~~~~
+=== Java 6 Required
 
 Gerrit now requires running within a Java 6 (or later) JVM.
 
-Protocol change
-~~~~~~~~~~~~~~~
+=== Protocol change
 
 The protocol between the browser based JavaScript and the server has changed.  After installing 2.0.12 users need to load the site page again to ensure they are running 2.0.12 or later.  Users can verify they have the new version by checking the version number in the footer in the lower right.  Users who don't load the new version (e.g. are using a stale tab from a week ago) will see errors when trying to view patches.
 
-New Features
-------------
+== New Features
 * Honor --reviewer=not.preferred.email during upload
 * Also scan by preferred email for --reviewers and --cc ...
 +
@@ -73,8 +68,7 @@
 +
 Keyboard bindings have been completely overhauled in this release, and should now work on every browser.  Press '?' in any context to see the available actions.  Please note that this help is context sensitive, so you will only see keys that make sense in the current context.  Actions in a user dashboard screen differ from actions in a patch (for example), but where possible the same key is used when the logical meaning is unchanged.
 
-Bug Fixes
----------
+== Bug Fixes
 * Ignore "SshException: Already closed" errors
 +
 Hides some non-errors from the log file.
@@ -83,8 +77,7 @@
 +
 Should be a minor improvement for MSIE 6 users.
 
-Other Changes
--------------
+== Other Changes
 * Start 2.0.12 development
 * Report what version we want on a schema version mismat...
 * Remove unused imports in SshServlet
diff --git a/ReleaseNotes/ReleaseNotes-2.0.13.txt b/ReleaseNotes/ReleaseNotes-2.0.13.txt
index 8ec13a8..7589568 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.13.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.13.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.0.13, 2.0.13.1
-=========================================
+= Release notes for Gerrit 2.0.13, 2.0.13.1
 
 Gerrit 2.0.13.1 is now available in the usual location:
 
@@ -23,8 +22,7 @@
   java -jar gerrit.war --cat sql/upgrade011_012_part2.sql | psql reviewdb
 ----
 
-Configuration Mapping
----------------------
+== Configuration Mapping
 || *system_config*                || *$site_path/gerrit.config*     ||
 || max_session_age                || auth.maxSessionAge             ||
 || canonical_url                  || gerrit.canonicalWebUrl         ||
@@ -45,8 +43,7 @@
 
 See also [http://gerrit.googlecode.com/svn/documentation/2.0/config-gerrit.html Gerrit2 Configuration].
 
-New Features
-------------
+== New Features
 * GERRIT-180  Rewrite outgoing email to be more user friendly
 +
 A whole slew of feature improvements on outgoing email formatting was closed by this one (massive) rewrite of the outgoing email implementation.
@@ -81,8 +78,7 @@
 +
 The new `sendemail` section of `$site_path/gerrit.config` now controls the configuration of the outgoing SMTP server, rather than relying upon a JNDI resource.  See [http://gerrit.googlecode.com/svn/documentation/2.0/config-gerrit.html configuration] section sendemail for more details.
 
-Bug Fixes
----------
+== Bug Fixes
 * Fix file browser in patch that is taller than the wind...
 * GERRIT-184  Make 'f' toggle the file browser popup closed
 * GERRIT-188  Fix key bindings in patch when changing the old or new...
@@ -123,8 +119,7 @@
 +
 Bug fixes identified after release of 2.0.13, rolled into 2.0.13.1.
 
-Other Changes
--------------
+== Other Changes
 * Start 2.0.13 development
 * Use gwtexpui 1.1.1-SNAPSHOT
 * Document the Patch.PatchType and Patch.ChangeType enum
diff --git a/ReleaseNotes/ReleaseNotes-2.0.14.txt b/ReleaseNotes/ReleaseNotes-2.0.14.txt
index de58035..128036d 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.14.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.14.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.0.14, 2.0.14.1
-=========================================
+= Release notes for Gerrit 2.0.14, 2.0.14.1
 
 Gerrit 2.0.14.1 is now available in the usual location:
 
@@ -13,8 +12,7 @@
   java -jar gerrit.war --cat sql/upgrade012_013_mysql.sql | mysql reviewdb
 ----
 
-New Features
-------------
+== New Features
 * GERRIT-177  Display branch name next to project in change list
 +
 Now its easier to see from your Mine>Changes what branch each change goes to.  For some users this may help prioritize reviews.
@@ -53,8 +51,7 @@
 +
 This is really for the server admin, the Git reflogs are now more likely to contain actual user information in them, rather than generic "gerrit2@localhost" identities.  This may help if you are mining "WTF happened to this branch" data from Git directly.
 
-Bug Fixes
----------
+== Bug Fixes
 * GERRIT-213  Fix n/p on a file with only one edit
 * GERRIT-66   Always show comments in patch views, even if no edit e...
 * Correctly handle comments after last hunk of patch
@@ -81,8 +78,7 @@
 +
 Fixed run-on addresses when more than one user was listed in To/CC headers.
 
-Other Changes
--------------
+== Other Changes
 * Start 2.0.14 development (again)
 * Small doc updates.
 * Merge change 10282
diff --git a/ReleaseNotes/ReleaseNotes-2.0.15.txt b/ReleaseNotes/ReleaseNotes-2.0.15.txt
index a87cba1..a8d60a4 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.15.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.15.txt
@@ -1,23 +1,19 @@
-Release notes for Gerrit 2.0.15
-===============================
+= Release notes for Gerrit 2.0.15
 
 Gerrit 2.0.15 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
-Schema Change
--------------
+== Schema Change
 
 None.  For a change.  :-)
 
-New Features
-------------
+== New Features
 * Allow other ignore whitespace settings beyond IGNORE_S...
 +
 Now you can ignore whitespace inside the middle of a line, in addition to on the ends.
 
-Bug Fixes
----------
+== Bug Fixes
 * Update SSHD to include SSHD-28 (deadlock on close) bug...
 +
 Fixes a major stability problem with the internal SSHD.  Without this patch the daemon can become unresponsive, requiring a complete JVM restart to recover the daemon.  The symptom is connections appear to work sporadically... some connections are fine while others freeze during setup, or during data transfer.
@@ -31,8 +27,7 @@
 +
 Stupid bugs in the patch viewing code.  Random server errors and/or client UI crashes.
 
-Other Changes
--------------
+== Other Changes
 * Restart 2.0.15 development
 * Update JGit to 0.4.0-411-g8076bdb
 * Remove dead isGerrit method from AbstractGitCommand
diff --git a/ReleaseNotes/ReleaseNotes-2.0.16.txt b/ReleaseNotes/ReleaseNotes-2.0.16.txt
index 4d0252d..4f5a5ba 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.16.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.16.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.0.16
-===============================
+= Release notes for Gerrit 2.0.16
 
 Gerrit 2.0.16 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
-Schema Change
--------------
+== Schema Change
 
 *WARNING: This version contains a schema change* (since 2.0.14)
 
@@ -16,8 +14,7 @@
   java -jar gerrit.war --cat sql/upgrade013_014_mysql.sql | mysql reviewdb
 ----
 
-New Features
-------------
+== New Features
 * Search for changes created or reviewed by a user
 +
 The search box in the upper right corner now accepts "owner:email" and "reviewer:email", in addition to change numbers and commit SHA-1s.  Using owner: and reviewer: is not the most efficient query plan, as potentially the entire database is scanned.  We hope to improve on that as we move to a pure git based backend.
@@ -42,8 +39,7 @@
 +
 The "/Gerrit" suffix is no longer necessary in the URL.  Gerrit now favors just "/" as its path location.  This drops one redirection during initial page loading, slightly improving page loading performance, and making all URLs 6 characters shorter.  :-)
 
-Bug Fixes
----------
+== Bug Fixes
 * Don't create reflogs for patch set refs
 +
 Previously Gerrit created pointless 1 record reflogs for each change ref under refs/changes/.  These waste an inode on the local filesystem and provide no metadata value, as the same information is also stored in the metadata database.  These reflogs are no longer created.
@@ -64,8 +60,7 @@
 +
 If the hostname is "localhost" or "127.0.0.1", such as might happen when a user tries to proxy through an SSH tunnel, we honor the hostname anyway if OpenID is not being used.
 
-Other Changes
--------------
+== Other Changes
 * Start 2.0.16 development
 * Update JGit to 0.4.9-18-g393ad45
 * Name replication threads by their remote name
diff --git a/ReleaseNotes/ReleaseNotes-2.0.17.txt b/ReleaseNotes/ReleaseNotes-2.0.17.txt
index 493a64b..8a24b22 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.17.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.17.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.0.17
-===============================
+= Release notes for Gerrit 2.0.17
 
 Gerrit 2.0.17 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
-Schema Change
--------------
+== Schema Change
 
 *WARNING: This version contains a schema change* (since 2.0.16)
 
@@ -22,8 +20,7 @@
   java -jar gerrit.war --cat sql/upgrade014_015_part2.sql | mysql reviewdb
 ----
 
-New Features
-------------
+== New Features
 * Add '[' and ']' shortcuts to PatchScreen.
 +
 The keys '[' and ']' can be used to navigate to previous and next file in a patch set.
@@ -56,8 +53,7 @@
 +
 The owner of a project was moved from the General tab to the Access Rights tab, under a new category called Owner.  This permits multiple groups to be designated the Owner of the project (simply grant Owner status to each group).
 
-Bug Fixes
----------
+== Bug Fixes
 * Permit author Signed-off-by to be optional
 +
 If a project requires Signed-off-by tags to appear the author tag is now optional, only the committer/uploader must provide a Signed-off-by tag.
@@ -83,8 +79,7 @@
 +
 Instead of crashing on a criss-cross merge case, Gerrit unsubmits the change and attaches a message, like it does when it encounters a path conflict.
 
-Other Changes
--------------
+== Other Changes
 * Start 2.0.17 development
 * Move '[' and ']' key bindings to Navigation category
 * Use gwtexpui 1.1.2-SNAPSHOT to fix navigation keys
diff --git a/ReleaseNotes/ReleaseNotes-2.0.18.txt b/ReleaseNotes/ReleaseNotes-2.0.18.txt
index df635d9..1028185 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.18.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.18.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.0.18
-===============================
+= Release notes for Gerrit 2.0.18
 
 Gerrit 2.0.18 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
-Important Notices
------------------
+== Important Notices
 
 Please ensure you read the following important notices about this release; .18 is a much larger release than usual.
 
@@ -41,8 +39,7 @@
 the SSH authentication system.  More details can be found in the
 [http://android.git.kernel.org/?p=tools/gerrit.git;a=commit;h=080b40f7bbe00ac5fc6f2b10a861b63ce63e8add commit message].
 
-Schema Change
--------------
+== Schema Change
 
 *WARNING: This version contains a schema change* (since 2.0.17)
 
@@ -71,15 +68,13 @@
   java -jar gerrit.war --cat sql/upgrade015_016_part2.sql | mysql reviewdb
 ----
 
-New Bugs
---------
+== New Bugs
 * Memory leaks during warm restarts
 
 2.0.18 includes [http://code.google.com/p/google-guice/ Google Guice], which leaves a finalizer thread dangling when the Gerrit web application is halted by the servlet container.  As this thread does not terminate, the web context stays loaded in memory indefinitely, creating a memory leak.  Cold restarting the container in order to restart Gerrit is highly recommended.
 
 
-New Features
-------------
+== New Features
 * GERRIT-104  Allow end-users to select their own SSH username
 +
 End users may now select their own SSH username through the web interface.  The username must be unique within a Gerrit server installation.  During upgrades from 2.0.17 duplicate users are resolved by giving the username to the user who most recently logged in under it; other users will need to login through the web interface and select a unique username.  This change was necessary to fix a very minor security bug (see above).
@@ -113,8 +108,7 @@
 +
 As noted above in the section about cache changes, the disk cache is now completely optional.
 
-Bug Fixes
----------
+== Bug Fixes
 * GERRIT-5    Remove PatchSetInfo from database and get it always fr...
 +
 A very, very old bug.  We no longer mirror the commit data into the SQL database, but instead pull it directly from Git when needed.  Removing duplicated data simplifies the data store model, something that is important as we shift from an SQL database to a Git backed database.
@@ -139,8 +133,7 @@
 +
 The database schema changed, adding `patch_set_id` to the approval object, and renaming the approval table to `patch_set_approvals`.  If you have external code writing to this table, uh, sorry, its broken with this release, you'll have to update that code first.  :-\
 
-Other Changes
--------------
+== Other Changes
 
 This release is really massive because the internal code moved from some really ugly static data variables to doing almost everything through Guice injection.  Nothing user visible, but code cleanup that needed to occur before we started making additional changes to the system.
 
diff --git a/ReleaseNotes/ReleaseNotes-2.0.19.txt b/ReleaseNotes/ReleaseNotes-2.0.19.txt
index 0e114c8..c9d9c56 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.19.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.19.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.0.19, 2.0.19.1, 2.0.19.2
-===================================================
+= Release notes for Gerrit 2.0.19, 2.0.19.1, 2.0.19.2
 
 Gerrit 2.0.19.2 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
-Important Notices
------------------
+== Important Notices
 
 * Prior User Sessions
 +
@@ -25,8 +23,7 @@
 set [http://gerrit.googlecode.com/svn/documentation/2.0/config-gerrit.html#cache.directory cache.directory] in gerrit.config.  This allows Gerrit to flush the set
 of active sessions to disk during shutdown, and load them back during startup.
 
-Schema Change
--------------
+== Schema Change
 
 *WARNING: This version contains a schema change* (since 2.0.18)
 
@@ -44,8 +41,7 @@
 ----
 
 
-New Features
-------------
+== New Features
 * New ssh create-project command
 +
 Thanks to Ulrik Sjölin we now have `gerrit create-project`
@@ -171,8 +167,7 @@
 For more details, please see the docs:
 link:http://gerrit.googlecode.com/svn/documentation/2.0/user-changeid.html[http://gerrit.googlecode.com/svn/documentation/2.0/user-changeid.html]
 
-Bug Fixes
----------
+== Bug Fixes
 * Fix yet another ArrayIndexOutOfBounds during side-by-s...
 +
 We found yet another bug with the side-by-side view failing
@@ -230,8 +225,7 @@
 wrong project, e.g. uploading a replacement commit to project
 B while picking a change number from project A.  Fixed.
 
-=Fixes in 2.0.19.1=
--------------------
+== =Fixes in 2.0.19.1=
 
 * Fix NPE during direct push to branch closing a change
 +
@@ -258,8 +252,7 @@
 +
 HTTP_LDAP broke using local usernames to match an account.  Fixed.
 
-=Fixes in 2.0.19.2=
--------------------
+== =Fixes in 2.0.19.2=
 * Don't line wrap project or group names in admin panels
 +
 Line wrapping group names like "All Users" when the description column
@@ -283,8 +276,7 @@
 As reported on repo-discuss, recursive search is sometimes necessary,
 and is now the default.
 
-Removed Features
-----------------
+== Removed Features
 
 * Remove support for /user/email style URLs
 +
@@ -292,8 +284,7 @@
 discoverable.  Its unlikely anyone is really using it, but if
 they are, they could try using "#q,owner:email,n,z" instead.
 
-Other Changes
--------------
+== Other Changes
 
 * Start 2.0.19 development
 * Document the Failure and UnloggedFailure classes in Ba...
diff --git a/ReleaseNotes/ReleaseNotes-2.0.2.txt b/ReleaseNotes/ReleaseNotes-2.0.2.txt
index b2d5b98..eb8546c 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.2.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.0.2
-==============================
+= Release notes for Gerrit 2.0.2
 
 Gerrit 2.0.2 is now available for download:
 
 link:https://www.gerritcodereview.com/[https://www.gerritcodereview.com/]
 
-Important Notes
----------------
+== Important Notes
 
 Starting with this version, Gerrit is now packaged as a single WAR file.
 Just download and drop into your webapps directory for easier deployment.
@@ -31,16 +29,14 @@
 and insert it into your container's CLASSPATH.  But I think all known
 instances are on PostgreSQL, so this is probably not a concern to anyone.
 
-New Features
-------------
+== New Features
 
 * Trailing whitespace is highlighted in diff views
 * SSHD upgraded with "faster connection" patch discussed on list
 * Git reflogs now contain the Gerrit account information of who did the push
 * Insanely long change subjects are now clipped at 80 characters
 
-All Changes
------------
+== All Changes
 
 * Switch back to -SNAPSHOT builds
 * Overhaul our build system to only create a WAR file
diff --git a/ReleaseNotes/ReleaseNotes-2.0.20.txt b/ReleaseNotes/ReleaseNotes-2.0.20.txt
index 527de8e..4f15bb0 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.20.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.20.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.0.20
-===============================
+= Release notes for Gerrit 2.0.20
 
 Gerrit 2.0.20 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
-Schema Change
--------------
+== Schema Change
 
 A prior bug (GERRIT-262) permitted some invalid data to enter into some databases.  Administrators should consider running the following update statement as part of their upgrade to .20 to make any comments which were created with this bug visible:
 ----
@@ -14,8 +12,7 @@
 ----
 Unfortunately the correct position of the comment has been lost, and the statement above will simply position them on the first line of the file.  Fortunately the lost comments were only on the wrong side of an insertion or deletion, and are generally rare.  (On my servers only 0.33% of the comments were created like this.)
 
-New Features
-------------
+== New Features
 * New ssh command approve
 +
 Patch sets can now be approved remotely via SSH.  For more
@@ -31,8 +28,7 @@
 administrators may permit automatically updating an existing
 account with a new identity by matching on the email address.
 
-Bug Fixes
----------
+== Bug Fixes
 * GERRIT-262  Disallow creating comments on line 0
 +
 Users were able to create comments in dead regions of a file.
@@ -59,8 +55,7 @@
 +
 MySQL schema upgrade scripts had a few bugs, fixed.
 
-Other Changes
--------------
+== Other Changes
 * Restart 2.0.20
 * Update MINA SSHD to 0.2.0 release
 * Update args4j to snapshot built from current CVS
diff --git a/ReleaseNotes/ReleaseNotes-2.0.21.txt b/ReleaseNotes/ReleaseNotes-2.0.21.txt
index 34ab581..5de84ff 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.21.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.21.txt
@@ -1,13 +1,11 @@
-Release notes for Gerrit 2.0.21
-===============================
+= Release notes for Gerrit 2.0.21
 
 Gerrit 2.0.21 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
 
-Schema Change
--------------
+== Schema Change
 
 *WARNING: This version contains a schema change* (since 2.0.19)
 
@@ -48,8 +46,7 @@
 ----
 
 
-Important Notices
------------------
+== Important Notices
 
 * Prior User Sessions
 +
@@ -106,8 +103,7 @@
 a supported provider.  I just want to make it clear that I no
 longer recommend it in production.
 
-New Features
-------------
+== New Features
 
 * GERRIT-189  Show approval status in account dashboards
 +
@@ -203,8 +199,7 @@
 to `USER` in gerrit.config.  For more details see
 link:http://gerrit.googlecode.com/svn/documentation/2.0/config-gerrit.html#sendemail.from[sendemail.from]
 
-Bug Fixes
----------
+== Bug Fixes
 
 * Fix ReviewDb to actually be per-request scoped
 +
@@ -276,8 +271,7 @@
 the same email address, or to have the same OpenID auth token.
 Fixed by asserting a unique constraint on the column.
 
-Other Changes
--------------
+== Other Changes
 * Start 2.0.21 development
 * Support cleaning up a Commons DBCP connection pool
 * Clarify which Factory we are importing in ApproveComma...
diff --git a/ReleaseNotes/ReleaseNotes-2.0.22.txt b/ReleaseNotes/ReleaseNotes-2.0.22.txt
index faaca81..5e2f8b5 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.22.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.22.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.0.22
-===============================
+= Release notes for Gerrit 2.0.22
 
 Gerrit 2.0.22 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
-Schema Change
--------------
+== Schema Change
 
 There is no schema change in this release.
 
@@ -32,8 +30,7 @@
    login over SSH until they return and select a new name.
 
 
-New Features
-------------
+== New Features
 * GERRIT-280  create-project: Add --branch and cleanup arguments
 +
 The --branch option to create-project can be used to setup the
@@ -94,8 +91,7 @@
 +
 Sample git pull lines are now included in email notifications.
 
-Bug Fixes
----------
+== Bug Fixes
 * create-project: Document needing to double quote descr...
 +
 The --description flag to create-project require two levels
@@ -141,8 +137,7 @@
 Merge commits created by Gerrit were still using the older style
 integer change number; changed to use the abbreviated Change-Id.
 
-Other Changes
--------------
+== Other Changes
 * Start 2.0.22 development
 * Configure Maven to build with UTF-8 encoding
 * Document minimum build requirement for Mac OS X
diff --git a/ReleaseNotes/ReleaseNotes-2.0.23.txt b/ReleaseNotes/ReleaseNotes-2.0.23.txt
index 16488d4..a3f28a7 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.23.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.23.txt
@@ -1,18 +1,15 @@
-Release notes for Gerrit 2.0.23
-===============================
+= Release notes for Gerrit 2.0.23
 
 Gerrit 2.0.23 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
-Schema Change
--------------
+== Schema Change
 
 There is no schema change in this release.
 
 
-New Features
-------------
+== New Features
 
 * Adding support to list merged and abandoned changes
 +
@@ -22,8 +19,7 @@
 changes in the same project while merged changes link to all merged
 changes in the same project.  These links are bookmarkable.
 
-Bug Fixes
----------
+== Bug Fixes
 
 * Fix new change email to always have SSH pull URL
 * Move git pull URL to bottom of email notifications
@@ -39,8 +35,7 @@
 
 * Fix MySQL CREATE USER example in install documentation
 
-Other Changes
--------------
+== Other Changes
 * Start 2.0.23 development
 * Move Jetty 6.x resources into a jetty6 directory
 * Move the Jetty 6.x start script to our extra directory
diff --git a/ReleaseNotes/ReleaseNotes-2.0.24.txt b/ReleaseNotes/ReleaseNotes-2.0.24.txt
index 1f08582..7da1693 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.24.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.24.txt
@@ -1,13 +1,11 @@
-Release notes for Gerrit 2.0.24, 2.0.24.1, 2.0.24.2
-===================================================
+= Release notes for Gerrit 2.0.24, 2.0.24.1, 2.0.24.2
 
 Gerrit 2.0.24 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
 
-Schema Change
--------------
+== Schema Change
 
 *WARNING: This version contains a schema change* (since 2.0.21)
 
@@ -18,8 +16,7 @@
 ----
 
 
-LDAP Change
------------
+== LDAP Change
 
 LDAP groups are now bound via their full distinguished name, and not
 by their common name.  Sites using LDAP groups will need to have the
@@ -34,8 +31,7 @@
 create identically named groups.
 
 
-New Features
-------------
+== New Features
 * Check if the user has permission to upload changes
 +
 The new READ +2 permission is required to upload a change to a
@@ -69,8 +65,7 @@
 Encrypted SMTP is now supported natively within Gerrit, see
 link:http://gerrit.googlecode.com/svn/documentation/2.0/config-gerrit.html#sendemail.smtpEncryption[sendemail.smtpEncryption]
 
-Bug Fixes
----------
+== Bug Fixes
 * issue 290    Fix invalid drop index in upgrade017_018_mysql
 +
 Minor syntax error in SQL script.
@@ -142,8 +137,7 @@
 identities when the claimed identity is just a delegate to the
 delegate provider.  We now store both in the account.
 
-Fixes in 2.0.24.1
------------------
+== Fixes in 2.0.24.1
 * Fix unused import in OpenIdServiceImpl
 * dev-readme: Fix formatting of initdb command
 +
@@ -159,8 +153,7 @@
 Fixes sendemail configuration to use the documented smtppass
 variable and not the undocumented smtpuserpass variable.
 
-Fixes in 2.0.24.2
------------------
+== Fixes in 2.0.24.2
 * Fix CreateSchema to create Administrators group
 * Fix CreateSchema to set type of Registered Users group
 * Default AccountGroup instances to type INTERNAL
@@ -180,8 +173,7 @@
 Added unit tests to validate CreateSchema works properly, so we
 don't have a repeat of breakage here.
 
-Other Changes
--------------
+== Other Changes
 * Start 2.0.24 development
 * Merge change Ie16b8ca2
 * Switch to the new org.eclipse.jgit package
diff --git a/ReleaseNotes/ReleaseNotes-2.0.3.txt b/ReleaseNotes/ReleaseNotes-2.0.3.txt
index 6bf3510..d319b35 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.3.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.3.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.0.3
-==============================
+= Release notes for Gerrit 2.0.3
 
 Gerrit 2.0.3 is now available in the usual location:
 
@@ -10,8 +9,7 @@
 existing change".  This has been an open issue in the bug tracker for a
 while, and its finally closed thanks to his work.
 
-New Features
-------------
+== New Features
 
 * GERRIT-37  Add additional reviewers to an existing change
 * Display old and new image line numbers in unified diff
@@ -19,8 +17,7 @@
 * Allow up/down arrow keys to scroll the page in patch view
 * Use a Java applet to help users load public SSH keys
 
-Bug Fixes
----------
+== Bug Fixes
 
 * GERRIT-72  Make review comments standout more from the surrounding text
 * GERRIT-7   Restart the merge queue when Gerrit starts up
@@ -36,8 +33,7 @@
 Gerrit UI.  Such a display might be able to convince a user they are
 clicking on one thing, while doing something else entirely.
 
-Other Changes
--------------
+== Other Changes
 
 * Restore -SNAPSHOT suffix after 2.0.2
 * Add a document describing Gerrit's high level design
diff --git a/ReleaseNotes/ReleaseNotes-2.0.4.txt b/ReleaseNotes/ReleaseNotes-2.0.4.txt
index fec2425..0b10756 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.4.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.4.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.0.4
-==============================
+= Release notes for Gerrit 2.0.4
 
 Gerrit 2.0.4 is now available in the usual location:
 
@@ -31,8 +30,7 @@
 individual user's privacy by strongly encrypting their contact
 information, and storing it "off site".
 
-Other Changes
--------------
+== Other Changes
 * Change to 2.0.3-SNAPSHOT
 * Correct grammar in the patch conflict messages
 * Document how to create branches through SSH and web
diff --git a/ReleaseNotes/ReleaseNotes-2.0.5.txt b/ReleaseNotes/ReleaseNotes-2.0.5.txt
index 70116d3..8006e12 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.5.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.5.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.0.5
-==============================
+= Release notes for Gerrit 2.0.5
 
 Gerrit 2.0.5 is now available in the usual location:
 
@@ -15,8 +14,7 @@
 
 link:http://gerrit.googlecode.com/svn/documentation/2.0/config-sso.html[http://gerrit.googlecode.com/svn/documentation/2.0/config-sso.html]
 
-New Features
-------------
+== New Features
 
 * GERRIT-62  Work around IE6's inability to set innerHTML on a tbody ...
 * GERRIT-62  Upgrade to gwtjsonrpc 1.0.2 for ie6 support
@@ -35,14 +33,12 @@
 +
 These features make it easier to copy patch download commands.
 
-Bug Fixes
----------
+== Bug Fixes
 
 * GERRIT-79  Error out with more useful message on "push :refs/change...
 * Invalidate all SSH keys when otherwise flushing all cach...
 
-Other Changes
--------------
+== Other Changes
 
 * Set version 2.0.4-SNAPSHOT
 * Correct note in developer setup about building SSHD
diff --git a/ReleaseNotes/ReleaseNotes-2.0.6.txt b/ReleaseNotes/ReleaseNotes-2.0.6.txt
index 9d0af33..1e28da8 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.6.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.6.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.0.6
-==============================
+= Release notes for Gerrit 2.0.6
 
 Gerrit 2.0.6 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
-New Features
-------------
+== New Features
 
 * GERRIT-41  Add support for abandoning a dead change
 +
@@ -14,8 +12,7 @@
 
 * Bold substrings which match query when showing completi...
 
-Bug Fixes
----------
+== Bug Fixes
 
 * GERRIT-43  Work around Safari 3.2.1 OpenID login problems
 * GERRIT-43  Suggest boosting the headerBufferSize when deploying un...
@@ -24,8 +21,7 @@
 * GERRIT-76  Upgrade to JGit 0.4.0-209-g9c26a41
 * Ensure branches modified through web UI replicate
 
-Other Changes
--------------
+== Other Changes
 
 * Start 2.0.6 development
 * Generate the id for the iframe used during OpenID login
diff --git a/ReleaseNotes/ReleaseNotes-2.0.7.txt b/ReleaseNotes/ReleaseNotes-2.0.7.txt
index afc7784..d1bc38f 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.7.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.7.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.0.7
-==============================
+= Release notes for Gerrit 2.0.7
 
 Gerrit 2.0.7 is now available in the usual location:
 
@@ -11,15 +10,13 @@
 
 Gerrit is still Apache 2/MIT/BSD licensed, despite the switch of a dependency.
 
-New Features
-------------
+== New Features
 
 * GERRIT-103  Display our server host keys for the client to copy an...
 +
 For the paranoid user, they can check the key fingerprint, or even copy the complete host key line for ~/.ssh/known_hosts, directly from Settings > SSH Keys.
 
-Bug Fixes
----------
+== Bug Fixes
 
 * GERRIT-98   Require that a change be open in order to abandon it
 * GERRIT-101  Switch OpenID relying party to openid4java
@@ -34,8 +31,7 @@
 
 * Fix a NullPointerException in OpenIdServiceImpl on res...
 
-Other Changes
--------------
+== Other Changes
 * Start 2.0.7 development
 * Upgrade JGit to 0.4.0-212-g9057f1b
 * Make the sign in dialog a bit taller to avoid clipping...
diff --git a/ReleaseNotes/ReleaseNotes-2.0.8.txt b/ReleaseNotes/ReleaseNotes-2.0.8.txt
index 4b2d10a5..89e7fdd 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.8.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.8.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.0.8
-==============================
+= Release notes for Gerrit 2.0.8
 
 Gerrit 2.0.8 is now available in the usual location:
 
@@ -14,8 +13,7 @@
 This version has some major bug fixes for JGit.  I strongly encourage people to upgrade, we had a number of JGit bugs identified last week, all of them should be fixed in this release.
 
 
-New Features
-------------
+== New Features
 * Allow users to subscribe to submitted change events
 +
 Someone asked me on an IRC channel to have Gerrit send emails when changes are actually merged into a project.  This is what triggered the schema change; there is a new checkbox on the Watched Projects list under Settings to subscribe to these email notifications.
@@ -33,15 +31,13 @@
 +
 The reflogs now contain the remote user's IP address when Gerrit makes edits, resulting in slightly more detail than was there before.
 
-Bug Fixes
----------
+== Bug Fixes
 * Make sure only valid ObjectIds can be passed into git d...
 * GERRIT-92  Upgrade JGit to 0.4.0-262-g3c268c8
 +
 The JGit bug fixes are rather major.  I would strongly encourage upgrading.
 
-Other Changes
--------------
+== Other Changes
 * Start 2.0.8 development
 * Upgrade MINA SSHD to SVN trunk 755651
 * Fix a minor whitespace error in ChangeMail
diff --git a/ReleaseNotes/ReleaseNotes-2.0.9.txt b/ReleaseNotes/ReleaseNotes-2.0.9.txt
index d2a9196..1f683cf 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.9.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.9.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.0.9
-==============================
+= Release notes for Gerrit 2.0.9
 
 Gerrit 2.0.9 is now available in the usual location:
 
@@ -20,8 +19,7 @@
 
 The SQL statement to insert a new project into the database has been changed.  Please see [http://gerrit.googlecode.com/svn/documentation/2.0/project-setup.html Project Setup] for the modified statement.
 
-New Features
-------------
+== New Features
 * GERRIT-69   Make the merge commit message more detailed when mergi...
 * Show the user's starred/not-starred icon in the change...
 * Modify Push Annotated Tag to require signed tags, or r...
@@ -34,8 +32,7 @@
 These last two changes move the hidden gerrit.fastforwardonly feature to the database and the user interface, so project owners can make use of it (or not).  Please see the new 'Change Submit Action' section in the user documentation:
 link:http://gerrit.googlecode.com/svn/documentation/2.0/project-setup.html[http://gerrit.googlecode.com/svn/documentation/2.0/project-setup.html]
 
-Bug Fixes
----------
+== Bug Fixes
 * Work around focus bugs in WebKit based browsers
 * Include our license list in the WAR file
 * Whack any prior submit approvals by myself when replac...
@@ -43,8 +40,7 @@
 * GERRIT-85   ie6: Correct rendering of commit messages
 * GERRIT-89   ie6: Fix date line wrapping in messages
 
-Other Changes
--------------
+== Other Changes
 * Start 2.0.9 development
 * Always show the commit SHA-1 next to the patch set hea...
 * Silence more non-critical log messages from openid4java
diff --git a/ReleaseNotes/ReleaseNotes-2.1.1.txt b/ReleaseNotes/ReleaseNotes-2.1.1.txt
index 9d795b6..38b6caf 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.1.txt
@@ -1,20 +1,17 @@
-Release notes for Gerrit 2.1.1, 2.1.1.1
-=======================================
+= Release notes for Gerrit 2.1.1, 2.1.1.1
 
 Gerrit 2.1.1.1 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
-Schema Change
--------------
+== Schema Change
 
 *WARNING* This release contains a schema change.  To upgrade:
 ----
   java -jar gerrit.war init -d site_path
 ----
 
-Patch 2.1.1.1
--------------
+== Patch 2.1.1.1
 
 * Update MINA SSHD to SVN 897374
 +
@@ -28,8 +25,7 @@
 Discarding a comment from the publish comments screen caused
 a ConcurrentModificationException.  Fixed.
 
-New Features
-------------
+== New Features
 
 * issue 322    Update to GWT 2.0.0
 +
@@ -106,8 +102,7 @@
 by an accidental click.  This is especially useful when there
 is a merge error during submit.
 
-Bug Fixes
----------
+== Bug Fixes
 
 * issue 359    Allow updates of commits where only the parent changes
 +
@@ -158,8 +153,7 @@
 to leak file descriptors, as pipes to the external CGI were not
 always closed.  Fixed.
 
-Other
------
+== Other
 * Switch to ClientBundle
 * Update to gwtexpui-1.2.0-SNAPSHOT
 * Merge branch 'master' into gwt-2.0
diff --git a/ReleaseNotes/ReleaseNotes-2.1.10.txt b/ReleaseNotes/ReleaseNotes-2.1.10.txt
index 5464267..5c5bcc6 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.10.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.10.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.1.10
-===============================
+= Release notes for Gerrit 2.1.10
 
 There are no schema changes from link:ReleaseNotes-2.1.9.html[2.1.9].
 
 link:https://www.gerritcodereview.com/download/gerrit-2.1.10.war[https://www.gerritcodereview.com/download/gerrit-2.1.10.war]
 
-Bug Fixes
----------
+== Bug Fixes
 * Fix clone for modern Git clients
 +
 The security fix in 2.1.9 broke clone for recent Git clients,
diff --git a/ReleaseNotes/ReleaseNotes-2.1.2.1.txt b/ReleaseNotes/ReleaseNotes-2.1.2.1.txt
index ae5d912..b181fee 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.2.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.2.1.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.1.2.1
-================================
+= Release notes for Gerrit 2.1.2.1
 
 Gerrit 2.1.2.1 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
-Bug Fixes
----------
+== Bug Fixes
 
 * Include smart http:// URLs in gitweb
 +
diff --git a/ReleaseNotes/ReleaseNotes-2.1.2.2.txt b/ReleaseNotes/ReleaseNotes-2.1.2.2.txt
index 6565833..305e3e1 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.2.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.2.2.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.1.2.2
-================================
+= Release notes for Gerrit 2.1.2.2
 
 Gerrit 2.1.2.2 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
-Bug Fixes
----------
+== Bug Fixes
 
 * Add ',' to be encoded in email headers.
 +
diff --git a/ReleaseNotes/ReleaseNotes-2.1.2.3.txt b/ReleaseNotes/ReleaseNotes-2.1.2.3.txt
index 3cfbdd1..f81092c 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.2.3.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.2.3.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.1.2.3
-================================
+= Release notes for Gerrit 2.1.2.3
 
 Gerrit 2.1.2.3 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
-Bug Fixes
----------
+== Bug Fixes
 
 * issue 528 gsql: Fix escaping of quotes in JSON
 +
diff --git a/ReleaseNotes/ReleaseNotes-2.1.2.4.txt b/ReleaseNotes/ReleaseNotes-2.1.2.4.txt
index 5e863f7..45fcb40 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.2.4.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.2.4.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.1.2.4
-================================
+= Release notes for Gerrit 2.1.2.4
 
 Gerrit 2.1.2.4 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
-New Features
-------------
+== New Features
 
 * Add 'checkout' download command to patch sets
 +
@@ -14,8 +12,7 @@
 and checkout the patch set on a detached HEAD.  This is more suitable
 for building and testing the change locally.
 
-Bug Fixes
----------
+== Bug Fixes
 
 * issue 545 Fallback to ISO-8859-1 if charset isn't supported
 +
@@ -45,8 +42,7 @@
 in the directory, Gerrit crashed during sign-in while trying to
 clear out the user name.  Fixed.
 
-Documentation Corrections
-~~~~~~~~~~~~~~~~~~~~~~~~~
+=== Documentation Corrections
 
 * documentation: Elaborate on branch level Owner
 +
diff --git a/ReleaseNotes/ReleaseNotes-2.1.2.5.txt b/ReleaseNotes/ReleaseNotes-2.1.2.5.txt
index 6e9a49e..eece1e7 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.2.5.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.2.5.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.1.2.5
-================================
+= Release notes for Gerrit 2.1.2.5
 
 Gerrit 2.1.2.5 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
-Bug Fixes
----------
+== Bug Fixes
 
 * issue 390 Resolve objects going missing
 +
diff --git a/ReleaseNotes/ReleaseNotes-2.1.2.txt b/ReleaseNotes/ReleaseNotes-2.1.2.txt
index e0d8c12..8e7cd5c 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.2.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.1.2
-==============================
+= Release notes for Gerrit 2.1.2
 
 Gerrit 2.1.2 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
-Schema Change
--------------
+== Schema Change
 
 *WARNING* This release contains multiple schema changes.  To upgrade:
 ----
@@ -14,8 +12,7 @@
 ----
 
 
-Breakages
----------
+== Breakages
 
 * issue 421 Force validation of the author and committer lines
 +
@@ -33,11 +30,9 @@
 exists, and Forge Identity +2 where Push Branch >= +1 exists.
 
 
-New Features
-------------
+== New Features
 
-UI - Diff Viewer
-~~~~~~~~~~~~~~~~
+=== UI - Diff Viewer
 
 * issue 169 Highlight line-level (aka word) differences in files
 +
@@ -110,8 +105,7 @@
 * Use RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK for tabs
 * Use a tooltip to explain whitespace errors
 
-UI - Other
-~~~~~~~~~~
+=== UI - Other
 
 * issue 408 Show summary of code review, verified on all open changes
 +
@@ -153,8 +147,7 @@
 Site administrators can now theme the UI with local site colors
 by setting theme variables in gerrit.config.
 
-Permissions
-~~~~~~~~~~~
+=== Permissions
 
 * issue 60 Change permissions to be branch based
 +
@@ -172,8 +165,7 @@
 See link:http://gerrit.googlecode.com/svn/documentation/2.1.2/access-control.html#function_MaxNoBlock[access control]
 for more details on this function.
 
-Remote Access
-~~~~~~~~~~~~~
+=== Remote Access
 
 * Enable smart HTTP under /p/ URLs
 +
@@ -210,8 +202,7 @@
 addition to single quotes.  This can make it easier to intermix
 quoting styles with the shell that is calling the SSH client .
 
-Server Administration
-~~~~~~~~~~~~~~~~~~~~~
+=== Server Administration
 
 * issue 383 Add event hook support
 +
@@ -272,8 +263,7 @@
 software driven queries over SSH easier.  The -c option accepts
 one query, executes it, and returns.
 
-Other
-~~~~~
+=== Other
 
 * Warn when a commit message isn't wrapped
 +
@@ -290,11 +280,9 @@
 rather than from the user's preferred account information.
 
 
-Bug Fixes
----------
+== Bug Fixes
 
-UI
-~~
+=== UI
 
 * Change "Publish Comments" to "Review"
 +
@@ -380,8 +368,7 @@
 * Update URL for GitHub's SSH key guide
 * issue 314 Hide group type choice if LDAP is not enabled
 
-Email
-~~~~~
+=== Email
 
 * Send missing dependencies to owners if they are the only reviewer
 +
@@ -401,8 +388,7 @@
 An additional from line is now injected at the start of the email
 body to indicate the actual user.
 
-Remote Access
-~~~~~~~~~~~~~
+=== Remote Access
 
 * issue 385 Delete session cookie when session is expired
 +
@@ -443,8 +429,7 @@
 whitespace, removing a common source of typos that lead to users
 being automatically assigned more than one Gerrit user account.
 
-Server Administration
-~~~~~~~~~~~~~~~~~~~~~
+=== Server Administration
 
 * daemon: Really allow httpd.listenUrl to end with /
 +
@@ -532,8 +517,7 @@
 sometimes failed.  Fixed by executing an implicit reload in these
 cases, reducing the number of times a user sees a failure.
 
-Development
-~~~~~~~~~~~
+=== Development
 
 * issue 427 Adjust SocketUtilTest to be more likely to pass
 +
@@ -569,8 +553,7 @@
 removed from the license file.
 
 
-Schema Changes in Detail
-------------------------
+== Schema Changes in Detail
 
 * Remove Project.Id and use only Project.NameKey
 +
@@ -594,8 +577,7 @@
 aren't possible, or to run on MySQL MyISAM tables.
 
 
-Other Changes
--------------
+== Other Changes
 * Update gwtorm to 1.1.4-SNAPSHOT
 * Add unique column ids to every column
 * Remove unused byName @SecondaryKey from ApprovalCategory
diff --git a/ReleaseNotes/ReleaseNotes-2.1.3.txt b/ReleaseNotes/ReleaseNotes-2.1.3.txt
index f4faf32..6226b93 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.3.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.3.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.1.3
-==============================
+= Release notes for Gerrit 2.1.3
 
 Gerrit 2.1.3 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
-Schema Change
--------------
+== Schema Change
 
 *WARNING* This release contains multiple schema changes.  To upgrade:
 ----
@@ -14,11 +12,9 @@
 ----
 
 
-New Features
-------------
+== New Features
 
-Web UI
-~~~~~~
+=== Web UI
 
 * issue 289 Remove reviewers (or self) from a change
 +
@@ -63,8 +59,7 @@
 Access tabs.  Editing is obviously disabled, unless the user has
 owner level access to the project, or one of its branches.
 
-Access Controls
-~~~~~~~~~~~~~~~
+=== Access Controls
 
 * Branch-level read access is now supported
 +
@@ -97,8 +92,7 @@
 inherited by default, but the old exclusive behavior can be obtained
 by prefixing the reference with '-'.
 
-SSH Commands
-~~~~~~~~~~~~
+=== SSH Commands
 
 * create-account: Permit creation of batch user accounts over SSH
 * issue 269 Enable create-project for non-Administrators
@@ -125,8 +119,7 @@
 The old `gerrit approve` name will be kept around as an alias to
 provide time to migrate hooks/scripts/etc.
 
-Hooks / Stream Events
-~~~~~~~~~~~~~~~~~~~~~
+=== Hooks / Stream Events
 
 * \--change-url parameter passed to hooks
 +
@@ -147,13 +140,11 @@
 set is now included in the stream-events record, making it possible
 for a monitor to easily pull down a patch set and compile it.
 
-Contrib
-~~~~~~~
+=== Contrib
 
 * Example hook to auto-re-approve a trivial rebase
 
-Misc.
-~~~~~
+=== Misc.
 
 * transfer.timeout: Support configurable timeouts for dead clients
 +
@@ -195,11 +186,9 @@
 Apache Commons DBCP to 1.4.
 
 
-Bug Fixes
----------
+== Bug Fixes
 
-Web UI
-~~~~~~
+=== Web UI
 
 * issue 396 Prevent 'no-score' approvals from being recorded
 +
@@ -237,8 +226,7 @@
 'Register New Email...' button.  A cancel button was added to
 close the dialog.
 
-Server Programs
-~~~~~~~~~~~~~~~
+=== Server Programs
 
 * init: Import non-standardly named Git repositories
 +
@@ -265,8 +253,7 @@
 were not properly logged by `gerrit approve` (now gerrit review).
 Fixed by logging the root cause of the failure.
 
-Configuration
-~~~~~~~~~~~~~
+=== Configuration
 
 * Display error when HTTP authentication isn't configured
 +
@@ -282,8 +269,7 @@
 during sign-in.  Administrators can enable following by adding
 `ldap.referral = follow` to their gerrit.config file.
 
-Documentation
-~~~~~~~~~~~~~
+=== Documentation
 
 * documentation: Clarified the ownership of '\-- All Projects \--'
 +
@@ -308,7 +294,6 @@
 the current stable version of the Maven plugin.
 
 
-Version
--------
+== Version
 
 e8fd49f5f7481e2f916cb0d8cfbada79309562b4
diff --git a/ReleaseNotes/ReleaseNotes-2.1.4.txt b/ReleaseNotes/ReleaseNotes-2.1.4.txt
index 3e25163..72eec55 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.4.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.4.txt
@@ -1,23 +1,19 @@
-Release notes for Gerrit 2.1.4
-==============================
+= Release notes for Gerrit 2.1.4
 
 Gerrit 2.1.4 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
-Schema Change
--------------
+== Schema Change
 
 *WARNING* This release contains multiple schema changes.  To upgrade:
 ----
   java -jar gerrit.war init -d site_path
 ----
 
-New Features
-------------
+== New Features
 
-Change Management
-~~~~~~~~~~~~~~~~~
+=== Change Management
 
 * issue 504 Implement full query operators
 +
@@ -51,8 +47,7 @@
 watched project list, and a new menu item was added under the My menu
 to select open changes matching these watched projects.
 
-Web UI
-~~~~~~
+=== Web UI
 
 * issue 579 Remember diff formatting preferences
 +
@@ -82,8 +77,7 @@
 
 * issue 509 Make branch columns link to changes on that branch
 
-Email Notifications
-~~~~~~~~~~~~~~~~~~~
+=== Email Notifications
 
 * issue 311 No longer CC a user by default
 +
@@ -116,8 +110,7 @@
 New fields in the email footer provide additional detail, enabling
 better filtering and classification of messages.
 
-Access Control
-~~~~~~~~~~~~~~
+=== Access Control
 
 * Support regular expressions for ref access rules
 +
@@ -136,8 +129,7 @@
 Groups can now be created over SSH by administrators using the
 `gerrit create-group` command.
 
-Authentication
-~~~~~~~~~~~~~~
+=== Authentication
 
 * Remove password authentication over SSH
 +
@@ -157,8 +149,7 @@
 setting, rather than expiring when the browser closes.  (Previously
 sessions expired when the browser exited.)
 
-Misc.
-~~~~~
+=== Misc.
 
 * Add topic, lastUpdated, sortKey to ChangeAttribute
 +
@@ -184,11 +175,9 @@
 Updated JGit to 0.8.4.89-ge2f5716, log4j to 1.2.16, GWT to 2.0.4,
 sfl4j to 1.6.1, easymock to 3.0, JUnit to 4.8.1.
 
-Bug Fixes
----------
+== Bug Fixes
 
-Web UI
-~~~~~~
+=== Web UI
 
 * issue 352 Confirm branch deletion in web UI
 +
@@ -211,15 +200,13 @@
 Keyboard navigation to standard links like 'Google Accounts'
 wasn't supported.  Fixed.
 
-Misc.
-~~~~~
+=== Misc.
 
 * issue 614 Fix 503 error when Jetty cancels a request
 +
 A bug was introduced in 2.1.3 that caused a server 503 error
 when a fetch/pull/clone or push request timed out.  Fixed.
 
-Version
--------
+== Version
 
 ae59d1bf232bba16d4d03ca924884234c68be0f2
diff --git a/ReleaseNotes/ReleaseNotes-2.1.5.txt b/ReleaseNotes/ReleaseNotes-2.1.5.txt
index 4934223..88288e2 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.5.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.5.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.1.5
-==============================
+= Release notes for Gerrit 2.1.5
 
 Gerrit 2.1.5 is now available:
 
@@ -8,8 +7,7 @@
 This is primarily a bug fix release to 2.1.4, but some additional
 new features were included so its named 2.1.5 rather than 2.1.4.1.
 
-Upgrade Instructions
---------------------
+== Upgrade Instructions
 
 If upgrading from version 2.1.4, simply replace the WAR file in
 `'site_path'/bin/gerrit.war` and restart Gerrit.
@@ -18,11 +16,9 @@
 `java -jar gerrit.war init -d 'site_path'` to upgrade the schema,
 and restart Gerrit.
 
-New Features
-------------
+== New Features
 
-Web UI
-~~~~~~
+=== Web UI
 * issue 361 Enable commenting on commit messages
 +
 The commit message of a change can now be commented on inline, and
@@ -49,8 +45,7 @@
 A 'diffstat' is shown for each file, summarizing the size of the
 change on that file in terms of number of lines added or deleted.
 
-Email Notifications
-~~~~~~~~~~~~~~~~~~~
+=== Email Notifications
 * issue 452 Include a quick summary of the size of a change in email
 +
 After the file listing, a summary totaling the number of files
@@ -58,11 +53,9 @@
 help reviewers to get a quick estimation on the time required for
 them to review the change.
 
-Bug Fixes
----------
+== Bug Fixes
 
-Web UI
-~~~~~~
+=== Web UI
 * issue 639 Fix keyboard shortcuts under Chrome/Safari
 +
 Keyboard shortcuts didn't work properly on modern WebKit browsers
@@ -91,8 +84,7 @@
 is present and will toggle the user's starred flag for that change.
 Fixed.
 
-Access Control
-~~~~~~~~~~~~~~
+=== Access Control
 * issue 672 Fix branch owner adding exclusive ACL
 +
 Branch owners could not add exclusive ACLs within their branch
@@ -118,8 +110,7 @@
 bug which failed to consider the project inheritance if any branch
 (not just the one being uploaded to) denied upload access.
 
-Misc.
-~~~~~
+=== Misc.
 * issue 641 Don't pass null arguments to hooks
 +
 Some hooks crashed inside of the server during invocation because the
@@ -162,12 +153,10 @@
 Gerrit Code Review was only accepting the ';' syntax.  Fixed
 to support both.
 
-Documentation
-~~~~~~~~~~~~~
+=== Documentation
 * Fixed example for gerrit create-account.
 * gerrit.sh: Correct /etc/default path in error message
 
-Version
--------
+== Version
 
 2765ff9e5f821100e9ca671f4d502b5c938457a5
diff --git a/ReleaseNotes/ReleaseNotes-2.1.6.1.txt b/ReleaseNotes/ReleaseNotes-2.1.6.1.txt
index a490c0a..4626c7b 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.6.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.6.1.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.1.6.1
-================================
+= Release notes for Gerrit 2.1.6.1
 
 Gerrit 2.1.6.1 is now available:
 
 link:https://www.gerritcodereview.com/download/gerrit-2.1.6.1.war[https://www.gerritcodereview.com/download/gerrit-2.1.6.1.war]
 
-Schema Change
--------------
+== Schema Change
 
 If upgrading from 2.1.6, there are no schema changes.  Replace the
 WAR and restart the daemon.
@@ -17,8 +15,7 @@
   java -jar gerrit.war init -d site_path
 ----
 
-New Features
-------------
+== New Features
 * Display the originator of each access rule
 +
 The project access panel now shows which project each rule inherits
@@ -37,8 +34,7 @@
 project, provided that the parent project is not the root level
 \-- All Projects \--.
 
-Bug Fixes
----------
+== Bug Fixes
 * Fix disabled intraline difference checkbox
 +
 Intraline difference couldn't be enabled once it was disabled by
diff --git a/ReleaseNotes/ReleaseNotes-2.1.6.txt b/ReleaseNotes/ReleaseNotes-2.1.6.txt
index 520b2a6..83689e7 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.6.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.6.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.1.6
-==============================
+= Release notes for Gerrit 2.1.6
 
 Gerrit 2.1.6 is now available:
 
 link:https://www.gerritcodereview.com/download/gerrit-2.1.6.war[https://www.gerritcodereview.com/download/gerrit-2.1.6.war]
 
-Schema Change
--------------
+== Schema Change
 
 *WARNING* This release contains multiple schema changes.  To upgrade:
 ----
@@ -14,11 +12,9 @@
 ----
 
 
-New Features
-------------
+== New Features
 
-Web UI
-~~~~~~
+=== Web UI
 * issue 312 Abandoned changes can now be restored.
 * issue 698 Make date and time fields customizable
 * issue 556 Preference to display patch sets in reverse order
@@ -39,8 +35,7 @@
 This is built upon experimental merge code inherited from JGit,
 and is therefore still experimental in Gerrit.
 
-Change Query
-~~~~~~~~~~~~
+=== Change Query
 * issue 688 Match branch, topic, project, ref by regular expressions
 +
 Similar to other features in Gerrit Code Review, starting any of these
@@ -80,8 +75,7 @@
 Gerrit change submission, Gerrit will now send a new "ref-updated"
 event to the event stream.
 
-User Management
-~~~~~~~~~~~~~~~
+=== User Management
 * SSO via client SSL certificates
 +
 A new auth.type of CLIENT_SSL_CERT_LDAP supports authenticating users
@@ -123,8 +117,7 @@
 The internal SSH daemon now supports additional configuration
 settings to reduce the risk of abuse.
 
-Administration
-~~~~~~~~~~~~~~
+=== Administration
 * issue 558 Allow Access rights to be edited by clicking on them.
 
 * New 'Project Owner' system group to define default rights
@@ -186,11 +179,9 @@
 prevent the older (pre-filter-branch) history from being reintroduced
 into the repository.
 
-Bug Fixes
----------
+== Bug Fixes
 
-Web UI
-~~~~~~
+=== Web UI
 * issue 498 Enable Keyboard navigation after change submit
 * issue 691 Make ']' on last file go up to change
 * issue 741 Make ENTER work for 'Create Group'
@@ -224,13 +215,11 @@
 are not project owners may now only view access rights for branches
 they have at least READ +1 permission on.
 
-Change Query
-~~~~~~~~~~~~
+=== Change Query
 * issue 689 Fix age:4days to parse correctly
 * Make branch: operator slightly less ambiguous
 
-Push Support
-~~~~~~~~~~~~
+=== Push Support
 * issue 695 Permit changing only the author of a commit
 +
 Correcting only the author of a change failed to upload the new patch
@@ -257,8 +246,7 @@
 create changes for review were unable to push to a project.  Fixed.
 This (finally) makes Gerrit a replacement for Gitosis or Gitolite.
 
-Replication
-~~~~~~~~~~~
+=== Replication
 * issue 683 Don't assume authGroup = "Registered Users" in replication
 +
 Previously a misconfigured authGroup in replication.config may have
@@ -281,8 +269,7 @@
 
 * issue 658 Allow refspec shortcuts (push = master) for replication
 
-User Management
-~~~~~~~~~~~~~~~
+=== User Management
 * Ensure proper escaping of LDAP group names
 +
 Some special characters may appear in LDAP group names, these must be
@@ -295,8 +282,7 @@
 but cannot because it is already in use by another user on this
 server, the new account won't be created.
 
-Administration
-~~~~~~~~~~~~~~
+=== Administration
 * gerrit.sh: actually verify running processes
 +
 Previously `gerrit.sh check` claimed a server was running if the
@@ -317,6 +303,5 @@
 child project.  Permissions can now be overidden if the category,
 group name and reference name all match.
 
-Version
--------
+== Version
 ef16a1816f293d00c33de9f90470021e2468a709
diff --git a/ReleaseNotes/ReleaseNotes-2.1.7.2.txt b/ReleaseNotes/ReleaseNotes-2.1.7.2.txt
index fdd7725..9c9e6e1 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.7.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.7.2.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.1.7.2
-================================
+= Release notes for Gerrit 2.1.7.2
 
 Gerrit 2.1.7.2 is now available:
 
 link:https://www.gerritcodereview.com/download/gerrit-2.1.7.2.war[https://www.gerritcodereview.com/download/gerrit-2.1.7.2.war]
 
-Bug Fixes
----------
+== Bug Fixes
 * issue 997 Resolve Project Owners when checking access rights
 +
 Members of the 'Project Owners' magical group did not always have
diff --git a/ReleaseNotes/ReleaseNotes-2.1.7.txt b/ReleaseNotes/ReleaseNotes-2.1.7.txt
index 5123279..ad440b5 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.7.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.7.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.1.7
-==============================
+= Release notes for Gerrit 2.1.7
 
 Gerrit 2.1.7 is now available:
 
 link:https://www.gerritcodereview.com/download/gerrit-2.1.7.war[https://www.gerritcodereview.com/download/gerrit-2.1.7.war]
 
-Schema Change
--------------
+== Schema Change
 *WARNING* This release contains multiple schema changes.  To upgrade:
 ----
   java -jar gerrit.war init -d site_path
@@ -18,8 +16,7 @@
   java -jar gerrit.war ExportReviewNotes -d site_path
 ----
 
-Memory Usage Increase
----------------------
+== Memory Usage Increase
 *WARNING* The JGit delta base cache, whose size is controlled by
 `core.deltaBaseCacheLimit`, has changed in this release from being a
 JVM-wide singleton to per-thread. This alters the memory usage, going
@@ -27,17 +24,14 @@
 change improves performance on big repositories, but may need a larger
 `container.heapLimit` if the number of concurrent operations is high.
 
-New Features
-------------
+== New Features
 
-Change Data
-~~~~~~~~~~~
+=== Change Data
 * issue 64 Create Git notes for submitted changes
 +
 Git notes are automatically added to the `refs/notes/review`.
 
-Query
-~~~~~
+=== Query
 * Search project names by substring
 +
 Entering a word with no operator (for example `gerrit`) will be
@@ -49,8 +43,7 @@
 search for changes whose owner or that has a reviewer in (or not
 in if prefixed with `-`) the specified group.
 
-Web UI
-~~~~~~
+=== Web UI
 * Add reviewer/verifier name beside check/plus/minus
 +
 Change lists (such as from a search result, or in a user's dashboard)
@@ -90,17 +83,14 @@
 SSH public key files by hand.
 
 
-SSH Commands
-~~~~~~~~~~~~
+=== SSH Commands
 * issue 674 Add abandon/restore to `gerrit review`
 * Add `gerrit version` command
 
-Change Upload
-~~~~~~~~~~~~~
+=== Change Upload
 * Display a more verbose "you are not author/committer" message
 
-Documentation
-~~~~~~~~~~~~~
+=== Documentation
 * Detailed error message explanations
 +
 Most common error messages are now described in detail in the
@@ -111,8 +101,7 @@
 * issue 905 Document reverse proxy using Nginx
 * Updated system scaling data in 'System Design'
 
-Outgoing Mail
-~~~~~~~~~~~~~
+=== Outgoing Mail
 * Optionally add Importance and Expiry-Days headers
 +
 New gerrit.config variable `sendemail.importance` can be set to `high`
@@ -122,8 +111,7 @@
 
 * Add support for SMTP AUTH LOGIN
 
-Administration
-~~~~~~~~~~~~~~
+=== Administration
 * Group option to make group visible to all users
 +
 A new group option permits the group to be visible to all users,
@@ -181,8 +169,7 @@
 path used for the authentication cookie, which may be necessary if
 a reverse proxy maps requests to the managed gitweb.
 
-Replication
-~~~~~~~~~~~
+=== Replication
 * Add adminUrl to replication for repository creation
 +
 Replication remotes can be configured with `remote.name.adminUrl` to
@@ -196,8 +183,7 @@
 Replication can now be performed over an authenticated smart HTTP
 transport, in addition to anonymous Git and authenticated SSH.
 
-Misc.
-~~~~~
+=== Misc.
 * Alternative URL for Gerrit's managed Gitweb
 +
 The internal gitweb served from `/gitweb` can now appear to be from a
@@ -210,11 +196,9 @@
 to 1.6, Apache Commons Net to 2.2, Apache Commons Pool to 1.5.5, JGit
 to 0.12.1.53-g5ec4977, MINA SSHD to 0.5.1-r1095809.
 
-Bug Fixes
----------
+== Bug Fixes
 
-Web UI
-~~~~~~
+=== Web UI
 * issue 853 Incorrect side-by-side display of modified lines
 +
 A bug in JGit lead to the side-by-side view displaying wrong and
@@ -261,12 +245,10 @@
 * Always display button text in black
 * Always disable content merge option if user can't change project
 
-commit-msg Hook
-~~~~~~~~~~~~~~~
+=== commit-msg Hook
 * issue 922 Fix commit-msg hook to run on Solaris
 
-Outgoing Mail
-~~~~~~~~~~~~~
+=== Outgoing Mail
 * issue 780 E-mail about failed merge should not use Anonymous Coward
 +
 Some email was sent as Anonymous Coward, even when the user had a
@@ -281,8 +263,7 @@
 * Do not email reviewers adding themselves as reviewers
 * Fix comma/space separation in email templates
 
-Pushing Changes
-~~~~~~~~~~~~~~~
+=== Pushing Changes
 * Avoid huge pushes during refs/for/BRANCH push
 +
 With Gerrit 2.1.6, clients started to push possibly hundreds of
@@ -356,8 +337,7 @@
 no mention if it on the server error log. Now it is reported so the
 site administrator also knows about it.
 
-SSH Commands
-~~~~~~~~~~~~
+=== SSH Commands
 * issue 755 Send new patchset event after its available
 * issue 814 Evict initial members of group created by SSH
 * issue 879 Fix replication of initial empty commit in new project
@@ -365,8 +345,7 @@
 * Automatically create user account(s) as necessary
 * Move SSH command creation off NioProcessor threads
 
-Administration
-~~~~~~~~~~~~~~
+=== Administration
 * Enable git reflog for all newly created projects
 +
 Previously branch updates were not being recorded in the native Git
@@ -388,8 +367,7 @@
 * gerrit.sh: Fix issues on Solaris
 * gerrit.sh: Support spaces in JAVA_HOME
 
-Documentation
-~~~~~~~~~~~~~
+=== Documentation
 * issue 800 documentation: Show example of review -m
 * issue 896 Clarify that $\{name\} is required for replication.
 * Fix spelling mistake in 'Searching Changes' documentation
diff --git a/ReleaseNotes/ReleaseNotes-2.1.8.txt b/ReleaseNotes/ReleaseNotes-2.1.8.txt
index 476e312..e1ed11c 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.8.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.8.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.1.8
-==============================
+= Release notes for Gerrit 2.1.8
 
 Gerrit 2.1.8 is now available:
 
 link:https://www.gerritcodereview.com/download/gerrit-2.1.8.war[https://www.gerritcodereview.com/download/gerrit-2.1.8.war]
 
-New Features
-------------
+== New Features
 * Add cache for tag advertisements
 +
 When READ level access controls are used on references/branches, this
@@ -39,8 +37,7 @@
 MS-DOS compatibility may have permitted access to special device
 files in any directory, rather than just the "\\.\" device namespace.
 
-Bug Fixes
----------
+== Bug Fixes
 * issue 518 Fix MySQL counter resets
 +
 MySQL databases lost their change_id, account_id counters after
diff --git a/ReleaseNotes/ReleaseNotes-2.1.9.txt b/ReleaseNotes/ReleaseNotes-2.1.9.txt
index 2efc5b6..63bcb20 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.9.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.9.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.1.9
-==============================
+= Release notes for Gerrit 2.1.9
 
 There are no schema changes from link:ReleaseNotes-2.1.8.html[2.1.8].
 
 link:https://www.gerritcodereview.com/download/gerrit-2.1.9.war[https://www.gerritcodereview.com/download/gerrit-2.1.9.war]
 
-Bug Fixes
----------
+== Bug Fixes
 * Patch JGit security hole
 +
 The security hole may permit a modified Git client to gain access
diff --git a/ReleaseNotes/ReleaseNotes-2.1.txt b/ReleaseNotes/ReleaseNotes-2.1.txt
index 127ab09..28cc90d 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.txt
@@ -1,20 +1,17 @@
-Release notes for Gerrit 2.1
-============================
+= Release notes for Gerrit 2.1
 
 Gerrit 2.1 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
 
-New site_path Layout
---------------------
+== New site_path Layout
 
 The layout of the `$site_path` directory has been changed in 2.1.
 Configuration files are now stored within the `etc/` subdirectory
 and will be automatically moved there by the init subcommand.
 
-Upgrading From 2.0.x
---------------------
+== Upgrading From 2.0.x
 
   If the server is running a version older than 2.0.24, upgrade the
   database schema to the current schema version of 19.  Download
@@ -37,8 +34,7 @@
 sendemail.smtpPass and ldap.password out of gerrit.config into a
 read-protected secure.config file.
 
-New Daemon Mode
----------------
+== New Daemon Mode
 
 Gerrit 2.1 and later embeds the Jetty servlet container, and
 runs it automatically as part of `java -jar gerrit.war daemon`.
@@ -57,8 +53,7 @@
 link:http://gerrit.googlecode.com/svn/documentation/2.1/index.html[http://gerrit.googlecode.com/svn/documentation/2.1/index.html]
 
 
-New Features
-------------
+== New Features
 
 * issue 19     Link to issue tracker systems from commits
 +
@@ -184,8 +179,7 @@
 +
 Most dependencies were updated to their current stable versions.
 
-Bug Fixes
----------
+== Bug Fixes
 
 * issue 259    Improve search hint to include owner:email
 +
@@ -268,8 +262,7 @@
 `$HOME/.gerritcodereview/tmp`, which should be isolated from
 the host system's /tmp cleaner.
 
-Other=
-------
+== Other=
 
 * Pick up gwtexpui 1.1.4-SNAPSHOT
 * Merge change Ia64286d3
diff --git a/ReleaseNotes/ReleaseNotes-2.10.1.txt b/ReleaseNotes/ReleaseNotes-2.10.1.txt
index 3065492..72d26d1 100644
--- a/ReleaseNotes/ReleaseNotes-2.10.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.10.1.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.10.1
-===============================
+= Release notes for Gerrit 2.10.1
 
 There are no schema changes from link:ReleaseNotes-2.10.html[2.10].
 
@@ -7,8 +6,7 @@
 link:https://www.gerritcodereview.com/download/gerrit-2.10.1.war[
 https://www.gerritcodereview.com/download/gerrit-2.10.1.war]
 
-Bug Fixes
----------
+== Bug Fixes
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=2260[Issue 2260]:
 LDAP horrendous login time due to recursive lookup.
@@ -19,8 +17,7 @@
 * link:https://code.google.com/p/gerrit/issues/detail?id=3211[Issue 3211]:
 Intermittent Null Pointer Exception when showing process queue.
 
-LDAP
-----
+== LDAP
 
 * Several performance improvements when using LDAP, both in the number of LDAP
 requests and in the amount of data transferred.
@@ -28,13 +25,11 @@
 * Sites using LDAP for authentication but otherwise rely on local Gerrit groups
 should set the new `ldap.fetchMemberOfEagerly` option to `false`.
 
-OAuth
------
+== OAuth
 
 * Expose extension point for generic OAuth providers.
 
-OpenID
-------
+== OpenID
 
 * Add support for Launchpad on the login form.
 
diff --git a/ReleaseNotes/ReleaseNotes-2.10.2.txt b/ReleaseNotes/ReleaseNotes-2.10.2.txt
index ac7c866..49be04e 100644
--- a/ReleaseNotes/ReleaseNotes-2.10.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.10.2.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.10.2
-===============================
+= Release notes for Gerrit 2.10.2
 
 There are no schema changes from link:ReleaseNotes-2.10.1.html[2.10.1].
 
@@ -7,8 +6,7 @@
 link:https://www.gerritcodereview.com/download/gerrit-2.10.2.war[
 https://www.gerritcodereview.com/download/gerrit-2.10.2.war]
 
-Bug Fixes
----------
+== 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
@@ -16,15 +14,13 @@
 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
----------------
+== 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
-------
+== 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
diff --git a/ReleaseNotes/ReleaseNotes-2.10.3.1.txt b/ReleaseNotes/ReleaseNotes-2.10.3.1.txt
index 39312eb..7777bd8 100644
--- a/ReleaseNotes/ReleaseNotes-2.10.3.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.10.3.1.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.10.3.1
-=================================
+= Release notes for Gerrit 2.10.3.1
 
 There are no schema changes from link:ReleaseNotes-2.10.3.html[2.10.3].
 
diff --git a/ReleaseNotes/ReleaseNotes-2.10.3.txt b/ReleaseNotes/ReleaseNotes-2.10.3.txt
index f7a69c3..1dd96e7 100644
--- a/ReleaseNotes/ReleaseNotes-2.10.3.txt
+++ b/ReleaseNotes/ReleaseNotes-2.10.3.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.10.3
-===============================
+= Release notes for Gerrit 2.10.3
 
 Download:
 link:https://www.gerritcodereview.com/download/gerrit-2.10.3.war[
 https://www.gerritcodereview.com/download/gerrit-2.10.3.war]
 
-Important Notes
----------------
+== 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.
@@ -26,8 +24,7 @@
   java -jar gerrit.war init -d site_path
 ----
 
-New Features
-------------
+== New Features
 
 * Support hybrid OpenID and OAuth2 authentication
 +
@@ -36,15 +33,13 @@
 Particularly, linking of user identities across protocol boundaries and even from
 one OAuth2 identity to another OAuth2 identity wasn't implemented yet.
 
-Configuration
-~~~~~~~~~~~~~
+=== Configuration
 
 * Allow to configure
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10.3/config-gerrit.html#sshd.rekeyBytesLimit[
 SSHD rekey parameters].
 
-SSH
----
+== SSH
 
 * Update SSHD to 0.14.0.
 +
@@ -56,8 +51,7 @@
 * link:https://code.google.com/p/gerrit/issues/detail?id=2797[Issue 2797]:
 Add support for ECDSA based public key authentication.
 
-Bug Fixes
----------
+== Bug Fixes
 
 * Prevent wrong content type for CSS files.
 +
@@ -69,8 +63,7 @@
 * link:https://code.google.com/p/gerrit/issues/detail?id=3289[Issue 3289]:
 Prevent NullPointerException in Gitweb servlet.
 
-Replication plugin
-~~~~~~~~~~~~~~~~~~
+=== Replication plugin
 
 * Set connection timeout to 120 seconds for SSH remote operations.
 +
@@ -78,8 +71,7 @@
 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
-~~~~~~~~~~~~~~~~~~~~~
+=== OAuth extension point
 
 * Respect servlet context path in URL for login token
 +
@@ -90,34 +82,29 @@
 +
 After web session cache expiration there is no way to re-sign-in into Gerrit.
 
-Daemon
-~~~~~~
+=== Daemon
 
 * Print proper names for tasks in output of `show-queue` command.
 +
 Some tasks were not displayed with the proper name.
 
-Web UI
-~~~~~~
+=== Web UI
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=3044[Issue 3044]:
 Remove stripping `#` in login redirect.
 
-SSH
-~~~
+=== SSH
 
 * Prevent double authentication for the same public key.
 
 
-Performance
------------
+== Performance
 
 * Improved performance when creating a new branch on a repository with a large
 number of changes.
 
 
-Upgrades
---------
+== Upgrades
 
 * Update Bouncycastle to 1.51.
 
diff --git a/ReleaseNotes/ReleaseNotes-2.10.4.txt b/ReleaseNotes/ReleaseNotes-2.10.4.txt
index c16e7e9..c69a946 100644
--- a/ReleaseNotes/ReleaseNotes-2.10.4.txt
+++ b/ReleaseNotes/ReleaseNotes-2.10.4.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.10.4
-===============================
+= Release notes for Gerrit 2.10.4
 
 There are no schema changes from link:ReleaseNotes-2.10.3.1.html[2.10.3.1].
 
@@ -7,8 +6,7 @@
 link:https://www.gerritcodereview.com/download/gerrit-2.10.4.war[
 https://www.gerritcodereview.com/download/gerrit-2.10.4.war]
 
-New Features
-------------
+== New Features
 
 * Support identity linking in hybrid OpenID and OAuth2 authentication.
 +
@@ -20,8 +18,7 @@
 Linking of user identities from one OAuth2 identity to another OAuth2
 identity is supported.
 
-Bug Fixes
----------
+== 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.
@@ -35,15 +32,13 @@
 The padding was not flushed, which caused the downloaded patch to not be
 valid base64.
 
-OAuth extension point
-~~~~~~~~~~~~~~~~~~~~~
+=== 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
--------
+== 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
index eb48c31..a221b58 100644
--- a/ReleaseNotes/ReleaseNotes-2.10.5.txt
+++ b/ReleaseNotes/ReleaseNotes-2.10.5.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.10.5
-===============================
+= Release notes for Gerrit 2.10.5
 
 There are no schema changes from link:ReleaseNotes-2.10.4.html[2.10.4].
 
@@ -7,8 +6,7 @@
 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
----------
+== Bug Fixes
 
 * Update JGit to include a memory leak fix as discussed
 link:https://groups.google.com/forum/#!topic/repo-discuss/RRQT_xCqz4o[here]
@@ -21,7 +19,6 @@
 * Fixed a regression caused by the defaultValue feature which broke the ability
 to remove labels in subprojects
 
-Updates
--------
+== 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
index 94a95bd..7c12d11 100644
--- a/ReleaseNotes/ReleaseNotes-2.10.6.txt
+++ b/ReleaseNotes/ReleaseNotes-2.10.6.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.10.6
-===============================
+= Release notes for Gerrit 2.10.6
 
 There are no schema changes from link:ReleaseNotes-2.10.5.html[2.10.5].
 
@@ -7,8 +6,7 @@
 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
----------
+== Bug Fixes
 
 * Fix generation of licenses in documentation.
 
diff --git a/ReleaseNotes/ReleaseNotes-2.10.7.txt b/ReleaseNotes/ReleaseNotes-2.10.7.txt
index 28cf37b..f369999 100644
--- a/ReleaseNotes/ReleaseNotes-2.10.7.txt
+++ b/ReleaseNotes/ReleaseNotes-2.10.7.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.10.7
-===============================
+= Release notes for Gerrit 2.10.7
 
 There are no schema changes from link:ReleaseNotes-2.10.6.html[2.10.6].
 
@@ -7,8 +6,7 @@
 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
----------
+== 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
diff --git a/ReleaseNotes/ReleaseNotes-2.10.txt b/ReleaseNotes/ReleaseNotes-2.10.txt
index f6bd951..4f068cc 100644
--- a/ReleaseNotes/ReleaseNotes-2.10.txt
+++ b/ReleaseNotes/ReleaseNotes-2.10.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.10
-=============================
+= Release notes for Gerrit 2.10
 
 
 Gerrit 2.10 is now available:
@@ -14,8 +13,7 @@
 link:ReleaseNotes-2.9.4.html[Gerrit 2.9.4].
 These bug fixes are *not* listed in these release notes.
 
-Important Notes
----------------
+== Important Notes
 
 
 *WARNING:* This release contains schema changes.  To upgrade:
@@ -44,8 +42,7 @@
 link:https://code.google.com/p/gerrit/issues/detail?id=3084[Issue 3084].
 
 
-Release Highlights
-------------------
+== Release Highlights
 
 
 * Support for externally loaded plugins.
@@ -62,16 +59,13 @@
 can configure the default contents of the menu.
 
 
-New Features
-------------
+== New Features
 
 
-Web UI
-~~~~~~
+=== Web UI
 
 
-Global
-^^^^^^
+==== Global
 
 * Add 'All-Users' project to store meta data for all users.
 
@@ -82,8 +76,7 @@
 * Allow UiActions to perform redirects without JavaScript.
 
 
-Change Screen
-^^^^^^^^^^^^^
+==== Change Screen
 
 
 * Display avatar for author, committer, and change owner.
@@ -104,8 +97,7 @@
 Allow to customize Submit button label and tooltip.
 
 
-Side-by-Side Diff Screen
-^^^^^^^^^^^^^^^^^^^^^^^^
+==== Side-by-Side Diff Screen
 
 * Allow the user to select the syntax highlighter.
 
@@ -116,8 +108,7 @@
 * Add syntax highlighting of the commit message.
 
 
-Change List / Dashboards
-^^^^^^^^^^^^^^^^^^^^^^^^
+==== Change List / Dashboards
 
 * Remove age operator when drilling down from a dashboard to a query.
 
@@ -132,8 +123,7 @@
 when 'R' is pressed.  The same binding is added for custom dashboards.
 
 
-Project Screens
-^^^^^^^^^^^^^^^
+==== 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.
@@ -142,8 +132,7 @@
 
 * Add branch actions to 'Projects > Branches' view.
 
-User Preferences
-^^^^^^^^^^^^^^^^
+==== User Preferences
 
 
 * Users can customize the contents of the 'My' menu from the preferences
@@ -156,8 +145,7 @@
 names and 'Show Username' to show usernames in the change list.
 
 
-Secondary Index / Search
-~~~~~~~~~~~~~~~~~~~~~~~~
+=== Secondary Index / Search
 
 
 * Allow to search projects by prefix.
@@ -177,8 +165,7 @@
 rather than just the project name.
 
 
-ssh
-~~~
+=== ssh
 
 
 * Expose SSHD backend in
@@ -187,12 +174,10 @@
 
 * Add support for JCE (Java Cryptography Extension) ciphers.
 
-REST API
-~~~~~~~~
+=== REST API
 
 
-General
-^^^^^^^
+==== General
 
 
 * Remove `kind` attribute from REST containers.
@@ -201,8 +186,7 @@
 
 * Accept `HEAD` in RestApiServlet.
 
-Accounts
-^^^^^^^^
+==== Accounts
 
 
 * link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-accounts.html#get-user-preferences[
@@ -211,8 +195,7 @@
 * link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-accounts.html#set-user-preferences[
 Set user preferences].
 
-Changes
-^^^^^^^
+==== Changes
 
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=2338[Issue 2338]:
@@ -226,8 +209,7 @@
 If the `other-branches` option is specified, the mergeability will also be
 checked for all other branches.
 
-Config
-^^^^^^
+==== Config
 
 
 * link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-config.html#list-tasks[
@@ -254,8 +236,7 @@
 * link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-config.html#get-summary[
 Get server summary].
 
-Projects
-^^^^^^^^
+==== Projects
 
 
 * link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-projects.html#ban-commit[
@@ -276,8 +257,7 @@
 list projects endpoint] to support query offset.
 
 
-Daemon
-~~~~~~
+=== Daemon
 
 
 * Add change subject to output of change URL on push.
@@ -292,8 +272,7 @@
 Add change kind to PatchSetCreatedEvent.
 
 
-Configuration
-~~~~~~~~~~~~~
+=== Configuration
 
 * Use
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/config-gerrit.html#core.useRecursiveMerge[
@@ -344,8 +323,7 @@
 configure Tomcat] to allow embedded slashes.
 
 
-Misc
-~~~~
+=== Misc
 
 * Don't allow empty user name and passwords in InternalAuthBackend.
 
@@ -353,8 +331,7 @@
 Add change-owner parameter to gerrit hooks.
 
 
-Plugins
-~~~~~~~
+=== Plugins
 
 * Support for externally loaded plugins.
 +
@@ -428,11 +405,9 @@
 ** Star/unstar changes
 ** Check if revision needs rebase
 
-Bug Fixes
----------
+== Bug Fixes
 
-General
-~~~~~~~
+=== General
 
 * Use fixed rate instead of fixed delay for log file compression.
 +
@@ -453,8 +428,7 @@
 made, but one was not being closed. This eventually caused resource exhaustion
 and LDAP authentications failed.
 
-Access Permissions
-~~~~~~~~~~~~~~~~~~
+=== Access Permissions
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=2995[Issue 2995]:
 Fix faulty behaviour in `BLOCK` permission.
@@ -464,11 +438,9 @@
 case the `BLOCK` would always win for the child, even though the `BLOCK` was
 overruled in the parent.
 
-Web UI
-~~~~~~
+=== Web UI
 
-General
-^^^^^^^
+==== General
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=2595[Issue 2595]:
 Make gitweb redirect to login.
@@ -486,8 +458,7 @@
 if a site administrator ran `java -war gerrit.war init -d /some/existing/site --batch`.
 
 
-Change Screen
-^^^^^^^^^^^^^
+==== Change Screen
 
 * Don't linkify trailing dot or comma in messages.
 +
@@ -525,8 +496,7 @@
 * Fix exception when clicking on a binary file without being signed in.
 
 
-Side-By-Side Diff
-^^^^^^^^^^^^^^^^^
+==== 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.
@@ -556,30 +526,25 @@
 * Include content on identical files with mode change.
 
 
-User Settings
-^^^^^^^^^^^^^
+==== User Settings
 
 * Avoid loading all SSH keys when adding a new one.
 
 
-Secondary Index / Search
-~~~~~~~~~~~~~~~~~~~~~~~~
+=== Secondary Index / Search
 
 
 * Omit corrupt changes from search results.
 
 * Allow illegal label names from default search predicate.
 
-REST
-~~~~
+=== REST
 
-General
-^^^^^^^
+==== General
 
 * Fix REST API responses for 3xx and 4xx classes.
 
-Changes
-^^^^^^^
+==== Changes
 
 * Fix inconsistent behaviour in the
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-changes.html#add-reviewer[
@@ -589,8 +554,7 @@
 to add a user who had no visibility to the change or whose account was invalid.
 
 
-Changes
-^^^^^^^
+==== 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.
@@ -620,8 +584,7 @@
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-changes.html#list-comments[
 List Comments] endpoint.
 
-SSH
-~~~
+=== SSH
 
 
 * Prevent double authentication for the same public key.
@@ -641,8 +604,7 @@
 from being logged when `git receive-pack` was executed instead of `git-receive-pack`.
 
 
-Daemon
-~~~~~~
+=== Daemon
 
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=2284[Issue 2284]:
@@ -661,11 +623,9 @@
 directly pushed changes.
 
 
-Plugins
-~~~~~~~
+=== Plugins
 
-General
-^^^^^^^
+==== General
 
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=2895[Issue 2895]:
@@ -680,8 +640,7 @@
 
 * Fix ChangeListener auto-registered implementations.
 
-Replication
-^^^^^^^^^^^
+==== Replication
 
 
 * Move replication logs into a separate file.
@@ -691,8 +650,7 @@
 * Show replication ID in the log and in show-queue command.
 
 
-Upgrades
---------
+== Upgrades
 
 
 * Update Guava to 17.0
diff --git a/ReleaseNotes/ReleaseNotes-2.11.1.txt b/ReleaseNotes/ReleaseNotes-2.11.1.txt
index be19fc5..3583421 100644
--- a/ReleaseNotes/ReleaseNotes-2.11.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.11.1.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.11.1
-===============================
+= Release notes for Gerrit 2.11.1
 
 Gerrit 2.11.1 is now available:
 
@@ -14,8 +13,7 @@
 There are no schema changes from link:ReleaseNotes-2.11.html[2.11].
 
 
-New Features
-------------
+== New Features
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=321[Issue 321]:
 Use in-memory Lucene index for a better reviewer suggestion.
@@ -27,11 +25,9 @@
 suggest.fullTextSearchRefresh] parameter.
 
 
-Bug Fixes
----------
+== Bug Fixes
 
-Performance
-~~~~~~~~~~~
+=== Performance
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=3363[Issue 3363]:
 Fix performance degrade in background mergeability checks.
@@ -57,8 +53,7 @@
 +
 The change edit information was being loaded twice.
 
-Index
-~~~~~
+=== Index
 
 * Fix `PatchLineCommentsUtil.draftByChangeAuthor`.
 +
@@ -67,8 +62,7 @@
 
 * Don't show stack trace when failing to build BloomFilter during reindex.
 
-Permissions
-~~~~~~~~~~~
+=== Permissions
 
 * Require 'View Plugins' capability to list plugins through SSH.
 +
@@ -83,8 +77,7 @@
 edit the `project.config` file.
 
 
-Change Screen / Diff / Inline Edit
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+=== Change Screen / Diff / Inline Edit
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=3191[Issue 3191]:
 Always show 'Not Current' as state when looking at old patch set.
@@ -103,8 +96,7 @@
 In the side-by-side diff, the cursor is placed on the first column of the diff,
 rather than at the end.
 
-Web Container
-~~~~~~~~~~~~~
+=== Web Container
 
 * Fix `gc_log` when running in a web container.
 +
@@ -117,8 +109,7 @@
 web container with the site path configured using the `gerrit.site_path`
 property.
 
-Plugins
-~~~~~~~
+=== Plugins
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=3310[Issue 3310]:
 Fix disabling plugins when Gerrit is running on Windows.
@@ -137,8 +128,7 @@
 When `replicateOnStartup` is enabled, the plugin was not emitting the status
 events after the initial sync.
 
-Miscellaneous
-~~~~~~~~~~~~~
+=== Miscellaneous
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=3328[Issue 3328]:
 Allow to push a tag that points to a non-commit object.
@@ -168,8 +158,7 @@
 
 * Assume change kind is 'rework' if `LargeObjectException` occurs.
 
-Documentation
-~~~~~~~~~~~~~
+=== Documentation
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=3325[Issue 3325]:
 Add missing `--newrev` parameter to the
@@ -185,8 +174,7 @@
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11.1/config-gerrit.html#auth.registerUrl[
 auth types].
 
-Updates
--------
+== Updates
 
 * Update CodeMirror to 5.0.
 
diff --git a/ReleaseNotes/ReleaseNotes-2.11.2.txt b/ReleaseNotes/ReleaseNotes-2.11.2.txt
index 07f99ae..98e66b0 100644
--- a/ReleaseNotes/ReleaseNotes-2.11.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.11.2.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.11.2
-===============================
+= Release notes for Gerrit 2.11.2
 
 Gerrit 2.11.2 is now available:
 
@@ -12,8 +11,7 @@
 
 There are no schema changes from link:ReleaseNotes-2.11.1.html[2.11.1].
 
-New Features
-------------
+== New Features
 
 New SSH commands:
 
@@ -29,8 +27,7 @@
 problems.
 
 
-Bug Fixes
----------
+== Bug Fixes
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=2761[Issue 2761]:
 Fix incorrect file list when comparing patchsets.
@@ -96,8 +93,7 @@
 
 * Print proper name for reindex after update tasks in `show-queue` command.
 
-Updates
--------
+== Updates
 
 * Update JGit to 4.0.1.201506240215-r.
 
diff --git a/ReleaseNotes/ReleaseNotes-2.11.3.txt b/ReleaseNotes/ReleaseNotes-2.11.3.txt
index 0df3a29..f705d1e 100644
--- a/ReleaseNotes/ReleaseNotes-2.11.3.txt
+++ b/ReleaseNotes/ReleaseNotes-2.11.3.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.11.3
-===============================
+= Release notes for Gerrit 2.11.3
 
 Gerrit 2.11.3 is now available:
 
@@ -9,8 +8,7 @@
 There are no schema changes from link:ReleaseNotes-2.11.2.html[2.11.2].
 
 
-Bug Fixes
----------
+== Bug Fixes
 
 * Do not suggest inactive accounts.
 +
@@ -86,8 +84,7 @@
 caused by plugins not loading that an admin should pay attention to and try to
 resolve.
 
-Updates
--------
+== Updates
 
 * Update Guice to 4.0.
 * Replace parboiled 1.1.7 with grappa 1.0.4.
diff --git a/ReleaseNotes/ReleaseNotes-2.11.4.txt b/ReleaseNotes/ReleaseNotes-2.11.4.txt
index 6037edd..cfa8576 100644
--- a/ReleaseNotes/ReleaseNotes-2.11.4.txt
+++ b/ReleaseNotes/ReleaseNotes-2.11.4.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.11.4
-===============================
+= Release notes for Gerrit 2.11.4
 
 Gerrit 2.11.4 is now available:
 
@@ -13,8 +12,7 @@
 There are no schema changes from link:ReleaseNotes-2.11.3.html[2.11.3].
 
 
-Bug Fixes
----------
+== Bug Fixes
 
 * Fix NullPointerException in `ls-project` command with `--has-acl-for` option.
 +
@@ -131,8 +129,7 @@
 was a merge commit, or if the change being viewed conflicted with an open merge
 commit.
 
-Plugin Bugfixes
----------------
+== Plugin Bugfixes
 
 * singleusergroup: Allow to add a user to a project's ACL using `user/username`.
 +
diff --git a/ReleaseNotes/ReleaseNotes-2.11.5.txt b/ReleaseNotes/ReleaseNotes-2.11.5.txt
index d7758cb..6957827 100644
--- a/ReleaseNotes/ReleaseNotes-2.11.5.txt
+++ b/ReleaseNotes/ReleaseNotes-2.11.5.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.11.5
-===============================
+= Release notes for Gerrit 2.11.5
 
 Gerrit 2.11.5 is now available:
 
@@ -9,8 +8,7 @@
 There are no schema changes from link:ReleaseNotes-2.11.4.html[2.11.4].
 
 
-Important Notes
----------------
+== Important Notes
 
 *WARNING:* This release uses a forked version of buck.
 
@@ -25,8 +23,7 @@
 ----
 
 
-Bug Fixes
----------
+== Bug Fixes
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=3442[Issue 3442]:
 Handle commit validation errors when creating/editing changes via REST.
diff --git a/ReleaseNotes/ReleaseNotes-2.11.6.txt b/ReleaseNotes/ReleaseNotes-2.11.6.txt
index d6f939f..977ea14 100644
--- a/ReleaseNotes/ReleaseNotes-2.11.6.txt
+++ b/ReleaseNotes/ReleaseNotes-2.11.6.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.11.6
-===============================
+= Release notes for Gerrit 2.11.6
 
 Gerrit 2.11.6 is now available:
 
@@ -8,11 +7,9 @@
 
 There are no schema changes from link:ReleaseNotes-2.11.5.html[2.11.5].
 
-Bug Fixes
----------
+== Bug Fixes
 
-General
-~~~~~~~
+=== General
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=3742[Issue 3742]:
 Use merge strategy for mergeability testing on 'Rebase if Necessary' strategy.
@@ -51,8 +48,7 @@
 couldn't be added as a reviewer by selecting it from the suggested list of
 accounts.
 
-Authentication
-~~~~~~~~~~~~~~
+=== Authentication
 
 * Fix handling of lowercase HTTP username.
 +
@@ -69,8 +65,7 @@
 of the user for the claimed identity would fail, causing a new account to be
 created.
 
-UI
-~~
+=== UI
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=3714[Issue 3714]:
 Improve visibility of comments on dark themes.
@@ -78,8 +73,7 @@
 * Fix highlighting of search results and trailing whitespaces in intraline
 diff chunks.
 
-Plugins
-~~~~~~~
+=== Plugins
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=3768[Issue 3768]:
 Fix usage of `EqualsFilePredicate` in plugins.
@@ -115,8 +109,7 @@
 
 ** Allow to use GWTORM `Key` classes.
 
-Documentation Updates
----------------------
+== Documentation Updates
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=412[Issue 412]:
 Update documentation of `commentlink.match` regular expression to clarify
diff --git a/ReleaseNotes/ReleaseNotes-2.11.7.txt b/ReleaseNotes/ReleaseNotes-2.11.7.txt
index 7a0de2d..6742279 100644
--- a/ReleaseNotes/ReleaseNotes-2.11.7.txt
+++ b/ReleaseNotes/ReleaseNotes-2.11.7.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.11.7
-===============================
+= Release notes for Gerrit 2.11.7
 
 Gerrit 2.11.7 is now available:
 
@@ -8,8 +7,7 @@
 
 There are no schema changes from link:ReleaseNotes-2.11.6.html[2.11.6].
 
-Bug Fixes
----------
+== Bug Fixes
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=3882[Issue 3882]:
 Fix 'No user on email thread' exception when label with group parameter is
diff --git a/ReleaseNotes/ReleaseNotes-2.11.8.txt b/ReleaseNotes/ReleaseNotes-2.11.8.txt
index 0f9dc21..0aa8dfc 100644
--- a/ReleaseNotes/ReleaseNotes-2.11.8.txt
+++ b/ReleaseNotes/ReleaseNotes-2.11.8.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.11.8
-===============================
+= Release notes for Gerrit 2.11.8
 
 Gerrit 2.11.8 is now available:
 
@@ -8,8 +7,7 @@
 
 There are no schema changes from link:ReleaseNotes-2.11.7.html[2.11.7].
 
-Bug Fixes
----------
+== Bug Fixes
 
 * Upgrade Apache commons-collections to version 3.2.2.
 +
diff --git a/ReleaseNotes/ReleaseNotes-2.11.9.txt b/ReleaseNotes/ReleaseNotes-2.11.9.txt
new file mode 100644
index 0000000..52ee3fe
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.11.9.txt
@@ -0,0 +1,49 @@
+= Release notes for Gerrit 2.11.9
+
+Gerrit 2.11.9 is now available:
+
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.11.9.war[
+https://gerrit-releases.storage.googleapis.com/gerrit-2.11.9.war]
+
+There are no schema changes from link:ReleaseNotes-2.11.8.html[2.11.8].
+
+== Bug Fixes
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=4070[Issue 4070]:
+Don't return current patch set in queries if the current patch set is not
+visible.
++
+When querying changes with the `gerrit query` ssh command, and passing the
+`--current-patch-set` option, the current patch set was included even when
+it is not visible to the caller (for example when the patch set is a draft,
+and the caller cannot see drafts).
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3970[Issue 3970]:
+Fix keyboard shortcuts for special processing of CTRL and META.
++
+The processing of CTRL and META was incorrectly removed in Gerrit version
+2.11.8, resulting in shortcuts like 'STRG+T' being interpreted as 'T'.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=4056[Issue 4056]:
+Fix download URLs for BouncyCastle libraries.
++
+The location of the libraries was moved, so the download URLs are updated
+accordingly.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=4055[Issue 4055]:
+Fix subject for 'Updated Changes' lines on push.
++
+When a change was updated it showed the subject from the previous patch set
+instead of the subject from the new current patch set.
+
+* Fix incorrect loading of access sections in `project.config` files.
+
+* Fix internal server error when `auth.userNameToLowerCase` is enabled
+and the auth backend does not provide the username.
+
+* Fix error reindexing changes when a change no longer exists.
+
+* Fix internal server error when loading submit rules.
+
+* Fix internal server error when parsing tracking footers from commit
+messages.
diff --git a/ReleaseNotes/ReleaseNotes-2.11.txt b/ReleaseNotes/ReleaseNotes-2.11.txt
index 44c5398..1ca6825 100644
--- a/ReleaseNotes/ReleaseNotes-2.11.txt
+++ b/ReleaseNotes/ReleaseNotes-2.11.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.11
-=============================
+= Release notes for Gerrit 2.11
 
 
 Gerrit 2.11 is now available:
@@ -14,8 +13,7 @@
 These bug fixes are *not* listed in these release notes.
 
 
-Important Notes
----------------
+== Important Notes
 
 
 *WARNING:* This release contains schema changes.  To upgrade:
@@ -82,8 +80,7 @@
 
 *WARNING:* The deprecated '/query' URL is removed and will now return `Not Found`.
 
-Release Highlights
-------------------
+== Release Highlights
 
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=505[Issue 505]:
@@ -95,16 +92,13 @@
 * The old change screen is removed.
 
 
-New Features
-------------
+== New Features
 
 
-Web UI
-~~~~~~
+=== Web UI
 
 [[inline-editing]]
-Inline Editing
-^^^^^^^^^^^^^^
+==== Inline Editing
 
 Refer to the
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/user-inline-edit.html[
@@ -133,8 +127,7 @@
 
 * Files can be added, deleted, restored and modified directly in browser.
 
-Change Screen
-^^^^^^^^^^^^^
+==== Change Screen
 
 * Remove the 'Edit Message' button from the change screen.
 +
@@ -175,8 +168,7 @@
 * Show changes across all projects and branches in the `Same Topic` tab.
 
 
-Side-By-Side Diff
-^^^^^^^^^^^^^^^^^
+==== Side-By-Side Diff
 
 * New button to switch between side-by-side diff and unified diff.
 
@@ -206,8 +198,7 @@
 ** Soy
 
 
-Projects Screen
-^^^^^^^^^^^^^^^
+==== Projects Screen
 
 * Add pagination and filtering on the branch list page.
 
@@ -220,17 +211,14 @@
 browser, which is useful since it is possible that not all configuration options
 are available in the UI.
 
-REST
-~~~~
+=== REST
 
-Accounts
-^^^^^^^^
+==== Accounts
 
 * Add new link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/rest-api-accounts.html#suggest-account[
 Suggest Account endpoint].
 
-Changes
-^^^^^^^
+==== Changes
 
 * The link:https://gerrit-review.googlesource.com/Documentation/2.10/rest-api-changes.html#message[
 Edit Commit Message] endpoint is removed in favor of the new
@@ -260,8 +248,7 @@
 Get Change Detail] endpoint.
 
 
-Change Edits
-^^^^^^^^^^^^
+==== Change Edits
 
 Several new endpoints are added to support the inline edit feature.
 
@@ -296,8 +283,7 @@
 Delete Change Edit].
 
 
-Projects
-^^^^^^^^
+==== Projects
 
 * Add new
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/rest-api-projects.html#delete-branches[
@@ -316,8 +302,7 @@
 Get Tag] endpoint.
 
 
-Configuration
-~~~~~~~~~~~~~
+=== Configuration
 
 * Add support for
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/config-gerrit.html#auth.httpExternalIdHeader[
@@ -395,8 +380,7 @@
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/config-gerrit.html#container.daemonOpt[
 options to pass to the daemon].
 
-Daemon
-~~~~~~
+=== Daemon
 
 * Allow to enable the http daemon when running in slave mode.
 +
@@ -416,8 +400,7 @@
 * Don't send 'new patch set' notification emails for trivial rebases.
 
 
-SSH
-~~~
+=== SSH
 
 * Add new commands
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/cmd-logging-ls-level.html[
@@ -448,8 +431,7 @@
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/cmd-set-account.html[
 `set-account` SSH command].
 
-Email
-~~~~~
+=== Email
 
 * Add `$change.originalSubject` field for email templates.
 +
@@ -464,11 +446,9 @@
 field during first use.
 
 
-Plugins
-~~~~~~~
+=== Plugins
 
-General
-^^^^^^^
+==== General
 
 * Plugins can listen to account group membership changes.
 +
@@ -518,17 +498,14 @@
 ** Get comments and drafts.
 ** Get change edit.
 
-Replication
-^^^^^^^^^^^
+==== Replication
 
 * Projects can be specified with wildcard in the `start` command.
 
 
-Bug Fixes
----------
+== Bug Fixes
 
-Daemon
-~~~~~~
+=== Daemon
 
 * Change 'Merge topic' to 'Merge changes from topic'.
 +
@@ -564,8 +541,7 @@
 of a repository with a space in its name was impossible.
 
 
-Secondary Index / Search
-~~~~~~~~~~~~~~~~~~~~~~~~
+=== Secondary Index / Search
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=2822[Issue 2822]:
 Improve Lucene analysis of words linked with underscore or dot.
@@ -576,8 +552,7 @@
 * Fix support for `change~branch~id` in query syntax.
 
 
-Configuration
-~~~~~~~~~~~~~
+=== Configuration
 
 [[remove-generate-http-password-capability]]
 * Remove the 'Generate HTTP Password' capability.
@@ -608,17 +583,14 @@
 read from `hooks.changeMerged`. Fix to use `hooks.changeMergedHook` as
 documented.
 
-Web UI
-~~~~~~
+=== Web UI
 
-Change List
-^^^^^^^^^^^
+==== Change List
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=3304[Issue 3304]:
 Always show a tooltip on the label column entries.
 
-Change Screen
-^^^^^^^^^^^^^
+==== Change Screen
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=3147[Issue 3147]:
 Allow to disable muting of common path prefixes in the file list.
@@ -742,8 +714,7 @@
 Align parent weblinks with parent commits in the commit box.
 
 
-Side-By-Side Diff
-^^^^^^^^^^^^^^^^^
+==== Side-By-Side Diff
 
 * Return to normal mode after editing a draft comment.
 +
@@ -756,18 +727,15 @@
 highlighter.
 
 
-Project Screen
-^^^^^^^^^^^^^^
+==== Project Screen
 
 * Fix alignment of checkboxes on project access screen.
 +
 The 'Exclusive' checkbox was not aligned with the other checkboxes.
 
-REST API
-~~~~~~~~
+=== REST API
 
-Changes
-^^^^^^^
+==== Changes
 
 * Remove the administrator restriction on the
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/rest-api-changes.html#index-change[
@@ -800,8 +768,7 @@
 `409 Conflict`.
 
 
-Projects
-^^^^^^^^
+==== Projects
 
 * Make it mandatory to specify at least one of the `--prefix`, `--match` or `--regex`
 options in the
@@ -826,11 +793,9 @@
 others.
 
 
-Plugins
-~~~~~~~
+=== Plugins
 
-Replication
-^^^^^^^^^^^
+==== Replication
 
 * Create missing repositories on the remote when replicating with the git
 protocol.
@@ -843,8 +808,7 @@
 create a project on the remote.
 
 
-Upgrades
---------
+== Upgrades
 
 * Update Antlr to 3.5.2.
 
diff --git a/ReleaseNotes/ReleaseNotes-2.12.1.txt b/ReleaseNotes/ReleaseNotes-2.12.1.txt
index f49de7d..e746d6e 100644
--- a/ReleaseNotes/ReleaseNotes-2.12.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.12.1.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.12.1
-===============================
+= Release notes for Gerrit 2.12.1
 
 Gerrit 2.12.1 is now available:
 
@@ -11,8 +10,7 @@
 link:ReleaseNotes-2.11.7.html[Gerrit 2.11.7]. These bug fixes are *not*
 listed in these release notes.
 
-Schema Upgrade
---------------
+== Schema Upgrade
 
 *WARNING:* This version includes a manual schema upgrade when upgrading
 from 2.12.
@@ -48,11 +46,9 @@
 necessary and should be omitted.
 
 
-Bug Fixes
----------
+== Bug Fixes
 
-General
-~~~~~~~
+=== General
 
 * Fix column type for signed push certificates.
 +
@@ -160,8 +156,7 @@
 * link:https://code.google.com/p/gerrit/issues/detail?id=3883[Issue 3883]:
 Respect the `core.commentchar` setting from `.gitconfig` in `commit-msg` hook.
 
-UI
-~~
+=== UI
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=3894[Issue 3894]:
 Fix display of 'Related changes' after change is rebased in web UI:
@@ -194,8 +189,7 @@
 * Improve tooltip on 'Submit' button when 'Submit whole topic' is enabled
 and the topic can't be submitted due to some changes not being ready.
 
-Plugins
-~~~~~~~
+=== Plugins
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=3821[Issue 3821]:
 Fix repeated reloading of plugins when running on OpenJDK 8.
@@ -223,14 +217,12 @@
 Allow plugins to suggest reviewers based on either change or project
 resources.
 
-Documentation
-~~~~~~~~~~~~~
+=== Documentation
 
 * Update documentation of `commentlink` to reflect changed search URL.
 
 * Add missing documentation of valid `database.type` values.
 
-Upgrades
---------
+== Upgrades
 
 * Upgrade JGit to 4.1.2.201602141800-r.
diff --git a/ReleaseNotes/ReleaseNotes-2.12.2.txt b/ReleaseNotes/ReleaseNotes-2.12.2.txt
index 500b015..8292eb5 100644
--- a/ReleaseNotes/ReleaseNotes-2.12.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.12.2.txt
@@ -1,13 +1,11 @@
-Release notes for Gerrit 2.12.2
-===============================
+= Release notes for Gerrit 2.12.2
 
 Gerrit 2.12.2 is now available:
 
 link:https://gerrit-releases.storage.googleapis.com/gerrit-2.12.2.war[
 https://gerrit-releases.storage.googleapis.com/gerrit-2.12.2.war]
 
-Schema Upgrade
---------------
+== Schema Upgrade
 
 *WARNING:* There are no schema changes from link:ReleaseNotes-2.12.1.html[
 2.12.1] but a manual schema upgrade is necessary when upgrading from 2.12.
@@ -43,8 +41,7 @@
 done the migration, this manual step is not necessary and should be omitted.
 
 
-Bug Fixes
----------
+== Bug Fixes
 
 * Upgrade Apache commons-collections to version 3.2.2.
 +
diff --git a/ReleaseNotes/ReleaseNotes-2.12.txt b/ReleaseNotes/ReleaseNotes-2.12.txt
index 797a138..65a4484 100644
--- a/ReleaseNotes/ReleaseNotes-2.12.txt
+++ b/ReleaseNotes/ReleaseNotes-2.12.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.12
-=============================
+= Release notes for Gerrit 2.12
 
 
 Gerrit 2.12 is now available:
@@ -7,8 +6,7 @@
 link:https://www.gerritcodereview.com/download/gerrit-2.12.war[
 https://www.gerritcodereview.com/download/gerrit-2.12.war]
 
-Important Notes
----------------
+== Important Notes
 
 *WARNING:* This release contains schema changes.  To upgrade:
 ----
@@ -39,8 +37,7 @@
 `refs/*/master` instead of `Plain` and `master`.
 
 
-Release Highlights
-------------------
+== Release Highlights
 
 This release includes the following new features. See the sections below for
 further details.
@@ -50,11 +47,9 @@
 * Support for GPG Keys and signed pushes.
 
 
-New Features
-------------
+== New Features
 
-New Change Submission Workflows
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+=== New Change Submission Workflows
 
 * New 'Submit Whole Topic' setting.
 +
@@ -76,8 +71,7 @@
 enter the 'Submitted, Merge Pending' state.
 
 
-GPG Keys and Signed Pushes
-~~~~~~~~~~~~~~~~~~~~~~~~~~
+=== GPG Keys and Signed Pushes
 
 * Signed push can be enabled by setting
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/config-gerrit.html#receive.enableSignedPush[
@@ -102,8 +96,7 @@
 `receive.certNonceSlop`].
 
 
-Secondary Index
-~~~~~~~~~~~~~~~
+=== Secondary Index
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=3333[Issue 3333]:
 Support searching for changes by author and committer.
@@ -141,11 +134,9 @@
 sense to enforce all changes to be written to disk ASAP.
 
 
-UI
-~~
+=== UI
 
-General
-^^^^^^^
+==== General
 
 * Edit and diff preferences can be modified from the user preferences screen.
 +
@@ -165,14 +156,12 @@
 users.
 
 
-Project Screen
-^^^^^^^^^^^^^^
+==== Project Screen
 
 * New tab to list the project's tags, similar to the branch list.
 
 
-Inline Editor
-^^^^^^^^^^^^^
+==== Inline Editor
 
 * Store and load edit preferences in git.
 +
@@ -187,8 +176,7 @@
 * Add support for Emacs and Vim key maps.
 
 
-Change Screen
-^^^^^^^^^^^^^
+==== Change Screen
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=3318[Issue 3318]:
 Highlight 'Reply' button if there are draft comments on any patch set.
@@ -216,8 +204,7 @@
 This helps to identify changes when the subject is truncated in the list.
 
 
-Side-By-Side Diff
-^^^^^^^^^^^^^^^^^
+==== Side-By-Side Diff
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=3293[Issue 3293]:
 Add syntax highlighting for Puppet.
@@ -226,40 +213,34 @@
 Add syntax highlighting for VHDL.
 
 
-Group Screen
-^^^^^^^^^^^^
+==== Group Screen
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=1479[Issue 1479]:
 The group screen now includes an 'Audit Log' panel showing member additions,
 removals, and the user who made the change.
 
 
-API
-~~~
+=== API
 
 Several new APIs are added.
 
-Accounts
-^^^^^^^^
+==== Accounts
 
 * Suggest accounts.
 
-Tags
-^^^^
+==== Tags
 
 * List tags.
 
 * Get tag.
 
 
-REST API
-~~~~~~~~
+=== REST API
 
 New REST API endpoints and new options on existing endpoints.
 
 
-Accounts
-^^^^^^^^
+==== Accounts
 
 * link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-accounts.html#set-username[
 Set Username]: Set the username of an account.
@@ -276,8 +257,7 @@
 account.
 
 
-Changes
-^^^^^^^
+==== Changes
 
 * link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-changes.html#set-review[
 Set Review]: Add an option to omit duplicate comments.
@@ -294,8 +274,7 @@
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-changes.html#set-review[
 Set Review]: Add an option to publish draft comments on all revisions.
 
-Config
-^^^^^^
+==== Config
 
 * link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-config.html#get-info[
 Get Server Info]: Return information about the Gerrit server configuration.
@@ -304,8 +283,7 @@
 Confirm Email]: Confirm that the user owns an email address.
 
 
-Groups
-^^^^^^
+==== Groups
 
 * link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-groups.html#list-group[
 List Groups]: Add option to suggest groups.
@@ -317,8 +295,7 @@
 additions, removals, and the user who made the change.
 
 
-Projects
-^^^^^^^^
+==== Projects
 
 * link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-projects.html#run-gc[
 Run GC]: Add `aggressive` option to specify whether or not to run an aggressive
@@ -329,8 +306,7 @@
 `--start` and `--end`.
 
 
-SSH
-~~~
+=== SSH
 
 * Add support for ZLib Compression.
 +
@@ -340,11 +316,9 @@
 
 * Add support for hmac-sha2-256 and hmac-sha2-512 as MACs.
 
-Plugins
-~~~~~~~
+=== Plugins
 
-General
-^^^^^^^
+==== General
 
 * Gerrit client can now pass JavaScriptObjects to extension panels.
 
@@ -387,8 +361,7 @@
 ** Allow to use GWTORM `Key` classes.
 
 
-Other
-~~~~~
+=== Other
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=3401[Issue 3401]:
 Add option to
@@ -433,8 +406,7 @@
 and queues, and invoke the index REST API on changes.
 
 
-Bug Fixes
----------
+== Bug Fixes
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=3499[Issue 3499]:
 Fix syntax highlighting of raw string literals in go.
@@ -536,8 +508,7 @@
 +
 Under some circumstances it was possible to fail with an IO error.
 
-Documentation Updates
----------------------
+== Documentation Updates
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=412[Issue 412]:
 Update documentation of `commentlink.match` regular expression to clarify
@@ -549,8 +520,7 @@
 
 * Document that `ldap.groupBase` and `ldap.accountBase` are repeatable.
 
-Upgrades
---------
+== Upgrades
 
 * Upgrade Asciidoctor to 1.5.2
 
diff --git a/ReleaseNotes/ReleaseNotes-2.13.txt b/ReleaseNotes/ReleaseNotes-2.13.txt
index aba6131..07ceb4d 100644
--- a/ReleaseNotes/ReleaseNotes-2.13.txt
+++ b/ReleaseNotes/ReleaseNotes-2.13.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.13
-=============================
+= Release notes for Gerrit 2.13
 
 
 Gerrit 2.13 is now available:
@@ -8,23 +7,19 @@
 https://www.gerritcodereview.com/download/gerrit-2.13.war]
 
 
-Important Notes
----------------
+== Important Notes
 
 TODO
 
 
-Release Highlights
-------------------
+== Release Highlights
 
 * Metrics interface.
 
 
-New Features
-------------
+== New Features
 
-Metrics
-~~~~~~~
+=== Metrics
 
 Metrics about Gerrit's internal state can be sent to external
 monitoring systems.
@@ -66,20 +61,39 @@
 
 * TODO add more
 
-Changes
-~~~~~~~
+=== Changes
 
 In order to avoid potentially confusing behavior, when submitting changes in a
 batch, submit type rules may not be used to mix submit types on a single branch,
 and trying to submit such a batch will fail.
 
-Bug Fixes
----------
+== Bug Fixes
 
 TODO
 
 
-Upgrades
---------
+== Upgrades
 
-* Upgrade Lucene to 5.3.1
+* Upgrade CodeMirror to 5.14.2
+
+* Upgrade Guava to 19.0
+
+* Upgrade Gson to 2.6.2
+
+* Upgrade gwtjsonrpc to version 1.8
+
+* Upgrade gwtorm to 1.15
+
+* Upgrade javassist.jar to 3.18.1
+
+* Upgrade Jetty to 9.2.14.v20151106
+
+* Upgrade JGit to 4.3.0.201604071810-r
+
+* Upgrade Lucene to 5.4.1
+
+* Upgrade mina to 2.10
+
+* Upgrade sshd-core to 1.2.0
+
+* Upgrade Truth to 0.28
diff --git a/ReleaseNotes/ReleaseNotes-2.2.0.txt b/ReleaseNotes/ReleaseNotes-2.2.0.txt
index 5938f66..5cc54f9 100644
--- a/ReleaseNotes/ReleaseNotes-2.2.0.txt
+++ b/ReleaseNotes/ReleaseNotes-2.2.0.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.2.0
-==============================
+= Release notes for Gerrit 2.2.0
 
 Gerrit 2.2.0 is now available:
 
 link:https://www.gerritcodereview.com/download/gerrit-2.2.0.war[https://www.gerritcodereview.com/download/gerrit-2.2.0.war]
 
-Schema Change
--------------
+== Schema Change
 *WARNING:* Upgrading to 2.2.0 requires the server be first upgraded
 to 2.1.7, and then to 2.2.0.
 
@@ -21,11 +19,9 @@
 Git repository. The init based upgrade tool will automatically
 export the current table contents and create the Git data.
 
-New Features
-------------
+== New Features
 
-Project Administration
-~~~~~~~~~~~~~~~~~~~~~~
+=== Project Administration
 * issue 436 List projects by scanning the managed Git directory
 +
 Instead of generating the list of projects from SQL database, the
@@ -52,11 +48,9 @@
 The Access panel of the project administration has been rewritten
 with a new UI that reflects the new Git based storage format.
 
-Bug Fixes
----------
+== Bug Fixes
 
-Project Administration
-~~~~~~~~~~~~~~~~~~~~~~
+=== Project Administration
 * Avoid unnecessary updates to $GIT_DIR/description
 +
 Gerrit always tried to rewrite the gitweb "description" file when the
diff --git a/ReleaseNotes/ReleaseNotes-2.2.1.txt b/ReleaseNotes/ReleaseNotes-2.2.1.txt
index 6a4829e..26aa8db 100644
--- a/ReleaseNotes/ReleaseNotes-2.2.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.2.1.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.2.1
-==============================
+= Release notes for Gerrit 2.2.1
 
 Gerrit 2.2.1 is now available:
 
 link:https://www.gerritcodereview.com/download/gerrit-2.2.1.war[https://www.gerritcodereview.com/download/gerrit-2.2.1.war]
 
-Schema Change
--------------
+== Schema Change
 *WARNING:* This release contains schema changes.  To upgrade:
 ----
   java -jar gerrit.war init -d site_path
@@ -15,8 +13,7 @@
 *WARNING:* Upgrading to 2.2.x requires the server be first upgraded
 to 2.1.7, and then to 2.2.x.
 
-New Features
-------------
+== New Features
 * Add 'Expand All Comments' checkbox in PatchScreen
 +
 Allows users to save a user preference that automatically expands
@@ -28,8 +25,7 @@
 usage adds a new column of output per project line listing the
 current value of that branch.
 
-Bug Fixes
----------
+== Bug Fixes
 * issue 994 Rename "-- All Projects --" to "All-Projects"
 +
 The name "-- All Projects --.git" is difficult to work with on
diff --git a/ReleaseNotes/ReleaseNotes-2.2.2.1.txt b/ReleaseNotes/ReleaseNotes-2.2.2.1.txt
index aabe03a..37f5a76 100644
--- a/ReleaseNotes/ReleaseNotes-2.2.2.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.2.2.1.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.2.2.1
-================================
+= Release notes for Gerrit 2.2.2.1
 
 Gerrit 2.2.2.1 is now available:
 
@@ -11,8 +10,7 @@
 link:ReleaseNotes-2.2.2.html[ReleaseNotes].
 
 
-Bug Fixes
----------
+== Bug Fixes
 * issue 1139 Fix change state in patch set approval if reviewer is added to
 closed change
 +
@@ -35,8 +33,7 @@
 commit 14246de3c0f81c06bba8d4530e6bf00e918c11b0
 
 
-Documentation
--------------
+== Documentation
 * Update top level SUBMITTING_PATCHES
 +
 This document is out of date, the URLs are from last August.
diff --git a/ReleaseNotes/ReleaseNotes-2.2.2.2.txt b/ReleaseNotes/ReleaseNotes-2.2.2.2.txt
index 2747ab0..f50c4e7 100644
--- a/ReleaseNotes/ReleaseNotes-2.2.2.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.2.2.2.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.2.2.2
-================================
+= Release notes for Gerrit 2.2.2.2
 
 Gerrit 2.2.2.2 is now available:
 
@@ -10,8 +9,7 @@
 However, if upgrading from anything earlier, follow the upgrade
 procedure in the 2.2.2 link:ReleaseNotes-2.2.2.html[ReleaseNotes].
 
-Security Fixes
---------------
+== Security Fixes
 * Some access control sections may be ignored
 +
 Gerrit sometimes ignored an access control section in a project
diff --git a/ReleaseNotes/ReleaseNotes-2.2.2.txt b/ReleaseNotes/ReleaseNotes-2.2.2.txt
index 3889dcc..276714c 100644
--- a/ReleaseNotes/ReleaseNotes-2.2.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.2.2.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.2.2
-==============================
+= Release notes for Gerrit 2.2.2
 
 Gerrit 2.2.2 is now available:
 
 link:https://www.gerritcodereview.com/download/gerrit-2.2.2.war[https://www.gerritcodereview.com/download/gerrit-2.2.2.war]
 
-Schema Change
--------------
+== Schema Change
 *WARNING:* This release contains schema changes.  To upgrade:
 ----
   java -jar gerrit.war init -d site_path
@@ -15,11 +13,9 @@
 *WARNING:* Upgrading to 2.2.x requires the server be first upgraded
 to 2.1.7 (or a later 2.1.x version), and then to 2.2.x.
 
-New Features
-------------
+== New Features
 
-Prolog
-~~~~~~
+=== Prolog
 * issue 971 Use Prolog Cafe for ChangeControl.canSubmit()
 
 *  Add per-project prolog submit rule files
@@ -59,8 +55,7 @@
 administrators can play around with by downloading the Gerrit WAR
 file and executing: java -jar gerrit.war prolog-shell
 
-Prolog Predicates
-^^^^^^^^^^^^^^^^^
+==== Prolog Predicates
 *  Add Prolog Predicates to check commit messages and edits
 +
 commit_message returns the commit message as a symbol.
@@ -104,8 +99,7 @@
 it exportable for now until we can come back and clean up the legacy
 approval data code.
 
-Web
-~~~
+=== Web
 
 * Support in Firefox delete key in NpIntTextBox
 +
@@ -118,8 +112,7 @@
 There is a bug in gwt 2.1.0 that prevents pressing special keys like
 Enter, Backspace etc. from being properly recognized and so they have no effect.
 
-ChangeScreen
-^^^^^^^^^^^^
+==== ChangeScreen
 * issue 855 Indicate outdated dependencies on the ChangeScreen
 +
 If a change dependency is no longer the latest patchSet for that
@@ -142,8 +135,7 @@
 permits the submit_rule to make an ApprovalCategory optional, or to
 make a new label required.
 
-Diff Screen
-^^^^^^^^^^^
+==== Diff Screen
 * Add top level menus for a new PatchScreen header
 +
 Modify the PatchScreen so that the header contents is selectable
@@ -183,8 +175,7 @@
 automatic result that Git would create, and the actual result that
 was uploaded by the author/committer of the merge.
 
-Groups
-^^^^^^
+==== Groups
 * Add menu to AccountGroupScreen
 +
 This change introduces a menu in the AccountGroupScreen and
@@ -199,8 +190,7 @@
 'groups' file in the 'refs/meta/config' branch which requires
 the UUID of the group to be known.
 
-Project Access
-^^^^^^^^^^^^^^
+==== Project Access
 * Automatically add new rule when adding new permission
 +
 If a new permission was added to a block, immediately create the new
@@ -218,8 +208,7 @@
 switch back to the "read-only" view where the widgets are all
 disabled and the Edit button is enabled.
 
-Project Branches
-^^^^^^^^^^^^^^^^
+==== Project Branches
 * Display refs/meta/config branch on ProjectBranchesScreen
 +
 The new refs/meta/config branch was not shown in the ProjectBranchesScreen.
@@ -231,8 +220,7 @@
 Since HEAD and refs/meta/config do not represent ordinary branches,
 highlight their rows with a special style in the ProjectBranchesScreen.
 
-URLs
-^^^^
+==== URLs
 * Modernize URLs to be shorter and consistent
 +
 Instead of http://site/#change,1234 we now use a slightly more
@@ -250,8 +238,7 @@
 
 * issue 1018 Accept ~ in linkify() URLs
 
-SSH
-~~~
+=== SSH
 * Added a set-reviewers ssh command
 
 * Support removing more than one reviewer at once
@@ -290,8 +277,7 @@
 will report additional data about the JVM, and tell the caller
 where it is running.
 
-Queries
-^^^^^^^
+==== Queries
 * Output patchset creation date for 'query' command.
 
 * issue 1053 Support comments option in query command
@@ -300,8 +286,7 @@
 used. If --comments is used together with --patch-sets all inline
 comments are included in the output.
 
-Config
-~~~~~~
+=== Config
 * Move batch user priority to a capability
 +
 Instead of using a magical group, use a special capability to
@@ -351,8 +336,7 @@
 This allows aliases which redirect to gerrit's ssh port (say
 from port 22) to be setup and advertised to users.
 
-Dev
-~~~
+=== Dev
 * Updated eclipse settings for 3.7 and m2e 1.0
 
 * Fix build in m2eclipse 1.0
@@ -375,8 +359,7 @@
 switching between different users when using the
 DEVELOPMENT_BECOME_ANY_ACCOUNT authentication type.
 
-Miscellaneous
-~~~~~~~~~~~~~
+=== Miscellaneous
 * Permit adding reviewers to closed changes
 +
 Permit adding a reviewer to closed changes to support post-submit
@@ -412,8 +395,7 @@
 gerrit page load.
 
 
-Performance
------------
+== Performance
 * Bumped Brics version to 1.11.8
 +
 This Brics version fixes a performance issue in some larger Gerrit systems.
@@ -453,8 +435,7 @@
 during the construction of ProjectState is a waste of resources.
 
 
-Upgrades
---------
+== Upgrades
 * Upgrade to GWT 2.3.0
 * Upgrade to Gson to 1.7.1
 * Upgrade to gwtjsonrpc 1.2.4
@@ -463,8 +444,7 @@
 * Upgrade to Brics 1.11.8
 
 
-Bug Fixes
----------
+== Bug Fixes
 * Fix: Issue where Gerrit could not linkify certain URLs
 
 * issue 1015 Fix handling of regex ref patterns in Access panel
@@ -563,11 +543,9 @@
 duplicate account_ids later.
 
 
-Documentation
--------------
+== Documentation
 
-New Documents
-~~~~~~~~~~~~~
+=== New Documents
 * First Cut of Gerrit Walkthrough Introduction documentation.
 +
 Add a new document intended to be a complement for the existing
@@ -580,8 +558,7 @@
 The new document covers quick installation, new project and first
 upload.  It contains lots of quoted output, with a demo style to it.
 
-Access Control
-~~~~~~~~~~~~~~
+=== Access Control
 * Code review
 
 * Conversion table between 2.1 and 2.2
@@ -614,8 +591,7 @@
 +
 Access categories are now sorted to match drop down box in UI
 
-Other Documentation
-~~~~~~~~~~~~~~~~~~~
+=== Other Documentation
 * Added additional information on the install instructions.
 +
 The installation instructions presumes much prior knowledge,
diff --git a/ReleaseNotes/ReleaseNotes-2.3.1.txt b/ReleaseNotes/ReleaseNotes-2.3.1.txt
index 8914c69..627fba5 100644
--- a/ReleaseNotes/ReleaseNotes-2.3.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.3.1.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.3.1
-==============================
+= Release notes for Gerrit 2.3.1
 
 Gerrit 2.3.1 is now available:
 
@@ -10,8 +9,7 @@
 However, if upgrading from anything earlier, follow the upgrade
 procedure in the 2.3 link:ReleaseNotes-2.3.html[ReleaseNotes].
 
-Security Fixes
---------------
+== Security Fixes
 * Some access control sections may be ignored
 +
 Gerrit sometimes ignored an access control section in a project
diff --git a/ReleaseNotes/ReleaseNotes-2.3.txt b/ReleaseNotes/ReleaseNotes-2.3.txt
index 9cdc886..7a29d0e 100644
--- a/ReleaseNotes/ReleaseNotes-2.3.txt
+++ b/ReleaseNotes/ReleaseNotes-2.3.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.3
-============================
+= Release notes for Gerrit 2.3
 
 Gerrit 2.3 is now available:
 
 link:https://www.gerritcodereview.com/download/gerrit-2.3.war[https://www.gerritcodereview.com/download/gerrit-2.3.war]
 
-Schema Change
--------------
+== Schema Change
 *WARNING:* This release contains schema changes.  To upgrade:
 ----
   java -jar gerrit.war init -d site_path
@@ -19,10 +17,8 @@
 upgrade directly to 2.3.x.
 
 
-New Features
-------------
-Drafts
-~~~~~~
+== New Features
+=== Drafts
 * New draft statuses and magic branches
 +
 Adds draft status to Change. DRAFT status in change occurs before NEW
@@ -65,8 +61,7 @@
 * When pushing changes as drafts, output [DRAFT] next to the change link
 
 
-Web
-~~~
+=== Web
 * issue 203 Create project through web interface
 +
 Add a new panel in the Admin->Projects Screen.  It
@@ -106,8 +101,7 @@
 * Disable SSH Keys in the web UI if SSHD is disabled
 
 
-SSH
-~~~
+=== SSH
 * Adds --description (-d) option to ls-projects
 +
 Allows listing of projects together with their respective
@@ -167,8 +161,7 @@
 labels could not be applied due to change being closed.
 
 
-Config
-~~~~~~
+=== Config
 * issue 349 Apply states for projects (active, readonly and hidden)
 +
 Active state indicates the project is regular and is the default value.
@@ -250,8 +243,7 @@
 logic associated with the site header, footer and CSS.
 
 
-Dev
-~~~
+=== Dev
 * Fix 'No source code is available for type org.eclipse.jgit.lib.Constants'
 
 * Fix miscellaneous compiler warnings
@@ -270,8 +262,7 @@
 Fixes java.lang.NoClassDefFoundError: com/google/gwt/dev/DevMode
 
 
-Miscellaneous
-~~~~~~~~~~~~~
+=== Miscellaneous
 * Allow superprojects to subscribe to submodules updates
 +
 The feature introduced in this release allows superprojects to
@@ -370,8 +361,7 @@
 * Improve validation of email registration tokens
 
 
-Upgrades
---------
+== Upgrades
 * Upgrade to gwtorm 1.2
 
 * Upgrade to JGit 1.1.0.201109151100-r.119-gb4495d1
@@ -382,8 +372,7 @@
 * Support Velocity 1.5 (as well as previous 1.6.4)
 
 
-Bug Fixes
----------
+== Bug Fixes
 * Avoid NPE when group is missing
 
 * Do not fail with NPE if context path of request is null
@@ -437,8 +426,7 @@
 * Update top level SUBMITTING_PATCHES URLs
 
 
-Documentation
--------------
+== Documentation
 * Some updates to the design docs
 
 * cmd-index: Fix link to documentation of rename-group command
diff --git a/ReleaseNotes/ReleaseNotes-2.4.1.txt b/ReleaseNotes/ReleaseNotes-2.4.1.txt
index dbe6c4b..f3c4765 100644
--- a/ReleaseNotes/ReleaseNotes-2.4.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.4.1.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.4.1
-==============================
+= Release notes for Gerrit 2.4.1
 
 Gerrit 2.4.1 is now available:
 
@@ -11,8 +10,7 @@
 link:ReleaseNotes-2.4.html[ReleaseNotes].
 
 
-Bug Fixes
----------
+== Bug Fixes
 * Catch all exceptions when async emailing
 +
 This fixes email notification issues reported
diff --git a/ReleaseNotes/ReleaseNotes-2.4.2.txt b/ReleaseNotes/ReleaseNotes-2.4.2.txt
index 5652d15..d5c2a11 100644
--- a/ReleaseNotes/ReleaseNotes-2.4.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.4.2.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.4.2
-==============================
+= Release notes for Gerrit 2.4.2
 
 Gerrit 2.4.2 is now available:
 
@@ -10,8 +9,7 @@
 However, if upgrading from anything earlier, follow the upgrade
 procedure in the 2.4 link:ReleaseNotes-2.4.html[ReleaseNotes].
 
-Security Fixes
---------------
+== Security Fixes
 * Some access control sections may be ignored
 +
 Gerrit sometimes ignored an access control section in a project
diff --git a/ReleaseNotes/ReleaseNotes-2.4.3.txt b/ReleaseNotes/ReleaseNotes-2.4.3.txt
index 6745564..ece0bda 100644
--- a/ReleaseNotes/ReleaseNotes-2.4.3.txt
+++ b/ReleaseNotes/ReleaseNotes-2.4.3.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.4.3
-==============================
+= Release notes for Gerrit 2.4.3
 
 There are no schema changes from link:ReleaseNotes-2.4.2.html[2.4.2].
 
 link:https://www.gerritcodereview.com/download/gerrit-2.4.3.war[https://www.gerritcodereview.com/download/gerrit-2.4.3.war]
 
-Bug Fixes
----------
+== Bug Fixes
 * Patch JGit security hole
 +
 The security hole may permit a modified Git client to gain access
diff --git a/ReleaseNotes/ReleaseNotes-2.4.4.txt b/ReleaseNotes/ReleaseNotes-2.4.4.txt
index 5570271..f9ea6b5 100644
--- a/ReleaseNotes/ReleaseNotes-2.4.4.txt
+++ b/ReleaseNotes/ReleaseNotes-2.4.4.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.4.4
-==============================
+= Release notes for Gerrit 2.4.4
 
 There are no schema changes from link:ReleaseNotes-2.4.4.html[2.4.4].
 
 link:https://www.gerritcodereview.com/download/gerrit-2.4.4.war[https://www.gerritcodereview.com/download/gerrit-2.4.4.war]
 
-Bug Fixes
----------
+== Bug Fixes
 * Fix clone for modern Git clients
 +
 The security fix in 2.4.3 broke clone for recent Git clients,
diff --git a/ReleaseNotes/ReleaseNotes-2.4.txt b/ReleaseNotes/ReleaseNotes-2.4.txt
index 0e11550..1db4ba3 100644
--- a/ReleaseNotes/ReleaseNotes-2.4.txt
+++ b/ReleaseNotes/ReleaseNotes-2.4.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.4
-============================
+= Release notes for Gerrit 2.4
 
 Gerrit 2.4 is now available:
 
 link:https://www.gerritcodereview.com/download/gerrit-2.4.war[https://www.gerritcodereview.com/download/gerrit-2.4.war]
 
-Schema Change
--------------
+== Schema Change
 *WARNING:* This release contains schema changes.  To upgrade:
 ----
   java -jar gerrit.war init -d site_path
@@ -16,11 +14,9 @@
 a later 2.1.x version), and then to 2.4.x.  If you are upgrading from 2.2.x.x or
 newer, you may ignore this warning and upgrade directly to 2.4.x.
 
-New Features
-------------
+== New Features
 
-Security
-~~~~~~~~
+=== Security
 
 * Restrict visibility to arbitrary user dashboards
 +
@@ -42,8 +38,7 @@
 
 * Indicate that 'not found' may actually be a permission issue
 
-Web
-~~~
+=== Web
 
 * Add user preference to mark files reviewed automatically or manually
 +
@@ -82,14 +77,12 @@
 * Change 'Loading ...' to say 'Working ...' as, often, there is more going on
 than just loading a response.
 
-Performance
-~~~~~~~~~~~
+=== Performance
 
 * Asynchronously send email so it does not block the UI
 * Optimize queries for open/merged changes by project + branch
 
-Git
-~~~
+=== Git
 
 * Implement a multi-sub-task progress monitor for ReceiveCommits
 
@@ -116,8 +109,7 @@
 can be monitored for timeouts and cancelled, and have stalls reported
 to the user from the main thread.
 
-Search
-~~~~~~
+=== Search
 
 * Add the '--dependencies' option to the 'query' command.
 +
@@ -141,15 +133,13 @@
 With this change, we can fetch the comments on a patchset by sending a
 request to 'https://site/query?comments=true'
 
-Access Rights
-~~~~~~~~~~~~~
+=== Access Rights
 
 * Added the 'emailReviewers' as a global capability.
 +
 This replaces the 'emailOnlyAuthors' flag of account groups.
 
-Dev
-~~~
+=== Dev
 
 * issue 1272 Add scripts to create release notes from git log
 +
@@ -164,8 +154,7 @@
 
 * Add '--issues' and '--issue_numbers' options to the 'gitlog2asciidoc.py'
 
-Miscellaneous
-~~~~~~~~~~~~~
+=== Miscellaneous
 
 * Remove perl from 'commit-msg' hook
 +
@@ -174,8 +163,7 @@
 
 * updating contrib 'trivial_rebase.py' for 2.2.2.1
 
-Upgrades
---------
+== Upgrades
 
 * Updated to Guice 3.0.
 * Updated to gwtorm 1.4.
@@ -185,8 +173,7 @@
 The change also shrinks the built WAR from 38M to 23M
 by excluding the now unnecessary GWT server code.
 
-Bug Fixes
----------
+== Bug Fixes
 
 * issue 904 Users who starred a change should receive all the emails about a change.
 
@@ -226,11 +213,9 @@
 * Fix inconsistent behavior when replicating refs/meta/config
 * Fix duplicated results on status:open project:P branch:B
 
-Documentation
--------------
+== Documentation
 
-Access Rights
-~~~~~~~~~~~~~
+=== Access Rights
 * Capabilities introduced
 * Kill and priority capabilities
 * Administrate server capability
@@ -246,8 +231,7 @@
 * Project owner example role
 * Administrator example role
 
-Miscellaneous
-~~~~~~~~~~~~~
+=== Miscellaneous
 * User upload documentation: Replace changes
 * Add visible-to-all flag in the documentation for cmd-create-group
 * Add a contributing guideline for annotations
diff --git a/ReleaseNotes/ReleaseNotes-2.5.1.txt b/ReleaseNotes/ReleaseNotes-2.5.1.txt
index 6e6a481..c2982df 100644
--- a/ReleaseNotes/ReleaseNotes-2.5.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.5.1.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.5.1
-==============================
+= Release notes for Gerrit 2.5.1
 
 Gerrit 2.5.1 is now available:
 
@@ -10,8 +9,7 @@
 However, if upgrading from a version older than 2.5, follow the upgrade
 procedure in the 2.5 link:ReleaseNotes-2.5.html[Release Notes].
 
-Security Fixes
---------------
+== Security Fixes
 * Correctly identify Git-over-HTTP operations
 +
 Git operations over HTTP should be classified as using AccessPath.GIT
@@ -45,8 +43,7 @@
   project by definition has no parent)
 by pushing changes of the `project.config` file to `refs/meta/config`.
 
-Bug Fixes
----------
+== Bug Fixes
 * Fix RequestCleanup bug with Git over HTTP
 +
 Decide if a continuation is going to be used early, before the filter
diff --git a/ReleaseNotes/ReleaseNotes-2.5.2.txt b/ReleaseNotes/ReleaseNotes-2.5.2.txt
index cc436ac..9bedeac 100644
--- a/ReleaseNotes/ReleaseNotes-2.5.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.5.2.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.5.2
-==============================
+= Release notes for Gerrit 2.5.2
 
 Gerrit 2.5.2 is now available:
 
@@ -10,8 +9,7 @@
 However, if upgrading from any earlier version, follow the upgrade
 procedure in the 2.5 link:ReleaseNotes-2.5.html[Release Notes].
 
-Bug Fixes
----------
+== Bug Fixes
 * Improve performance of ReceiveCommits for repos with many refs
 +
 When validating the received commits all existing refs were added as
diff --git a/ReleaseNotes/ReleaseNotes-2.5.3.txt b/ReleaseNotes/ReleaseNotes-2.5.3.txt
index 8e9db0c..6448f1c 100644
--- a/ReleaseNotes/ReleaseNotes-2.5.3.txt
+++ b/ReleaseNotes/ReleaseNotes-2.5.3.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.5.3
-==============================
+= Release notes for Gerrit 2.5.3
 
 Gerrit 2.5.3 is now available:
 
@@ -10,8 +9,7 @@
 However, if upgrading from a version older than 2.5, follow the upgrade
 procedure in the 2.5 link:ReleaseNotes-2.5.html[Release Notes].
 
-Security Fixes
---------------
+== Security Fixes
 * Patch vulnerabilities in OpenID client library
 +
 Installations using OpenID for authentication were vulnerable to a
diff --git a/ReleaseNotes/ReleaseNotes-2.5.4.txt b/ReleaseNotes/ReleaseNotes-2.5.4.txt
index 4d51528..6ea93bb 100644
--- a/ReleaseNotes/ReleaseNotes-2.5.4.txt
+++ b/ReleaseNotes/ReleaseNotes-2.5.4.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.5.4
-==============================
+= Release notes for Gerrit 2.5.4
 
 Gerrit 2.5.4 is now available:
 
@@ -10,8 +9,7 @@
 However, if upgrading from a version older than 2.5, follow the upgrade
 procedure in the 2.5 link:ReleaseNotes-2.5.html[Release Notes].
 
-Bug Fixes
----------
+== Bug Fixes
 * Require preferred email to be verified
 +
 Some users were able to select a preferred email address that was
diff --git a/ReleaseNotes/ReleaseNotes-2.5.5.txt b/ReleaseNotes/ReleaseNotes-2.5.5.txt
index 146fd40..27dd2b6 100644
--- a/ReleaseNotes/ReleaseNotes-2.5.5.txt
+++ b/ReleaseNotes/ReleaseNotes-2.5.5.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.5.5
-==============================
+= Release notes for Gerrit 2.5.5
 
 There are no schema changes from link:ReleaseNotes-2.5.4.html[2.5.4].
 
 link:https://www.gerritcodereview.com/download/gerrit-2.5.5.war[https://www.gerritcodereview.com/download/gerrit-2.5.5.war]
 
-Bug Fixes
----------
+== Bug Fixes
 * Patch JGit security hole
 +
 The security hole may permit a modified Git client to gain access
diff --git a/ReleaseNotes/ReleaseNotes-2.5.6.txt b/ReleaseNotes/ReleaseNotes-2.5.6.txt
index b1e88f9..393eb93 100644
--- a/ReleaseNotes/ReleaseNotes-2.5.6.txt
+++ b/ReleaseNotes/ReleaseNotes-2.5.6.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.5.6
-==============================
+= Release notes for Gerrit 2.5.6
 
 There are no schema changes from link:ReleaseNotes-2.5.6.html[2.5.6].
 
 link:https://www.gerritcodereview.com/download/gerrit-2.5.6.war[https://www.gerritcodereview.com/download/gerrit-2.5.6.war]
 
-Bug Fixes
----------
+== Bug Fixes
 * Fix clone for modern Git clients
 +
 The security fix in 2.5.4 broke clone for recent Git clients,
diff --git a/ReleaseNotes/ReleaseNotes-2.5.txt b/ReleaseNotes/ReleaseNotes-2.5.txt
index 8519ee9..6bcb87a 100644
--- a/ReleaseNotes/ReleaseNotes-2.5.txt
+++ b/ReleaseNotes/ReleaseNotes-2.5.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.5
-============================
+= Release notes for Gerrit 2.5
 
 Gerrit 2.5 is now available:
 
@@ -10,8 +9,7 @@
 link:ReleaseNotes-2.4.2.html[Gerrit 2.4.2]. These bug fixes are *not*
 listed in these release notes.
 
-Schema Change
--------------
+== Schema Change
 *WARNING:* This release contains schema changes.  To upgrade:
 ----
   java -jar gerrit.war init -d site_path
@@ -21,8 +19,7 @@
 a later 2.1.x version), and then to 2.5.x.  If you are upgrading from 2.2.x.x or
 newer, you may ignore this warning and upgrade directly to 2.5.x.
 
-Warning on upgrade to schema version 68
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+=== Warning on upgrade to schema version 68
 
 The migration to schema version 68, may result in a warning, which can
 be ignored when running init in the interactive mode.
@@ -48,19 +45,16 @@
 message you can see that the migration failed because the index exists
 already (as in the example above), you can safely ignore this warning.
 
-Upgrade Warnings
-----------------
+== Upgrade Warnings
 
 [[replication]]
-Replication
-~~~~~~~~~~~
+=== Replication
 
 Gerrit 2.5 no longer includes replication support out of the box.
 Servers that reply upon `replication.config` to copy Git repository
 data to other locations must also install the replication plugin.
 
-Cache Configuration
-~~~~~~~~~~~~~~~~~~~
+=== Cache Configuration
 
 Disk caches are now backed by individual H2 databases, rather than
 Ehcache's own private format. Administrators are encouraged to clear
@@ -98,11 +92,9 @@
 updates are often made to the source without telling Gerrit to reload
 the cache.
 
-New Features
-------------
+== New Features
 
-Plugins
-~~~~~~~
+=== Plugins
 
 The Gerrit server functionality can be extended by installing plugins.
 Depending on how tightly the extension code is coupled with the Gerrit
@@ -261,8 +253,7 @@
 +
 This enables plugins to make use of servlet sessions.
 
-REST API
-~~~~~~~~
+=== REST API
 Gerrit now supports a REST like API available over HTTP. The API is
 suitable for automated tools to build upon, as well as supporting some
 ad-hoc scripting use cases.
@@ -302,11 +293,9 @@
 `site.enableDeprecatedQuery`] parameter in the Gerrit config file. This
 allows to enforce tools to move to the new API.
 
-Web
-~~~
+=== Web
 
-Change Screen
-^^^^^^^^^^^^^
+==== Change Screen
 
 * Display commit message in a box
 +
@@ -384,8 +373,7 @@
 
 * Use more gentle shade of red to highlight outdated dependencies
 
-Patch Screens
-^^^^^^^^^^^^^
+==== Patch Screens
 
 * New patch screen header
 +
@@ -447,8 +435,7 @@
 
 * Use download icons instead of the `Download` text links
 
-User Dashboard
-^^^^^^^^^^^^^^
+==== User Dashboard
 * Support for link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/user-custom-dashboards.html[custom
   dashboards]
 
@@ -472,8 +459,7 @@
 and the oldest stale at the bottom. This may help users to identify
 items to take immediate action on, as they appear closer to the top.
 
-Access Rights Screen
-^^^^^^^^^^^^^^^^^^^^
+==== Access Rights Screen
 
 * Display error if modifying access rights for a ref is forbidden
 +
@@ -512,8 +498,7 @@
 For project owners now also groups to which they are not a member are
 suggested when editing the access rights of the project.
 
-Other
-^^^^^
+==== Other
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=1592[issue 1592]:
   Ask user to login if change is not found
@@ -626,8 +611,7 @@
 The URL for the external system can be configured for the
 link:#custom-extension[`CUSTOM_EXTENSION`] auth type.
 
-Access Rights
-~~~~~~~~~~~~~
+=== Access Rights
 
 * Restrict rebasing of a change in the web UI to the change owner and
   the submitter
@@ -657,8 +641,7 @@
 `Anonymous users` were able to access the `refs/meta/config` branch
 which by default should only be accessible by the project owners.
 
-Search
-~~~~~~
+=== Search
 * Offer suggestions for the search operators in the search panel
 +
 There are many search operators and it's difficult to remember all of
@@ -678,8 +661,7 @@
 
 * `/query` API has been link:#query-deprecation[deprecated]
 
-SSH
-~~~
+=== SSH
 * link:http://code.google.com/p/gerrit/issues/detail?id=1095[issue 1095]
   link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/cmd-set-account.html[SSH command to manage
   accounts]
@@ -738,11 +720,9 @@
 command output a tab-separated table containing all available
 information about each group (though not its members).
 
-Documentation
-~~~~~~~~~~~~~
+=== Documentation
 
-Commands
-^^^^^^^^
+==== Commands
 
 * document for the link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/cmd-create-group.html[`create-group`]
   command that for unknown users an account is automatically created if
@@ -770,8 +750,7 @@
 
 * Fix and complete synopsis of commands
 
-Access Control
-^^^^^^^^^^^^^^
+==== Access Control
 
 * Clarify the ref format for
   link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/access-control.html#category_push_merge[`Push
@@ -785,8 +764,7 @@
   link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/access-control.html#capability_emailReviewers[
   `emailReviewers`] capability
 
-Error
-^^^^^
+==== Error
 * Improve documentation of link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/error-change-closed.html[
   `change closed` error]
 +
@@ -806,8 +784,7 @@
   a tag fails because the tagger is somebody else and the `Forge
   Committer` access right is not assigned.
 
-Dev
-^^^
+==== Dev
 
 * Update push URL in link:../SUBMITTING_PATCHES[SUBMITTING_PATCHES]
 +
@@ -840,8 +817,7 @@
 Document what it takes to make a Gerrit stable or stable-fix release,
 and how to release Gerrit subprojects.
 
-Other
-^^^^^
+==== Other
 * Add link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/prolog-cookbook.html[Cookbook for Prolog
   submit rules]
 +
@@ -884,8 +860,7 @@
 +
 Correct typos, spelling mistakes, and grammatical errors.
 
-Dev
-~~~
+=== Dev
 * Add link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-release.html#plugin-api[script for
   releasing plugin API jars]
 
@@ -948,8 +923,7 @@
 `all` profile so that the plugin modules are always built for release
 builds.
 
-Mail
-~~~~
+=== Mail
 
 * Add unified diff to newchange mail template
 +
@@ -997,8 +971,7 @@
 +
 Show the URL right away in the body.
 
-Miscellaneous
-~~~~~~~~~~~~~
+=== Miscellaneous
 * Back in-memory caches with Guava, disk caches with H2
 +
 Instead of using Ehcache for in-memory caches, use Guava. The Guava
@@ -1264,8 +1237,7 @@
 eventually go away when there is proper support for authentication
 plugins.
 
-Performance
-~~~~~~~~~~~
+=== Performance
 [[performance-issue-on-showing-group-list]]
 * Fix performance issues on showing the list of groups in the Gerrit
   WebUI
@@ -1399,8 +1371,7 @@
 front allows the database to send all requests to the backend as early
 as possible, allowing the network latency to overlap.
 
-Upgrades
---------
+== Upgrades
 * Update Gson to 2.1
 * Update GWT to 2.4.0
 * Update JGit to 2.0.0.201206130900-r.23-gb3dbf19
@@ -1416,11 +1387,9 @@
 inner table did not dynamically resize to handle a larger number
 of cached items, causing O(N) lookup performance for most objects.
 
-Bug Fixes
----------
+== Bug Fixes
 
-Security
-~~~~~~~~
+=== Security
 * Ensure that only administrators can change the global capabilities
 +
 Only Gerrit server administrators (members of the groups that have
@@ -1468,8 +1437,7 @@
 Hence this check is currently not done and these access rights in this
 case have simply no effect.
 
-Web
-~~~
+=== Web
 
 * Do not show "Session cookie not available" on sign in
 +
@@ -1625,8 +1593,7 @@
 and redirect to 'Admin' > 'Projects' to show the projects the caller
 has access to.
 
-Mail
-~~~~
+=== Mail
 
 * Fix: Rebase did not mail all reviewers
 
@@ -1653,8 +1620,7 @@
 `Reverted.vm` were not extracted during the initialization of a new
 site.
 
-SSH
-~~~
+=== SSH
 * Fix reject message if bypassing code review is not allowed
 +
 If a user is not allowed to bypass code review, but tries to push a
@@ -1726,8 +1692,7 @@
 non existing project name this was logged in the `error.log` but
 printing the error out to the user is sufficient.
 
-Authentication
-~~~~~~~~~~~~~~
+=== Authentication
 
 * Fix NPE in LdapRealm caused by non-LDAP users
 +
@@ -1748,8 +1713,7 @@
 is valid. When the URL is missing (e.g. because the provider is
 still broken) rely on the context path of the application instead.
 
-Replication
-~~~~~~~~~~~
+=== Replication
 
 * Fix inconsistent behavior when replicating `refs/meta/config`
 +
@@ -1766,8 +1730,7 @@
 The groupCache was being used before it was set in the class. Fix the
 ordering of the assignment.
 
-Approval Categories
-~~~~~~~~~~~~~~~~~~~
+=== Approval Categories
 
 * Make `NoBlock` and `NoOp` approval category functions work
 +
@@ -1811,8 +1774,7 @@
 collection came out null, which cannot be iterated. Make it always be
 an empty list.
 
-Other
-~~~~~
+=== Other
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=1554[issue 1554]:
   Fix cloning of new projects from slave servers
diff --git a/ReleaseNotes/ReleaseNotes-2.6.1.txt b/ReleaseNotes/ReleaseNotes-2.6.1.txt
index e43b077..94de483 100644
--- a/ReleaseNotes/ReleaseNotes-2.6.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.6.1.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.6.1
-==============================
+= Release notes for Gerrit 2.6.1
 
 There are no schema changes from link:ReleaseNotes-2.6.html[2.6].
 
 link:https://www.gerritcodereview.com/download/gerrit-2.6.1.war[https://www.gerritcodereview.com/download/gerrit-2.6.1.war]
 
-Bug Fixes
----------
+== Bug Fixes
 * Patch JGit security hole
 +
 The security hole may permit a modified Git client to gain access
diff --git a/ReleaseNotes/ReleaseNotes-2.6.txt b/ReleaseNotes/ReleaseNotes-2.6.txt
index dfd5d80..26b0b0e 100644
--- a/ReleaseNotes/ReleaseNotes-2.6.txt
+++ b/ReleaseNotes/ReleaseNotes-2.6.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.6
-============================
+= Release notes for Gerrit 2.6
 
 Gerrit 2.6 is now available:
 
@@ -12,8 +11,7 @@
 link:ReleaseNotes-2.5.4.html[Gerrit 2.5.4]. These bug fixes are *not*
 listed in these release notes.
 
-Schema Change
--------------
+== Schema Change
 *WARNING:* This release contains schema changes.  To upgrade:
 ----
   java -jar gerrit.war init -d site_path
@@ -23,8 +21,7 @@
 a later 2.1.x version), and then to 2.6.x.  If you are upgrading from 2.2.x.x or
 newer, you may ignore this warning and upgrade directly to 2.6.x.
 
-Reverse Proxy Configuration Changes
------------------------------------
+== Reverse Proxy Configuration Changes
 
 If you are running a reverse proxy in front of Gerrit (e.g. Apache or Nginx),
 make sure to check your configuration, especially if you are encountering
@@ -34,8 +31,7 @@
 
 Gerrit now requires passed URLs to be unchanged by the proxy.
 
-Release Highlights
-------------------
+== Release Highlights
 * 42x improvement on `git clone` and `git fetch`
 +
 Running link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/cmd-gc.html[
@@ -51,14 +47,11 @@
 labels] and link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/prolog-cookbook.html[
 Prolog rules].
 
-New Features
-------------
+== New Features
 
-Web UI
-~~~~~~
+=== Web UI
 
-Global
-^^^^^^
+==== Global
 
 * New Login Screens
 +
@@ -93,8 +86,7 @@
 
 * Add a link to the REST API documentation in the top menu.
 
-Search
-^^^^^^
+==== Search
 * Suggest projects, groups and users in search panel
 +
 Suggest projects, groups and users in the search panel as parameter for
@@ -107,8 +99,7 @@
 The values that are suggested for the search operators in the search
 panel are now only quoted if they contain a whitespace.
 
-Change Screens
-^^^^^^^^^^^^^^
+==== Change Screens
 
 * A change's commit message can be edited from the change screen.
 
@@ -182,8 +173,7 @@
 
 * Rename "Old Version History" to "Reference Version".
 
-Patch Screens
-^^^^^^^^^^^^^
+==== Patch Screens
 
 * Support for file comments
 +
@@ -205,8 +195,7 @@
 
 * Enable expanding skipped lines even if 'Syntax Coloring' is off.
 
-Project Screens
-^^^^^^^^^^^^^^^
+==== Project Screens
 
 * Support filtering of projects in the project list screen
 +
@@ -260,8 +249,7 @@
 Improve the error messages that are displayed in the WebUI if the
 creation of a branch fails due to invalid user input.
 
-Group Screens
-^^^^^^^^^^^^^
+==== Group Screens
 
 * Support filtering of groups in the group list screen
 +
@@ -276,8 +264,7 @@
 well-known system groups which are of type 'SYSTEM'. The system groups
 are so well-known that there is no need to display the type for them.
 
-Dashboard Screens
-^^^^^^^^^^^^^^^^^
+==== Dashboard Screens
 
 * Link dashboard title to a URL version of itself
 +
@@ -290,8 +277,7 @@
 
 * Increase time span for "Recently Closed" section in user dashboard to 4 weeks.
 
-Account Screens
-^^^^^^^^^^^^^^^
+==== Account Screens
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=1740[Issue 1740]:
   Display description how to generate SSH Key in SshPanel
@@ -302,16 +288,14 @@
 
 * Make the text for "Register" customizable
 
-Plugin Screens
-^^^^^^^^^^^^^^
+==== Plugin Screens
 
 * Show status for enabled plugins in the WebUI as 'Enabled'
 +
 Earlier no status was shown for enabled plugins, which was confusing to
 some users.
 
-REST API
-~~~~~~~~
+=== REST API
 
 * A big chunk of the Gerrit functionality is now available via the
   link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/rest-api.html[REST API].
@@ -443,8 +427,7 @@
 HTML thanks to Gson encoding HTML control characters using Unicode
 character escapes within JSON strings.
 
-Project Dashboards
-~~~~~~~~~~~~~~~~~~
+=== Project Dashboards
 * link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/user-dashboards.html#project-dashboards[
   Support for storing custom dashboards for projects]
 +
@@ -472,8 +455,7 @@
 The `foreach` parameter which will get appended to all the queries in
 the dashboard.
 
-Access Controls
-~~~~~~~~~~~~~~~
+=== Access Controls
 * Allow to overrule `BLOCK` permissions on the same project
 +
 It was impossible to block a permission for a group and allow the same
@@ -567,8 +549,7 @@
 Having the `Push` access right for `refs/meta/config` on the
 `All-Projects` project without being administrator has no effect.
 
-Hooks
-~~~~~
+=== Hooks
 * Change topic is passed to hooks as `--topic NAME`.
 * link:https://code.google.com/p/gerrit/issues/detail?id=1200[Issue 1200]:
 New `reviewer-added` hook and stream event when a reviewer is added.
@@ -581,8 +562,7 @@
 
 * Add `--is-draft` parameter to `comment-added` hook
 
-Git
-~~~
+=== Git
 * Add options to `refs/for/` magic branch syntax
 +
 Git doesn't want to modify the network protocol to support passing
@@ -629,8 +609,7 @@
 
 * Add `oldObjectId` and `newObjectId` to the `GitReferenceUpdatedListener.Update`
 
-SSH
-~~~
+=== SSH
 * New SSH command to http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/cmd-gc.html[
   run Git garbage collection]
 +
@@ -659,8 +638,7 @@
 * http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/cmd-test-submit-type.html[
   test-submit type] tests the Prolog submit type with a chosen change.
 
-Query
-~~~~~
+=== Query
 * Allow `{}` to be used for quoting in query expressions
 +
 This makes it a little easier to query for group names that contain
@@ -676,8 +654,7 @@
 * When a file is renamed the old file name is included in the Patch
   attribute
 
-Plugins
-~~~~~~~
+=== Plugins
 * Plugins can contribute Prolog facts/predicates from Java.
 * Plugins can prompt for parameters during `init` with `InitStep`.
 * Plugins can now contribute JavaScript to the web UI. UI plugins can
@@ -697,8 +674,7 @@
 delegate to the plugin servlet's magic handling for static files and
 documentation. Add JAR attributes to configure these prefixes.
 
-Prolog
-~~~~~~
+=== Prolog
 [[submit-type-from-prolog]]
 * link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/prolog-cookbook.html#HowToWriteSubmitType[
   Support controlling the submit type for changes from Prolog]
@@ -723,8 +699,7 @@
 
 * A new `max_with_block` predicate was added for more convenient usage
 
-Email
-~~~~~
+=== Email
 * Notify project watchers if draft change is published
 * Notify users mentioned in commit footer on draft publish
 * Add new notify type that allows watching of new patch sets
@@ -747,8 +722,7 @@
 which review updates should send email, and which categories of users
 on a change should get that email.
 
-Labels
-~~~~~~
+=== Labels
 * Approval categories stored in the database have been replaced with labels
   configured in `project.config`. Existing categories are migrated to
   `project.config` in `All-Projects` as part of the schema upgrade; no user
@@ -765,8 +739,7 @@
 or their own build system they can now trivially add the `Verified`
 category by pasting 5 lines into `project.config`.
 
-Dev
-~~~
+=== Dev
 
 * link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/dev-readme.html#debug-javascript[
   Support loading debug JavaScript]
@@ -815,8 +788,7 @@
 * "Become Any Account" can be used for accounts whose full name is an empty string.
 
 
-Performance
-~~~~~~~~~~~
+=== Performance
 * Bitmap Optimizations
 +
 On running the http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/cmd-gc.html[
@@ -904,8 +876,7 @@
 database traffic a cache for changes was introduced. This cache is
 disabled by default since it can mess up multi-server setups.
 
-Misc
-~~~~
+=== Misc
 * Add config parameter to make new groups by default visible to all
 +
 Add a new http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/config-gerrit.html#groups.newGroupsVisibleToAll[
@@ -1018,8 +989,7 @@
 
 * Show path to gerrit.war in command for upgrade schema
 
-Upgrades
-~~~~~~~~
+=== Upgrades
 * link:https://code.google.com/p/gerrit/issues/detail?id=1619[Issue 1619]:
 Embedded Jetty is now 8.1.7.v20120910.
 
@@ -1033,11 +1003,9 @@
 +
 Fixes some issues with IE9 and IE10.
 
-Bug Fixes
----------
+== Bug Fixes
 
-Web UI
-~~~~~~
+=== Web UI
 * link:https://code.google.com/p/gerrit/issues/detail?id=1662[Issue 1662]:
   Don't show error on ACL modification if empty permissions are added
 +
@@ -1234,8 +1202,7 @@
 Correctly keep patch set ordering after a new patch set is added via
 the Web UI.
 
-REST API
-~~~~~~~~
+=== REST API
 * Fix returning of 'Email Reviewers' capability via REST
 +
 The `/accounts/self/capabilities/` didn't return the 'Email Reviewers'
@@ -1249,8 +1216,7 @@
 * Provide a more descriptive error message for unauthenticated REST
   API access
 
-Git
-~~~
+=== Git
 * The wildcard `.` is now permitted in reference regex rules.
 
 * Checking if a change is mergeable no longer writes to the repository.
@@ -1356,8 +1322,7 @@
 "Only 0 of 0 new change refs created in xxx; aborting"
 could appear in the error log.
 
-SSH
-~~~
+=== SSH
 * `review --restore` allows a review score to be added on the restored change.
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=1721[Issue 1721]:
@@ -1379,8 +1344,7 @@
 
 * Fix setting account's full name via ssh.
 
-Query
-~~~~~
+=== Query
 * link:https://code.google.com/p/gerrit/issues/detail?id=1729[Issue 1729]:
   Fix query by 'label:Verified=0'
 
@@ -1389,8 +1353,7 @@
 
 * Fix query cost for "status:merged commit:c0ffee"
 
-Plugins
-~~~~~~~
+=== Plugins
 * Skip disabled plugins on rescan
 +
 In a background thread Gerrit periodically scans for new or changed
@@ -1450,8 +1413,7 @@
   Allow InternalUser (aka plugins) to see any internal group, and run
   plugin startup and shutdown as PluginUser.
 
-Email
-~~~~~
+=== Email
 * Merge failure emails are only sent once per day.
 * Unused macros are removed from the mail templates.
 * Unnecessary ellipses are no longer applied to email subjects.
@@ -1467,8 +1429,7 @@
 If a user is watching 'All Comments' on `All-Projects` this should
 apply to all projects.
 
-Misc
-~~~~
+=== Misc
 * Provide more descriptive message for NoSuchProjectException
 
 * On internal error due to receive timeout include the value of
@@ -1630,15 +1591,13 @@
 standard `Authorization` header unspecified and available for use in
 HTTP reverse proxies.
 
-Documentation
--------------
+== Documentation
 
 The link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/index.html[
 documentation index] is restructured to make it easier to use for different kinds of
 users.
 
-User Documentation
-~~~~~~~~~~~~~~~~~~
+=== User Documentation
 * Split link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/rest-api.html[
   REST API documentation] and have one page per top level resource
 
@@ -1750,8 +1709,7 @@
 * Fix external links in 2.0.21 and 2.0.24 release notes
 * Manual pages can be optionally created/installed for core gerrit ssh commands.
 
-Developer And Maintainer Documentation
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+=== Developer And Maintainer Documentation
 * Updated the link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/dev-eclipse.html#maven[
   Maven plugin installation instructions] for Eclipse 3.7 (Indigo).
 
diff --git a/ReleaseNotes/ReleaseNotes-2.7.txt b/ReleaseNotes/ReleaseNotes-2.7.txt
index 9782b08..0870cbf 100644
--- a/ReleaseNotes/ReleaseNotes-2.7.txt
+++ b/ReleaseNotes/ReleaseNotes-2.7.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.7
-============================
+= Release notes for Gerrit 2.7
 
 
 Gerrit 2.7 is now available:
@@ -10,8 +9,7 @@
 Gerrit 2.7 includes the bug fixes done with link:ReleaseNotes-2.6.1.html[Gerrit 2.6.1].
 These bug fixes are *not* listed in these release notes.
 
-Schema Change
--------------
+== Schema Change
 
 
 *WARNING:* This release contains schema changes.  To upgrade:
@@ -25,8 +23,7 @@
 
 
 
-Gerrit Trigger Plugin in Jenkins
---------------------------------
+== Gerrit Trigger Plugin in Jenkins
 
 
 *WARNING:* Upgrading to 2.7 may cause the Gerrit Trigger Plugin in Jenkins to
@@ -34,8 +31,7 @@
 below.
 
 
-Release Highlights
-------------------
+== Release Highlights
 
 
 * New `copyMaxScore` setting for labels.
@@ -46,12 +42,10 @@
 * Several new REST APIs.
 
 
-New Features
-------------
+== New Features
 
 
-General
-~~~~~~~
+=== General
 
 * New `copyMaxScore` setting for labels.
 +
@@ -105,12 +99,10 @@
 * Allow administrators to see all groups.
 
 
-Web UI
-~~~~~~
+=== Web UI
 
 
-Global
-^^^^^^
+==== Global
 
 * User avatars are displayed in more places in the Web UI.
 
@@ -120,8 +112,7 @@
 mouse over avatar images.
 
 
-Change Screens
-^^^^^^^^^^^^^^
+==== Change Screens
 
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=667[Issue 667]:
@@ -140,8 +131,7 @@
 change comments.
 
 
-Diff Screens
-^^^^^^^^^^^^
+==== Diff Screens
 
 * Show images in side-by-side and unified diffs.
 
@@ -150,15 +140,13 @@
 * Harmonize unified diff's styling of images with that of text.
 
 
-REST API
-~~~~~~~~
+=== REST API
 
 
 Several new link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.7/rest-api.html[
 REST API endpoints] are added.
 
-Accounts
-^^^^^^^^
+==== Accounts
 
 
 * link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.7/rest-api-accounts.html#get-diff-preferences[
@@ -168,8 +156,7 @@
 Set account diff preferences]
 
 
-Changes
-^^^^^^^
+==== Changes
 
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=1820[Issue 1820]:
@@ -181,16 +168,14 @@
 
 
 
-Projects
-^^^^^^^^
+==== Projects
 
 
 * link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.7/rest-api-projects.html#get-config[
 Get project configuration]
 
 
-ssh
-~~~
+=== ssh
 
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=1088[Issue 1088]:
@@ -198,11 +183,9 @@
 Kerberos authentication for ssh interaction].
 
 
-Bug Fixes
----------
+== Bug Fixes
 
-General
-~~~~~~~
+=== General
 
 * Postpone check for first account until adding an account.
 
@@ -240,8 +223,7 @@
 draft was already deleted.
 
 
-Web UI
-~~~~~~
+=== Web UI
 
 
 * Properly handle double-click on external group in GroupTable.
@@ -282,8 +264,7 @@
 Fix browser null-pointer exception when ChangeCache is incomplete.
 
 
-REST API
-~~~~~~~~
+=== REST API
 
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=1819[Issue 1819]:
@@ -294,37 +275,32 @@
 * Correct URL encoding in 'GroupInfo'.
 
 
-Email
-~~~~~
+=== Email
 
 * Log failure to access reviewer list for notification emails.
 
 * Log when appropriate if email delivery is skipped.
 
 
-ssh
-~~~
+=== ssh
 
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=2016[Issue 2016]:
 Flush caches after adding or deleting ssh keys via the `set-account` ssh command.
 
-Tools
-~~~~~
+=== Tools
 
 
 * The release build now builds for all browser configurations.
 
 
-Upgrades
---------
+== Upgrades
 
 * `gwtexpui` is now built in the gerrit tree rather than linking a separate module.
 
 
 
-Documentation
--------------
+== Documentation
 
 
 * Update the access control documentation to clarify how to set
diff --git a/ReleaseNotes/ReleaseNotes-2.8.1.txt b/ReleaseNotes/ReleaseNotes-2.8.1.txt
index 26414a1..5e32cf5 100644
--- a/ReleaseNotes/ReleaseNotes-2.8.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.8.1.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.8.1
-==============================
+= Release notes for Gerrit 2.8.1
 
 There are no schema changes from link:ReleaseNotes-2.8.html[2.8].
 
 link:https://www.gerritcodereview.com/download/gerrit-2.8.1.war[https://www.gerritcodereview.com/download/gerrit-2.8.1.war]
 
-Bug Fixes
----------
+== Bug Fixes
 * link:https://code.google.com/p/gerrit/issues/detail?id=2073[Issue 2073]:
 Changes that depend on outdated patch sets were missing in the related changes list.
 +
diff --git a/ReleaseNotes/ReleaseNotes-2.8.2.txt b/ReleaseNotes/ReleaseNotes-2.8.2.txt
index 926db02..99cb437 100644
--- a/ReleaseNotes/ReleaseNotes-2.8.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.8.2.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.8.2
-==============================
+= Release notes for Gerrit 2.8.2
 
 There are no schema changes from link:ReleaseNotes-2.8.1.html[2.8.1].
 
@@ -8,8 +7,7 @@
 https://www.gerritcodereview.com/download/gerrit-2.8.2.war]
 
 
-Lucene Index
-------------
+== Lucene Index
 
 * Support committing Lucene writes within a fixed interval.
 +
@@ -24,8 +22,7 @@
 indexes are committed.
 
 
-General
--------
+== General
 
 * Only add "cherry picked from" when cherry picking a merged change.
 +
@@ -142,8 +139,7 @@
 The joda time library was being unnecessarily packaged in the root of
 the gerrit.war file.
 
-Change Screen / Diff Screen
----------------------------
+== Change Screen / Diff Screen
 
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=2398[Issue 2398]:
@@ -231,8 +227,7 @@
 +
 Now, an error message will be displayed in the UI.
 
-ssh
----
+== ssh
 
 
 * Support for nio2 backend is removed.
@@ -263,8 +258,7 @@
 * link:https://code.google.com/p/gerrit/issues/detail?id=2515[Issue 2515]:
 Fix internal server error when updating an existing label with `gerrit review`.
 
-Replication Plugin
-------------------
+== Replication Plugin
 
 
 * Never replicate automerge-cache commits.
@@ -287,14 +281,12 @@
 * Update documentation to clarify replication of refs/meta/config when
 refspec is 'all refs'.
 
-Upgrades
---------
+== Upgrades
 
 
 * JGit is upgraded to 3.2.0.201312181205-r
 
-Documentation
--------------
+== Documentation
 
 
 * Add missing documentation of the secondary index configuration.
diff --git a/ReleaseNotes/ReleaseNotes-2.8.3.txt b/ReleaseNotes/ReleaseNotes-2.8.3.txt
index 2bd4aa7..f94dce0 100644
--- a/ReleaseNotes/ReleaseNotes-2.8.3.txt
+++ b/ReleaseNotes/ReleaseNotes-2.8.3.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.8.3
-==============================
+= Release notes for Gerrit 2.8.3
 
 There are no schema changes from link:ReleaseNotes-2.8.2.html[2.8.2].
 
@@ -8,8 +7,7 @@
 https://www.gerritcodereview.com/download/gerrit-2.8.3.war]
 
 
-Bug Fixes
----------
+== Bug Fixes
 
 * Fix for merging multiple changes with "Cherry Pick", "Merge Always" and
 "Merge If Necessary" strategies.
@@ -19,8 +17,7 @@
 them to actually land into the branch.
 
 
-Documentation
--------------
+== Documentation
 
 * Minor fixes in the
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8.3/dev-buck.html[
diff --git a/ReleaseNotes/ReleaseNotes-2.8.4.txt b/ReleaseNotes/ReleaseNotes-2.8.4.txt
index b80ac17..8aac71c 100644
--- a/ReleaseNotes/ReleaseNotes-2.8.4.txt
+++ b/ReleaseNotes/ReleaseNotes-2.8.4.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.8.4
-==============================
+= Release notes for Gerrit 2.8.4
 
 There are no schema changes from link:ReleaseNotes-2.8.3.html[2.8.3].
 
@@ -8,12 +7,10 @@
 https://www.gerritcodereview.com/download/gerrit-2.8.4.war]
 
 
-Bug Fixes
----------
+== Bug Fixes
 
 
-Secondary Index
-~~~~~~~~~~~~~~~
+=== Secondary Index
 
 
 * Disable `commitWithin` when running Reindex.
@@ -31,8 +28,7 @@
 `SubIndex.NrtFuture` objects were being added as listeners of `searchManager`
 and never released.
 
-Change Screen
-~~~~~~~~~~~~~
+=== Change Screen
 
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=2456[Issue 2456]:
@@ -84,8 +80,7 @@
 tooltip on the up arrow but did not show the tooltip on the left
 or right arrows.
 
-Plugins
-~~~~~~~
+=== Plugins
 
 
 * Fix ChangeListener auto-registered implementations.
@@ -98,8 +93,7 @@
 Plugins could be built, but not loaded, if they had any manifest entries
 that contained a dollar sign.
 
-Misc
-~~~~
+=== Misc
 
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=2564[Issue 2564],
@@ -156,8 +150,7 @@
 so stream-events consumers can properly detect who uploaded the
 rebased patch set.
 
-Documentation
-~~~~~~~~~~~~~
+=== Documentation
 
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=1273[Issue 1273]:
diff --git a/ReleaseNotes/ReleaseNotes-2.8.5.txt b/ReleaseNotes/ReleaseNotes-2.8.5.txt
index db18083..ae30530 100644
--- a/ReleaseNotes/ReleaseNotes-2.8.5.txt
+++ b/ReleaseNotes/ReleaseNotes-2.8.5.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.8.5
-==============================
+= Release notes for Gerrit 2.8.5
 
 Download:
 link:https://www.gerritcodereview.com/download/gerrit-2.8.5.war[
 https://www.gerritcodereview.com/download/gerrit-2.8.5.war]
 
-Schema Changes and Upgrades
----------------------------
+== Schema Changes and Upgrades
 
 
 * There are no schema changes from link:ReleaseNotes-2.8.4.html[2.8.4].
@@ -24,19 +22,16 @@
   java -jar gerrit.war init -d site_path
 ----
 
-Bug Fixes
----------
+== Bug Fixes
 
 
-Secondary Index
-~~~~~~~~~~~~~~~
+=== Secondary Index
 
 
 * Fix deadlocks on index shutdown.
 
 
-Change Screen
-~~~~~~~~~~~~~
+=== Change Screen
 
 
 * Only permit current patch set to edit the commit message.
@@ -68,8 +63,7 @@
 * Fix failure to load side-by-side diff due to "ISE EditIterator out of bounds"
 error.
 
-ssh
-~~~
+=== ssh
 
 * Upgrade SSHD to version 0.11.0.
 +
@@ -90,8 +84,7 @@
 link:https://issues.apache.org/jira/browse/SSHD-252[bug in SSHD].  That bug
 was fixed in SSHD version 0.10.0, so now we can re-enable nio2.
 
-Misc
-~~~~
+=== Misc
 
 
 * Keep old timestamps during data migration.
diff --git a/ReleaseNotes/ReleaseNotes-2.8.6.1.txt b/ReleaseNotes/ReleaseNotes-2.8.6.1.txt
index d1ed9e9..81e7297 100644
--- a/ReleaseNotes/ReleaseNotes-2.8.6.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.8.6.1.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.8.6.1
-================================
+= Release notes for Gerrit 2.8.6.1
 
 There are no schema changes from link:ReleaseNotes-2.8.6.html[2.8.6].
 
@@ -7,8 +6,7 @@
 link:https://www.gerritcodereview.com/download/gerrit-2.8.6.1.war[
 https://www.gerritcodereview.com/download/gerrit-2.8.6.1.war]
 
-Bug Fixes
----------
+== Bug Fixes
 
 * The fix in 2.8.6 for the merge queue race condition caused a regression
 in database transaction handling.
@@ -17,7 +15,6 @@
 database support.
 
 
-Updates
--------
+== Updates
 
 * gwtorm is updated to 1.7.3
diff --git a/ReleaseNotes/ReleaseNotes-2.8.6.txt b/ReleaseNotes/ReleaseNotes-2.8.6.txt
index ab79a20..a810ad0 100644
--- a/ReleaseNotes/ReleaseNotes-2.8.6.txt
+++ b/ReleaseNotes/ReleaseNotes-2.8.6.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.8.6
-==============================
+= Release notes for Gerrit 2.8.6
 
 There are no schema changes from link:ReleaseNotes-2.8.5.html[2.8.5].
 
@@ -10,8 +9,7 @@
 *Warning*: Support for MySQL's MyISAM storage engine is discontinued.
 Only transactional storage engines are supported.
 
-Bug Fixes
----------
+== Bug Fixes
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=2034[Issue 2034],
 link:https://code.google.com/p/gerrit/issues/detail?id=2383[Issue 2383],
@@ -40,8 +38,7 @@
 * Fix sporadic SSHD handshake failures
 (link:https://issues.apache.org/jira/browse/SSHD-330[SSHD-330]).
 
-Updates
--------
+== Updates
 
 * gwtorm is updated to 1.7.1
 * sshd is updated to 0.11.1-atlassian-1
diff --git a/ReleaseNotes/ReleaseNotes-2.8.txt b/ReleaseNotes/ReleaseNotes-2.8.txt
index 2d1dc7a..472f0dc 100644
--- a/ReleaseNotes/ReleaseNotes-2.8.txt
+++ b/ReleaseNotes/ReleaseNotes-2.8.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.8
-============================
+= Release notes for Gerrit 2.8
 
 
 Gerrit 2.8 is now available:
@@ -8,8 +7,7 @@
 https://www.gerritcodereview.com/download/gerrit-2.8.war]
 
 
-Schema Change
--------------
+== Schema Change
 
 
 *WARNING:* This release contains schema changes.  To upgrade:
@@ -46,8 +44,7 @@
 site initialization].
 
 
-Release Highlights
-------------------
+== Release Highlights
 
 
 * link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/intro-change-screen.html[
@@ -70,11 +67,9 @@
 * New core plugin: Download Commands.
 
 
-New Features
-------------
+== New Features
 
-Build
-~~~~~
+=== Build
 
 * Gerrit is now built with
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/dev-buck.html[
@@ -83,8 +78,7 @@
 * Documentation is now built with Buck and link:http://asciidoctor.org[Asciidoctor].
 
 
-Indexing and Search
-~~~~~~~~~~~~~~~~~~~
+=== Indexing and Search
 
 Gerrit can be configured to use a
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/config-gerrit.html#__a_id_index_a_section_index[
@@ -111,8 +105,7 @@
 reindex program] before restarting the Gerrit server.
 
 
-Configuration
-~~~~~~~~~~~~~
+=== Configuration
 
 * Project owners can define `receive.maxObjectSizeLimit` in the
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/config-gerrit.html#receive.maxObjectSizeLimit[
@@ -168,17 +161,14 @@
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/config-labels.html#httpd.label_copyAllScoresOnTrivialRebase[
 configured to copy scores forward to new patch sets for trivial rebases].
 
-Web UI
-~~~~~~
+=== Web UI
 
 
-Global
-^^^^^^
+==== Global
 
 * The change status is shown in a separate column on dashboards and search results.
 
-Change Screens
-^^^^^^^^^^^^^^
+==== Change Screens
 
 
 * New change screen with completely redesigned UI, using the REST API.
@@ -220,8 +210,7 @@
 are uploaded.
 
 
-REST API
-~~~~~~~~
+=== REST API
 
 * Several new link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api.html[
 REST API endpoints] are added.
@@ -230,15 +219,13 @@
 
 * REST views can handle 'HTTP 422 Unprocessable Entity' responses.
 
-Access Rights
-^^^^^^^^^^^^^
+==== Access Rights
 
 
 * link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-access.html#list-access[
 List access rights for project(s)]
 
-Accounts
-^^^^^^^^
+==== Accounts
 
 
 * link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-accounts.html#create-account[
@@ -310,8 +297,7 @@
 * link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-accounts.html#unstar-change[
 Unstar change]
 
-Changes
-^^^^^^^
+==== Changes
 
 
 * link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-changes.html#rebase-change[
@@ -345,8 +331,7 @@
 Get included in]
 
 
-Config
-^^^^^^
+==== Config
 
 * link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-config.html#get-capabilities[
 Get capabilities]
@@ -355,8 +340,7 @@
 Get version] (of the Gerrit server)
 
 
-Projects
-^^^^^^^^
+==== Projects
 
 
 * link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-projects.html#list-branches[
@@ -381,8 +365,7 @@
 Set configuration]
 
 
-Capabilities
-~~~~~~~~~~~~
+=== Capabilities
 
 
 New global capabilities are added.
@@ -403,8 +386,7 @@
 explicitly.
 
 
-Emails
-~~~~~~
+=== Emails
 
 * The `RebasedPatchSet` template is removed.  Email notifications for rebased
 changes are now sent with the `ReplacePatchSet` template.
@@ -413,12 +395,10 @@
 to, and links to the file(s) in which comments are made.
 
 
-Plugins
-~~~~~~~
+=== Plugins
 
 
-Global
-^^^^^^
+==== Global
 
 
 * Plugins may now contribute buttons to various parts of the UI using the
@@ -475,14 +455,12 @@
 
 
 
-Commit Message Length Checker
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+==== Commit Message Length Checker
 
 
 * Commits whose subject or body length exceeds the limit can be rejected.
 
-Replication
-^^^^^^^^^^^
+==== Replication
 
 * Automatically create missing repositories on the destination.
 +
@@ -528,8 +506,7 @@
 delay.
 
 
-ssh
-~~~
+=== ssh
 
 
 * The `commit-msg` hook installation command is now
@@ -557,8 +534,7 @@
 * The 'CHANGEID,PATCHSET' format for specifying a patch set in the `review` command
 is no longer considered to be a 'legacy' feature that will be removed in future.
 
-Daemon
-~~~~~~
+=== Daemon
 
 
 * Add `--init` option to Daemon to initialize site on daemon start.
@@ -567,12 +543,10 @@
 non-interactive (batch) mode.
 
 
-Bug Fixes
----------
+== Bug Fixes
 
 
-General
-~~~~~~~
+=== General
 
 
 * Use the parent change on the same branch for rebases.
@@ -619,8 +593,7 @@
 lots of time can be saved when pushing to complex Gits.
 
 
-Configuration
-~~~~~~~~~~~~~
+=== Configuration
 
 
 * Do not persist default project state in `project.config`.
@@ -644,12 +617,10 @@
 
 * Fix JdbcSQLException when numbers are read from cache.
 
-Web UI
-~~~~~~
+=== Web UI
 
 
-Global
-^^^^^^
+==== Global
 
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=1574[Issue 1574]:
@@ -669,8 +640,7 @@
 If a user voted '-1', and then another user voted '+1' for a label, the
 label was shown as a red '1' in the change list instead of red '-1'.
 
-Change Screens
-^^^^^^^^^^^^^^
+==== Change Screens
 
 
 * Default review comment visibility is changed to expand all recent.
@@ -694,8 +664,7 @@
 
 * Prevent duplicate permitted_labels from being shown in labels list.
 
-Diff Screens
-^^^^^^^^^^^^
+==== Diff Screens
 
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=1233[Issue 1233]:
@@ -708,8 +677,7 @@
 that comment was not shown if there was no code changed between
 the two patch sets
 
-Project Screens
-^^^^^^^^^^^^^^^
+==== Project Screens
 
 
 * Only enable the delete branch button when branches are selected.
@@ -717,16 +685,14 @@
 * Disable the delete branch button while branch deletion requests are
 still being processed.
 
-User Profile Screens
-^^^^^^^^^^^^^^^^^^^^
+==== User Profile Screens
 
 
 * The preferred email address field is shown as empty if the user has no
 preferred email address.
 
 
-REST API
-~~~~~~~~
+=== REST API
 
 
 * Support raw input also in POST requests.
@@ -735,8 +701,7 @@
 
 * Return all revisions when `o=ALL_REVISIONS` is set on `/changes/`.
 
-ssh
-~~~
+=== ssh
 
 
 * The `--force-message` option is removed from the
@@ -762,11 +727,9 @@
 * Improve the error message when rejecting upload for review to a read-only project.
 
 
-Plugins
-~~~~~~~
+=== Plugins
 
-Global
-^^^^^^
+==== Global
 
 * Better error message when a Javascript plugin cannot be loaded.
 
@@ -784,8 +747,7 @@
 * Make plugin servlet's context path authorization aware.
 
 
-Review Notes
-^^^^^^^^^^^^
+==== Review Notes
 
 * Do not try to create review notes for ref deletion events.
 
@@ -800,22 +762,19 @@
 
 * Correct documentation of the export command.
 
-Emails
-~~~~~~
+=== Emails
 
 * Email notifications are sent for new changes created via actions in the
 Web UI such as cherry-picking or reverting a change.
 
 
-Tools
-~~~~~
+=== Tools
 
 
 * git-exproll.sh: return non-zero on errors
 
 
-Documentation
--------------
+== Documentation
 
 
 * The link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/index.html[
@@ -832,8 +791,7 @@
 Documentation of the query operator is fixed.
 
 
-Upgrades
---------
+== Upgrades
 
 * Update JGit to 3.1.0.201310021548-r
 * Update gwtorm to 1.7
diff --git a/ReleaseNotes/ReleaseNotes-2.9.1.txt b/ReleaseNotes/ReleaseNotes-2.9.1.txt
index 3377df4..b584193 100644
--- a/ReleaseNotes/ReleaseNotes-2.9.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.9.1.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.9.1
-==============================
+= Release notes for Gerrit 2.9.1
 
 There are no schema changes from link:ReleaseNotes-2.9.html[2.9].
 
@@ -13,8 +12,7 @@
 startup failure described in
 link:https://code.google.com/p/gerrit/issues/detail?id=3084[Issue 3084].
 
-Bug Fixes
----------
+== Bug Fixes
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=2801[Issue 2801]:
 Set default for review SSH command to `notify=ALL`.
diff --git a/ReleaseNotes/ReleaseNotes-2.9.2.txt b/ReleaseNotes/ReleaseNotes-2.9.2.txt
index 4e5de01..ec5b77e 100644
--- a/ReleaseNotes/ReleaseNotes-2.9.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.9.2.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.9.2
-==============================
+= Release notes for Gerrit 2.9.2
 
 Download:
 link:https://www.gerritcodereview.com/download/gerrit-2.9.2.war[
 https://www.gerritcodereview.com/download/gerrit-2.9.2.war]
 
-Important Notes
----------------
+== Important Notes
 
 *WARNING:* There are no schema changes from
 link:ReleaseNotes-2.9.1.html[2.9.1], but when upgrading from an existing site
@@ -28,11 +26,9 @@
 startup failure described in
 link:https://code.google.com/p/gerrit/issues/detail?id=3084[Issue 3084].
 
-Bug Fixes
----------
+== Bug Fixes
 
-ssh
-~~~
+=== ssh
 
 * Update SSHD to 0.13.0.
 +
@@ -41,8 +37,7 @@
 +
 Also update SSHD Mina to 2.0.8 and Bouncycastle to 1.51.
 
-Database
-~~~~~~~~
+=== Database
 
 * Update gwtorm to 1.14.
 +
@@ -54,8 +49,7 @@
 were initialized with Gerrit version 2.6 or later, the primary key column
 order will be fixed during initialization when upgrading to 2.9.2.
 
-Secondary Index
-~~~~~~~~~~~~~~~
+=== Secondary Index
 
 * Fix "400 cannot create query for index" error in "Conflicts With" list.
 +
@@ -79,8 +73,7 @@
 the database. If a user clicks on search result from a stale change, they will
 get a 404 page and the change will be removed from the index.
 
-Change Screen
-~~~~~~~~~~~~~
+=== Change Screen
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=2964[Issue 2964]:
 Fix comment box font colors of dark CodeMirror themes.
@@ -107,8 +100,7 @@
 
 * Remove 'send email' checkbox from reply box on change screen.
 
-Plugins
-~~~~~~~
+=== Plugins
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=543[Issue 543]
 Replication plugin: Prevent creating repos on extra servers.
@@ -120,8 +112,7 @@
 By ensuring the authGroup can see the project first, the repository is
 not created if it's not needed.
 
-Security
-~~~~~~~~
+=== Security
 
 * Do not throw away bytes from the CNSPRG when generating HTTP passwords.
 +
@@ -141,8 +132,7 @@
 with BLOCK or DENY action were considered as project owners.
 
 
-Miscellaneous Fixes
-~~~~~~~~~~~~~~~~~~~
+=== Miscellaneous Fixes
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=2911[Issue 2911]:
 Fix Null Pointer Exception after a MergeValidationListener throws
diff --git a/ReleaseNotes/ReleaseNotes-2.9.3.txt b/ReleaseNotes/ReleaseNotes-2.9.3.txt
index e6c8573..1b732cb 100644
--- a/ReleaseNotes/ReleaseNotes-2.9.3.txt
+++ b/ReleaseNotes/ReleaseNotes-2.9.3.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.9.3
-==============================
+= Release notes for Gerrit 2.9.3
 
 Download:
 link:https://www.gerritcodereview.com/download/gerrit-2.9.3.war[
 https://www.gerritcodereview.com/download/gerrit-2.9.3.war]
 
-Important Notes
----------------
+== Important Notes
 
 *WARNING:* There are no schema changes from
 link:ReleaseNotes-2.9.2.html[2.9.2], but when upgrading from an existing site
@@ -28,8 +26,7 @@
 startup failure described in
 link:https://code.google.com/p/gerrit/issues/detail?id=3084[Issue 3084].
 
-Bug Fixes
----------
+== Bug Fixes
 
 *Downgrade SSHD to 0.9.0-4-g5967cfd*
 
diff --git a/ReleaseNotes/ReleaseNotes-2.9.4.txt b/ReleaseNotes/ReleaseNotes-2.9.4.txt
index 5063489..e2ad6ac 100644
--- a/ReleaseNotes/ReleaseNotes-2.9.4.txt
+++ b/ReleaseNotes/ReleaseNotes-2.9.4.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.9.4
-==============================
+= Release notes for Gerrit 2.9.4
 
 Download:
 link:https://www.gerritcodereview.com/download/gerrit-2.9.4.war[
 https://www.gerritcodereview.com/download/gerrit-2.9.4.war]
 
-Important Notes
----------------
+== Important Notes
 
 *WARNING:* There are no schema changes from
 link:ReleaseNotes-2.9.3.html[2.9.3], but when upgrading from an existing site
@@ -28,8 +26,7 @@
 startup failure described in
 link:https://code.google.com/p/gerrit/issues/detail?id=3084[Issue 3084].
 
-Bug Fixes
----------
+== Bug Fixes
 
 * Update JGit to 3.4.2.201412180340-r
 +
diff --git a/ReleaseNotes/ReleaseNotes-2.9.txt b/ReleaseNotes/ReleaseNotes-2.9.txt
index 3387f98..c026914 100644
--- a/ReleaseNotes/ReleaseNotes-2.9.txt
+++ b/ReleaseNotes/ReleaseNotes-2.9.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.9
-============================
+= Release notes for Gerrit 2.9
 
 
 Gerrit 2.9 is now available:
@@ -20,8 +19,7 @@
 link:ReleaseNotes-2.8.6.1.html[Gerrit 2.8.6.1].
 These bug fixes are *not* listed in these release notes.
 
-Important Notes
----------------
+== Important Notes
 
 
 *WARNING:* This release contains schema changes.  To upgrade:
@@ -98,8 +96,7 @@
 the site's `plugins` folder.
 
 
-Release Highlights
-------------------
+== Release Highlights
 
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=2065[Issue 2065]:
@@ -116,22 +113,18 @@
 to the old change screen.
 
 
-New Features
-------------
+== New Features
 
 
-Web UI
-~~~~~~
+=== Web UI
 
 
-Global
-^^^^^^
+==== Global
 
 * Project links by default link to the project dashboard.
 
 
-New Change Screen
-^^^^^^^^^^^^^^^^^
+==== New Change Screen
 
 
 * The new change screen is now the default change screen.
@@ -197,8 +190,7 @@
 New copy-to-clipboard button for commit ID.
 
 
-New Side-by-Side Diff Screen
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+==== New Side-by-Side Diff Screen
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=348[Issue 348]:
 The lines of a patch file are linkable.
@@ -220,8 +212,7 @@
 The full file path is shown.
 
 
-Change List / Dashboards
-^^^^^^^^^^^^^^^^^^^^^^^^
+==== Change List / Dashboards
 
 * The `Status` column shows `Merge Conflict` for changes that are not
 mergeable.
@@ -240,8 +231,7 @@
 without the `limit` operator.
 
 
-Project Screens
-^^^^^^^^^^^^^^^
+==== Project Screens
 
 * The general project screen provides a copyable clone command that
 automatically installs the `commit-msg` hook.
@@ -263,15 +253,13 @@
 Run Garbage Collection] global capability.
 
 
-User Preferences
-^^^^^^^^^^^^^^^^
+==== User Preferences
 
 * Users can choose the UK date format to render dates and timestamps in
 the UI.
 
 
-Secondary Index
-~~~~~~~~~~~~~~~
+=== Secondary Index
 
 * Support for query via the SQL index is removed. The usage of
 a secondary index is now mandatory.
@@ -280,8 +268,7 @@
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.9/pgm-reindex.html[
 reindex] program.
 
-ssh
-~~~
+=== ssh
 
 * New `--notify` option on the
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.9/cmd-review.html[
@@ -305,12 +292,10 @@
 New link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.9/cmd-create-branch.html[
 create-branch] command.
 
-REST API
-~~~~~~~~
+=== REST API
 
 
-Changes
-^^^^^^^
+==== Changes
 
 
 [[sortkey-deprecation]]
@@ -325,22 +310,19 @@
 Queries with sortkeys are still supported against old index versions, to enable
 online reindexing while clients have an older JS version.
 
-Projects
-^^^^^^^^
+==== Projects
 
 
 * link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.9/rest-api-projects.html#get-content[
 Get content of a file from HEAD of a branch].
 
-Documentation
-^^^^^^^^^^^^^
+==== Documentation
 
 
 * link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.9/rest-api-documentation.html#search-documentation.html[
 Search documentation].
 
-Access Rights
-~~~~~~~~~~~~~
+=== Access Rights
 
 
 * New link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.9/access-control.html#capability_viewAllAccounts[
@@ -374,8 +356,7 @@
 Gerrit but not in LDAP are authenticated with their HTTP password from
 the Gerrit database.
 
-Search
-~~~~~~
+=== Search
 
 * New link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.9/user-search.html#mergeable[
 is:mergeable] search operator.
@@ -411,8 +392,7 @@
 ** `p` = `project`
 ** `f` = `file`
 
-Daemon
-~~~~~~
+=== Daemon
 
 
 * link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.9/dev-inspector.html[
@@ -421,8 +401,7 @@
 New `-s` option is added to the Daemon to start an interactive Jython shell for inspection and
 troubleshooting of live data of the Gerrit instance.
 
-Documentation
-~~~~~~~~~~~~~
+=== Documentation
 
 
 * The documentation is now
@@ -442,8 +421,7 @@
 Newly structured documentation index].
 
 
-Configuration
-~~~~~~~~~~~~~
+=== Configuration
 
 * New init step for installing the `Verified` label.
 
@@ -463,8 +441,7 @@
 Allow the text of the "Report Bug" link to be configured.
 
 
-Misc
-~~~~
+=== Misc
 
 * The removal of reviewers and their votes is recorded as a change
 message.
@@ -478,8 +455,7 @@
 * Stable CSS class names.
 
 
-Plugins
-~~~~~~~
+=== Plugins
 
 
 * Plugin API to invoke the REST API.
@@ -499,8 +475,7 @@
 Remote plugin administration is by default disabled].
 
 
-Extension Points
-^^^^^^^^^^^^^^^^
+==== Extension Points
 
 
 * Extension point to provide a "Message Of The Day".
@@ -531,8 +506,7 @@
 DataSource Interception].
 
 
-JavaScript Plugins
-^^^^^^^^^^^^^^^^^^
+==== JavaScript Plugins
 
 
 * link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.9/js-api.html#self_on[
@@ -545,19 +519,16 @@
 insert arbitrary HTML fragments from plugins.
 
 
-Bug Fixes
----------
+== Bug Fixes
 
 
-Access Rights
-~~~~~~~~~~~~~
+=== Access Rights
 
 
 * Fix possibility to overcome BLOCK permissions.
 
 
-Web UI
-~~~~~~
+=== Web UI
 
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=2652[Issue 2652]:
@@ -622,8 +593,7 @@
 Fix copying from copyable label in Safari.
 
 
-Secondary Index
-~~~~~~~~~~~~~~~
+=== Secondary Index
 
 * Fix Online Reindexing.
 
@@ -640,16 +610,14 @@
 Reindex change after updating commit message.
 
 
-REST
-~~~~
+=== REST
 
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=2568[Issue 2568]:
 Update description file during `PUT /projects/{name}/config`.
 
 
-SSH
-~~~
+=== SSH
 
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=2516[Issue 2516]:
@@ -659,8 +627,7 @@
 Clarify for review command when `--verified` can be used.
 
 
-Plugins
-~~~~~~~
+=== Plugins
 
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=2551[Issue 2551]:
@@ -670,16 +637,14 @@
 Respect servlet context path in URL for top menu items.
 
 
-Other
-~~~~~
+=== Other
 
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=2382[Issue 2382]:
 Clean left over data migration after removal of TrackingIds table.
 
 
-Upgrades
---------
+== Upgrades
 
 * Update JGit to 3.4.0.201405051725-m7
 +
@@ -704,11 +669,9 @@
 * Update GWT to 2.6.0
 
 
-Plugins
--------
+== Plugins
 
-Replication
-~~~~~~~~~~~
+=== Replication
 
 * Default push refSpec is changed to `refs/*:refs/*` (non-forced push).
 +
@@ -728,8 +691,7 @@
 * Configuration changes can be detected and replication is
 automatically restarted.
 
-Issue Tracker System plugins
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+=== Issue Tracker System plugins
 
 *WARNING:* The `hooks-*` plugins (`plugins/hooks-bugzilla`,
 `plugins/hooks-jira` and `plugins/hooks-rtc`) are deprecated with
diff --git a/ReleaseNotes/asciidoc.conf b/ReleaseNotes/asciidoc.conf
deleted file mode 100644
index 527f58f..0000000
--- a/ReleaseNotes/asciidoc.conf
+++ /dev/null
@@ -1,19 +0,0 @@
-[attributes]
-asterisk=&#42;
-plus=&#43;
-caret=&#94;
-startsb=&#91;
-endsb=&#93;
-tilde=&#126;
-max-width=55em
-
-[specialsections]
-GERRIT=gerrituplink
-
-[gerrituplink]
-<hr style="
-  height: 2px;
-  color: silver;
-  margin-top: 1.2em;
-  margin-bottom: 0.5em;
-">
diff --git a/ReleaseNotes/config.defs b/ReleaseNotes/config.defs
new file mode 100644
index 0000000..86b7603
--- /dev/null
+++ b/ReleaseNotes/config.defs
@@ -0,0 +1,14 @@
+def release_notes_attributes():
+  return [
+    'toc',
+    'newline="\\n"',
+    'asterisk="&#42;"',
+    'plus="&#43;"',
+    'caret="&#94;"',
+    'startsb="&#91;"',
+    'endsb="&#93;"',
+    'tilde="&#126;"',
+    'last-update-label!',
+    'stylesheet=DEFAULT',
+    'linkcss=true',
+  ]
diff --git a/ReleaseNotes/index.txt b/ReleaseNotes/index.txt
index 4cab151..49491ac 100644
--- a/ReleaseNotes/index.txt
+++ b/ReleaseNotes/index.txt
@@ -1,21 +1,18 @@
-Gerrit Code Review - Release Notes
-==================================
+= Gerrit Code Review - Release Notes
 
-[[2_13]]
-Version 2.13.x
---------------
+[[s2_13]]
+== Version 2.13.x
 * link:ReleaseNotes-2.13.html[2.13]
 
-[[2_12]]
-Version 2.12.x
---------------
+[[s2_12]]
+== Version 2.12.x
 * link:ReleaseNotes-2.12.2.html[2.12.2]
 * link:ReleaseNotes-2.12.1.html[2.12.1]
 * link:ReleaseNotes-2.12.html[2.12]
 
-[[2_11]]
-Version 2.11.x
---------------
+[[s2_11]]
+== Version 2.11.x
+* link:ReleaseNotes-2.11.9.html[2.11.9]
 * link:ReleaseNotes-2.11.8.html[2.11.8]
 * link:ReleaseNotes-2.11.7.html[2.11.7]
 * link:ReleaseNotes-2.11.6.html[2.11.6]
@@ -26,9 +23,8 @@
 * link:ReleaseNotes-2.11.1.html[2.11.1]
 * link:ReleaseNotes-2.11.html[2.11]
 
-[[2_10]]
-Version 2.10.x
---------------
+[[s2_10]]
+== Version 2.10.x
 * 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]
@@ -39,18 +35,16 @@
 * link:ReleaseNotes-2.10.1.html[2.10.1]
 * link:ReleaseNotes-2.10.html[2.10]
 
-[[2_9]]
-Version 2.9.x
--------------
+[[s2_9]]
+== Version 2.9.x
 * link:ReleaseNotes-2.9.4.html[2.9.4]
 * link:ReleaseNotes-2.9.3.html[2.9.3]
 * link:ReleaseNotes-2.9.2.html[2.9.2]
 * link:ReleaseNotes-2.9.1.html[2.9.1]
 * link:ReleaseNotes-2.9.html[2.9]
 
-[[2_8]]
-Version 2.8.x
--------------
+[[s2_8]]
+== Version 2.8.x
 * link:ReleaseNotes-2.8.6.1.html[2.8.6.1]
 * link:ReleaseNotes-2.8.6.html[2.8.6]
 * link:ReleaseNotes-2.8.5.html[2.8.5]
@@ -60,20 +54,17 @@
 * link:ReleaseNotes-2.8.1.html[2.8.1]
 * link:ReleaseNotes-2.8.html[2.8]
 
-[[2_7]]
-Version 2.7.x
--------------
+[[s2_7]]
+== Version 2.7.x
 * link:ReleaseNotes-2.7.html[2.7]
 
-[[2_6]]
-Version 2.6.x
--------------
+[[s2_6]]
+== Version 2.6.x
 * link:ReleaseNotes-2.6.1.html[2.6.1]
 * link:ReleaseNotes-2.6.html[2.6]
 
-[[2_5]]
-Version 2.5.x
--------------
+[[s2_5]]
+== Version 2.5.x
 * link:ReleaseNotes-2.5.6.html[2.5.6]
 * link:ReleaseNotes-2.5.5.html[2.5.5]
 * link:ReleaseNotes-2.5.4.html[2.5.4]
@@ -82,33 +73,29 @@
 * link:ReleaseNotes-2.5.1.html[2.5.1]
 * link:ReleaseNotes-2.5.html[2.5]
 
-[[2_4]]
-Version 2.4.x
--------------
+[[s2_4]]
+== Version 2.4.x
 * link:ReleaseNotes-2.4.4.html[2.4.4]
 * link:ReleaseNotes-2.4.3.html[2.4.3]
 * link:ReleaseNotes-2.4.2.html[2.4.2]
 * link:ReleaseNotes-2.4.1.html[2.4.1]
 * link:ReleaseNotes-2.4.html[2.4]
 
-[[2_3]]
-Version 2.3.x
--------------
+[[s2_3]]
+== Version 2.3.x
 * link:ReleaseNotes-2.3.1.html[2.3.1]
 * link:ReleaseNotes-2.3.html[2.3]
 
-[[2_2]]
-Version 2.2.x
--------------
+[[s2_2]]
+== Version 2.2.x
 * link:ReleaseNotes-2.2.2.2.html[2.2.2.2]
 * link:ReleaseNotes-2.2.2.1.html[2.2.2.1]
 * link:ReleaseNotes-2.2.2.html[2.2.2]
 * link:ReleaseNotes-2.2.1.html[2.2.1]
 * link:ReleaseNotes-2.2.0.html[2.2.0]
 
-[[2_1]]
-Version 2.1.x
--------------
+[[s2_1]]
+== Version 2.1.x
 * link:ReleaseNotes-2.1.10.html[2.1.10]
 * link:ReleaseNotes-2.1.9.html[2.1.9]
 * link:ReleaseNotes-2.1.8.html[2.1.8]
@@ -129,9 +116,8 @@
 * link:ReleaseNotes-2.1.1.html[2.1.1]
 * link:ReleaseNotes-2.1.html[2.1]
 
-[[2_0]]
-Version 2.0.x
--------------
+[[s2_0]]
+== Version 2.0.x
 * link:ReleaseNotes-2.0.24.html[2.0.24.2]
 * link:ReleaseNotes-2.0.24.html[2.0.24.1]
 * link:ReleaseNotes-2.0.24.html[2.0.24]
diff --git a/gerrit-acceptance-framework/BUCK b/gerrit-acceptance-framework/BUCK
index d642942..ba68fa3 100644
--- a/gerrit-acceptance-framework/BUCK
+++ b/gerrit-acceptance-framework/BUCK
@@ -14,9 +14,9 @@
   '//lib/httpcomponents:httpclient',
   '//lib/httpcomponents:httpcore',
   '//lib/jetty:servlet',
+  '//lib/jgit/org.eclipse.jgit.junit:junit',
   '//lib/log:impl_log4j',
   '//lib/log:log4j',
-  '@jgit//org.eclipse.jgit.junit:junit',
 ]
 
 PROVIDED = [
@@ -30,9 +30,9 @@
   '//gerrit-server:server',
   '//lib:gson',
   '//lib:jsch',
+  '//lib/jgit/org.eclipse.jgit:jgit',
   '//lib/mina:sshd',
   '//lib:servlet-api-3_1',
-  '@jgit//org.eclipse.jgit:jgit',
 ]
 
 java_binary(
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 9cf1515..1b3b9d9 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -31,6 +31,7 @@
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.RevisionApi;
+import com.google.gerrit.extensions.api.groups.GroupInput;
 import com.google.gerrit.extensions.api.projects.BranchApi;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
@@ -197,13 +198,17 @@
   @Inject
   protected ChangeNoteUtil changeNoteUtil;
 
+  @Inject
+  protected ChangeResource.Factory changeResourceFactory;
+
   protected TestRepository<InMemoryRepository> testRepo;
   protected GerritServer server;
   protected TestAccount admin;
   protected TestAccount user;
-  protected RestSession adminSession;
-  protected RestSession userSession;
-  protected SshSession sshSession;
+  protected RestSession adminRestSession;
+  protected RestSession userRestSession;
+  protected SshSession adminSshSession;
+  protected SshSession userSshSession;
   protected ReviewDb db;
   protected Project.NameKey project;
 
@@ -300,14 +305,18 @@
     accountCache.evict(admin.getId());
     accountCache.evict(user.getId());
 
-    adminSession = new RestSession(server, admin);
-    userSession = new RestSession(server, user);
+    adminRestSession = new RestSession(server, admin);
+    userRestSession = new RestSession(server, user);
     initSsh(admin);
     db = reviewDbProvider.open();
-    Context ctx = newRequestContext(admin);
+    Context ctx = newRequestContext(user);
     atrScope.set(ctx);
-    sshSession = ctx.getSession();
-    sshSession.open();
+    userSshSession = ctx.getSession();
+    userSshSession.open();
+    ctx = newRequestContext(admin);
+    atrScope.set(ctx);
+    adminSshSession = ctx.getSession();
+    adminSshSession.open();
     resourcePrefix = UNSAFE_PROJECT_NAME.matcher(
         description.getClassName() + "_"
         + description.getMethodName() + "_").replaceAll("");
@@ -423,7 +432,8 @@
       repo.close();
     }
     db.close();
-    sshSession.close();
+    adminSshSession.close();
+    userSshSession.close();
     if (server != commonServer) {
       server.stop();
     }
@@ -490,6 +500,11 @@
     revision(r).submit();
   }
 
+  protected PushOneCommit.Result amendChangeAsDraft(String changeId)
+      throws Exception {
+    return amendChange(changeId, "refs/drafts/master");
+  }
+
   protected ChangeInfo info(String id)
       throws RestApiException {
     return gApi.changes().id(id).info();
@@ -516,7 +531,7 @@
   }
 
   private Context newRequestContext(TestAccount account) {
-    return atrScope.newContext(reviewDbProvider, new SshSession(server, admin),
+    return atrScope.newContext(reviewDbProvider, new SshSession(server, account),
         identifiedUserFactory.create(Providers.of(db), account.getId()));
   }
 
@@ -529,6 +544,16 @@
         atrScope.newContext(reviewDbProvider, null, anonymousUser.get()));
   }
 
+  protected Context disableDb() {
+    notesMigration.setFailOnLoad(true);
+    return atrScope.disableDb();
+  }
+
+  protected void enableDb(Context preDisableContext) {
+    notesMigration.setFailOnLoad(false);
+    atrScope.set(preDisableContext);
+  }
+
   protected static Gson newGson() {
     return OutputFormat.JSON_COMPACT.newGson();
   }
@@ -617,6 +642,10 @@
     projectCache.evict(cfg.getProject());
   }
 
+  protected void saveProjectConfig(ProjectConfig cfg) throws Exception {
+    saveProjectConfig(project, cfg);
+  }
+
   protected void grant(String permission, Project.NameKey project, String ref)
       throws RepositoryNotFoundException, IOException, ConfigInvalidException {
     grant(permission, project, ref, false);
@@ -716,6 +745,19 @@
     List<ChangeControl> ctls = changeFinder.find(
         changeId, atrScope.get().getUser());
     assertThat(ctls).hasSize(1);
-    return new ChangeResource(ctls.get(0));
+    return changeResourceFactory.create(ctls.get(0));
+  }
+
+  protected String createGroup(String name) throws Exception {
+    return createGroup(name, "Administrators");
+  }
+
+  protected String createGroup(String name, String owner) throws Exception {
+    name = name(name);
+    GroupInput in = new GroupInput();
+    in.name = name;
+    in.ownerId = owner;
+    gApi.groups().create(in);
+    return name;
   }
 }
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
index 34379a1..71d738c 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.acceptance;
 
-import com.google.common.collect.Maps;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
@@ -32,6 +31,7 @@
 import com.google.inject.Scope;
 import com.google.inject.util.Providers;
 
+import java.util.HashMap;
 import java.util.Map;
 
 /** Guice scopes for state during an Acceptance Test connection. */
@@ -44,7 +44,7 @@
 
   public static class Context implements RequestContext {
     private final RequestCleanup cleanup = new RequestCleanup();
-    private final Map<Key<?>, Object> map = Maps.newHashMap();
+    private final Map<Key<?>, Object> map = new HashMap<>();
     private final SchemaFactory<ReviewDb> schemaFactory;
     private final SshSession session;
     private final CurrentUser user;
@@ -182,6 +182,14 @@
     return old;
   }
 
+  public Context reopenDb() {
+    // Setting a new context with the same fields is enough to get the ReviewDb
+    // provider to reopen the database.
+    Context old = current.get();
+    return set(
+        new Context(old.schemaFactory, old.session, old.user, old.created));
+  }
+
   /** Returns exactly one instance per command executed. */
   static final Scope REQUEST = new Scope() {
     @Override
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
index ee0688b..3632502 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
@@ -22,13 +22,12 @@
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gerrit.reviewdb.client.AccountSshKey;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.AccountByEmailCache;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.VersionedAuthorizedKeys;
 import com.google.gerrit.server.ssh.SshKeyCache;
-import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -47,18 +46,23 @@
 public class AccountCreator {
   private final Map<String, TestAccount> accounts;
 
-  private SchemaFactory<ReviewDb> reviewDbProvider;
-  private GroupCache groupCache;
-  private SshKeyCache sshKeyCache;
-  private AccountCache accountCache;
-  private AccountByEmailCache byEmailCache;
+  private final SchemaFactory<ReviewDb> reviewDbProvider;
+  private final VersionedAuthorizedKeys.Accessor authorizedKeys;
+  private final GroupCache groupCache;
+  private final SshKeyCache sshKeyCache;
+  private final AccountCache accountCache;
+  private final AccountByEmailCache byEmailCache;
 
   @Inject
-  AccountCreator(SchemaFactory<ReviewDb> schema, GroupCache groupCache,
-      SshKeyCache sshKeyCache, AccountCache accountCache,
+  AccountCreator(SchemaFactory<ReviewDb> schema,
+      VersionedAuthorizedKeys.Accessor authorizedKeys,
+      GroupCache groupCache,
+      SshKeyCache sshKeyCache,
+      AccountCache accountCache,
       AccountByEmailCache byEmailCache) {
     accounts = new HashMap<>();
     reviewDbProvider = schema;
+    this.authorizedKeys = authorizedKeys;
     this.groupCache = groupCache;
     this.sshKeyCache = sshKeyCache;
     this.accountCache = accountCache;
@@ -66,17 +70,14 @@
   }
 
   public synchronized TestAccount create(String username, String email,
-      String fullName, String... groups)
-      throws OrmException, UnsupportedEncodingException, JSchException {
+      String fullName, String... groups) throws Exception {
     TestAccount account = accounts.get(username);
     if (account != null) {
       return account;
     }
     try (ReviewDb db = reviewDbProvider.open()) {
       Account.Id id = new Account.Id(db.nextAccountId());
-      KeyPair sshKey = genSshKey();
-      AccountSshKey key =
-          new AccountSshKey(new AccountSshKey.Id(id, 1), publicKey(sshKey, email));
+
       AccountExternalId extUser =
           new AccountExternalId(id, new AccountExternalId.Key(
               AccountExternalId.SCHEME_USERNAME, username));
@@ -95,8 +96,6 @@
       a.setPreferredEmail(email);
       db.accounts().insert(Collections.singleton(a));
 
-      db.accountSshKeys().insert(Collections.singleton(key));
-
       if (groups != null) {
         for (String n : groups) {
           AccountGroup.NameKey k = new AccountGroup.NameKey(n);
@@ -107,7 +106,10 @@
         }
       }
 
+      KeyPair sshKey = genSshKey();
+      authorizedKeys.addKey(id, publicKey(sshKey, email));
       sshKeyCache.evict(username);
+
       accountCache.evictByUsername(username);
       byEmailCache.evict(email);
 
@@ -118,35 +120,29 @@
     }
   }
 
-  public TestAccount create(String username, String group)
-      throws OrmException, UnsupportedEncodingException, JSchException {
+  public TestAccount create(String username, String group) throws Exception {
     return create(username, null, username, group);
   }
 
-  public TestAccount create(String username)
-      throws UnsupportedEncodingException, OrmException, JSchException {
+  public TestAccount create(String username) throws Exception {
     return create(username, null, username, (String[]) null);
   }
 
-  public TestAccount admin()
-      throws UnsupportedEncodingException, OrmException, JSchException {
+  public TestAccount admin() throws Exception {
     return create("admin", "admin@example.com", "Administrator",
       "Administrators");
   }
 
-  public TestAccount admin2()
-      throws UnsupportedEncodingException, OrmException, JSchException {
+  public TestAccount admin2() throws Exception {
     return create("admin2", "admin2@example.com", "Administrator2",
       "Administrators");
   }
 
-  public TestAccount user()
-      throws UnsupportedEncodingException, OrmException, JSchException {
+  public TestAccount user() throws Exception {
     return create("user", "user@example.com", "User");
   }
 
-  public TestAccount user2()
-      throws UnsupportedEncodingException, OrmException, JSchException {
+  public TestAccount user2() throws Exception {
     return create("user2", "user2@example.com", "User2");
   }
 
@@ -169,6 +165,6 @@
       throws UnsupportedEncodingException {
     ByteArrayOutputStream out = new ByteArrayOutputStream();
     sshKey.writePublicKey(out, comment);
-    return out.toString(US_ASCII.name());
+    return out.toString(US_ASCII.name()).trim();
   }
 }
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java
index 08977f0..c4e636b 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java
@@ -151,7 +151,7 @@
         public Void call() throws Exception {
           int rc = daemon.main(new String[] {
               "-d", site.getPath(),
-              "--headless", "--console-log", "--show-stack-trace"});
+              "--headless", "--console-log", "--show-stack-trace",});
           if (rc != 0) {
             System.err.println("Failed to start Gerrit daemon");
             serverStarted.reset();
@@ -172,7 +172,7 @@
     Init init = new Init();
     int rc = init.main(new String[] {
         "-d", tmp.getPath(), "--batch", "--no-auto-start",
-        "--skip-plugins"});
+        "--skip-plugins",});
     if (rc != 0) {
       throw new RuntimeException("Couldn't initialize site");
     }
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InProcessProtocol.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InProcessProtocol.java
index c16eed7..14188bd 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InProcessProtocol.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InProcessProtocol.java
@@ -259,11 +259,8 @@
         UploadPack up = new UploadPack(repo);
         up.setPackConfig(transferConfig.getPackConfig());
         up.setTimeout(transferConfig.getTimeout());
-
-        if (!ctl.allRefsAreVisible()) {
-          up.setAdvertiseRefsHook(new VisibleRefFilter(
-              tagCache, changeCache, repo, ctl, dbProvider.get(), true));
-        }
+        up.setAdvertiseRefsHook(new VisibleRefFilter(
+            tagCache, changeCache, repo, ctl, dbProvider.get(), true));
         List<PreUploadHook> hooks = Lists.newArrayList(preUploadHooks);
         hooks.add(uploadValidatorsFactory.create(
             ctl.getProject(), repo, "localhost-test"));
diff --git a/gerrit-acceptance-tests/BUCK b/gerrit-acceptance-tests/BUCK
index 9adaa01..d5d0b0d 100644
--- a/gerrit-acceptance-tests/BUCK
+++ b/gerrit-acceptance-tests/BUCK
@@ -33,8 +33,8 @@
     '//lib/guice:guice-assistedinject',
     '//lib/guice:guice-servlet',
     '//lib/log:api',
+    '//lib/jgit/org.eclipse.jgit:jgit',
     '//lib/mina:sshd',
-    '@jgit//org.eclipse.jgit:jgit',
   ],
   visibility = [
     '//gerrit-plugin-api/...',
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/UseGerritConfigAnnotationTest.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/UseGerritConfigAnnotationTest.java
index f4d156c..2f50480 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/UseGerritConfigAnnotationTest.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/UseGerritConfigAnnotationTest.java
@@ -29,15 +29,15 @@
   Config serverConfig;
 
   @Test
-  @GerritConfig(name="x.y", value="z")
+  @GerritConfig(name = "x.y", value = "z")
   public void testOne() {
     assertThat(serverConfig.getString("x", null, "y")).isEqualTo("z");
   }
 
   @Test
   @GerritConfigs({
-      @GerritConfig(name="x.y", value="z"),
-      @GerritConfig(name="a.b", value="c")
+      @GerritConfig(name = "x.y", value = "z"),
+      @GerritConfig(name = "a.b", value = "c")
   })
   public void testMultiple() {
     assertThat(serverConfig.getString("x", null, "y")).isEqualTo("z");
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
index 59f0150..53a47a3 100644
--- 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
@@ -23,20 +23,25 @@
 import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithExpiration;
 import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithSecondUserId;
 import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithoutExpiration;
+import static com.google.gerrit.server.StarredChangesUtil.DEFAULT_LABEL;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.base.Function;
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.io.BaseEncoding;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.AccountCreator;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.extensions.api.accounts.EmailInput;
+import com.google.gerrit.extensions.api.changes.StarsInput;
 import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.GpgKeyInfo;
 import com.google.gerrit.extensions.common.SshKeyInfo;
+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.ResourceNotFoundException;
@@ -173,11 +178,65 @@
     gApi.accounts()
         .self()
         .starChange(triplet);
-    assertThat(info(triplet).starred).isTrue();
+    ChangeInfo change = info(triplet);
+    assertThat(change.starred).isTrue();
+
     gApi.accounts()
         .self()
         .unstarChange(triplet);
-    assertThat(info(triplet).starred).isNull();
+    change = info(triplet);
+    assertThat(change.starred).isNull();
+  }
+
+  @Test
+  public void starUnstarChangeWithLabels() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String triplet = project.get() + "~master~" + r.getChangeId();
+    assertThat(gApi.accounts().self().getStars(triplet)).isEmpty();
+    assertThat(gApi.accounts().self().getStarredChanges()).isEmpty();
+
+    gApi.accounts().self().setStars(triplet,
+        new StarsInput(ImmutableSet.of(DEFAULT_LABEL, "red", "blue")));
+    ChangeInfo change = info(triplet);
+    assertThat(change.starred).isTrue();
+    assertThat(gApi.accounts().self().getStars(triplet))
+        .containsExactly("blue", "red", DEFAULT_LABEL).inOrder();
+    List<ChangeInfo> starredChanges =
+        gApi.accounts().self().getStarredChanges();
+    assertThat(starredChanges).hasSize(1);
+    ChangeInfo starredChange = starredChanges.get(0);
+    assertThat(starredChange._number).isEqualTo(r.getChange().getId().get());
+    assertThat(starredChange.starred).isTrue();
+
+    gApi.accounts().self().setStars(triplet,
+        new StarsInput(ImmutableSet.of("yellow"),
+            ImmutableSet.of(DEFAULT_LABEL, "blue")));
+    change = info(triplet);
+    assertThat(change.starred).isNull();
+    assertThat(gApi.accounts().self().getStars(triplet)).containsExactly(
+        "red", "yellow").inOrder();
+    starredChanges = gApi.accounts().self().getStarredChanges();
+    assertThat(starredChanges).hasSize(1);
+    starredChange = starredChanges.get(0);
+    assertThat(starredChange._number).isEqualTo(r.getChange().getId().get());
+    assertThat(starredChange.starred).isNull();
+
+    setApiUser(user);
+    exception.expect(AuthException.class);
+    exception.expectMessage("not allowed to get stars of another account");
+    gApi.accounts().id(Integer.toString((admin.id.get()))).getStars(triplet);
+  }
+
+  @Test
+  public void starWithInvalidLabels() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String triplet = project.get() + "~master~" + r.getChangeId();
+    exception.expect(BadRequestException.class);
+    exception.expectMessage(
+        "invalid labels: another invalid label, invalid label");
+    gApi.accounts().self().setStars(triplet,
+        new StarsInput(ImmutableSet.of(DEFAULT_LABEL, "invalid label", "blue",
+            "another invalid label")));
   }
 
   @Test
@@ -340,6 +399,7 @@
     // The test account should initially have exactly one ssh key
     List<SshKeyInfo> info = gApi.accounts().self().listSshKeys();
     assertThat(info).hasSize(1);
+    assertSequenceNumbers(info);
     SshKeyInfo key = info.get(0);
     String inital = AccountCreator.publicKey(admin.sshKey, admin.email);
     assertThat(key.sshPublicKey).isEqualTo(inital);
@@ -350,11 +410,35 @@
     gApi.accounts().self().addSshKey(newKey);
     info = gApi.accounts().self().listSshKeys();
     assertThat(info).hasSize(2);
+    assertSequenceNumbers(info);
 
-    // Add an existing key again
+    // Add an existing key (the request succeeds, but the key isn't added again)
     gApi.accounts().self().addSshKey(inital);
     info = gApi.accounts().self().listSshKeys();
+    assertThat(info).hasSize(2);
+    assertSequenceNumbers(info);
+
+    // Add another new key
+    String newKey2 = AccountCreator.publicKey(
+        AccountCreator.genSshKey(), admin.email);
+    gApi.accounts().self().addSshKey(newKey2);
+    info = gApi.accounts().self().listSshKeys();
     assertThat(info).hasSize(3);
+    assertSequenceNumbers(info);
+
+    // Delete second key
+    gApi.accounts().self().deleteSshKey(2);
+    info = gApi.accounts().self().listSshKeys();
+    assertThat(info).hasSize(2);
+    assertThat(info.get(0).seq).isEqualTo(1);
+    assertThat(info.get(1).seq).isEqualTo(3);
+  }
+
+  private void assertSequenceNumbers(List<SshKeyInfo> sshKeys) {
+    int seq = 1;
+    for (SshKeyInfo key : sshKeys) {
+      assertThat(key.seq).isEqualTo(seq++);
+    }
   }
 
   private PGPPublicKey getOnlyKeyFromStore(TestKey key) throws Exception {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/DiffPreferencesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/DiffPreferencesIT.java
index 14eaa90..f1b8a2d 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/DiffPreferencesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/DiffPreferencesIT.java
@@ -45,6 +45,7 @@
     assertThat(o.showTabs).isEqualTo(d.showTabs);
     assertThat(o.showWhitespaceErrors).isEqualTo(d.showWhitespaceErrors);
     assertThat(o.skipDeleted).isNull();
+    assertThat(o.skipUnchanged).isNull();
     assertThat(o.skipUncommented).isNull();
     assertThat(o.syntaxHighlighting).isEqualTo(d.syntaxHighlighting);
     assertThat(o.hideTopMenu).isNull();
@@ -76,6 +77,7 @@
     i.showTabs ^= true;
     i.showWhitespaceErrors ^= true;
     i.skipDeleted ^= true;
+    i.skipUnchanged ^= true;
     i.skipUncommented ^= true;
     i.syntaxHighlighting ^= true;
     i.hideTopMenu ^= true;
@@ -101,6 +103,7 @@
     assertThat(o.showTabs).isNull();
     assertThat(o.showWhitespaceErrors).isNull();
     assertThat(o.skipDeleted).isEqualTo(i.skipDeleted);
+    assertThat(o.skipUnchanged).isEqualTo(i.skipUnchanged);
     assertThat(o.skipUncommented).isEqualTo(i.skipUncommented);
     assertThat(o.syntaxHighlighting).isNull();
     assertThat(o.hideTopMenu).isEqualTo(i.hideTopMenu);
@@ -130,6 +133,7 @@
     assertThat(a.showTabs).isNull();
     assertThat(a.showWhitespaceErrors).isNull();
     assertThat(a.skipDeleted).isEqualTo(o.skipDeleted);
+    assertThat(a.skipUnchanged).isEqualTo(o.skipUnchanged);
     assertThat(a.skipUncommented).isEqualTo(o.skipUncommented);
     assertThat(a.syntaxHighlighting).isNull();
     assertThat(a.hideTopMenu).isEqualTo(o.hideTopMenu);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/EditPreferencesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/EditPreferencesIT.java
index fd810c4..dd5bcbb 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/EditPreferencesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/EditPreferencesIT.java
@@ -33,6 +33,7 @@
         .getEditPreferences();
 
     assertThat(out.lineLength).isEqualTo(100);
+    assertThat(out.indentUnit).isEqualTo(2);
     assertThat(out.tabSize).isEqualTo(8);
     assertThat(out.cursorBlinkRate).isEqualTo(0);
     assertThat(out.hideTopMenu).isNull();
@@ -42,11 +43,13 @@
     assertThat(out.hideLineNumbers).isNull();
     assertThat(out.matchBrackets).isTrue();
     assertThat(out.autoCloseBrackets).isNull();
+    assertThat(out.showBase).isNull();
     assertThat(out.theme).isEqualTo(Theme.DEFAULT);
     assertThat(out.keyMapType).isEqualTo(KeyMapType.DEFAULT);
 
     // change some default values
     out.lineLength = 80;
+    out.indentUnit = 4;
     out.tabSize = 4;
     out.cursorBlinkRate = 500;
     out.hideTopMenu = true;
@@ -56,6 +59,7 @@
     out.hideLineNumbers = true;
     out.matchBrackets = false;
     out.autoCloseBrackets = true;
+    out.showBase = true;
     out.theme = Theme.TWILIGHT;
     out.keyMapType = KeyMapType.EMACS;
 
@@ -80,6 +84,7 @@
   private void assertEditPreferences(EditPreferencesInfo out,
       EditPreferencesInfo in) throws Exception {
     assertThat(out.lineLength).isEqualTo(in.lineLength);
+    assertThat(out.indentUnit).isEqualTo(in.indentUnit);
     assertThat(out.tabSize).isEqualTo(in.tabSize);
     assertThat(out.cursorBlinkRate).isEqualTo(in.cursorBlinkRate);
     assertThat(out.hideTopMenu).isEqualTo(in.hideTopMenu);
@@ -89,6 +94,7 @@
     assertThat(out.hideLineNumbers).isEqualTo(in.hideLineNumbers);
     assertThat(out.matchBrackets).isNull();
     assertThat(out.autoCloseBrackets).isEqualTo(in.autoCloseBrackets);
+    assertThat(out.showBase).isEqualTo(in.showBase);
     assertThat(out.theme).isEqualTo(in.theme);
     assertThat(out.keyMapType).isEqualTo(in.keyMapType);
   }
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 cf866e1..dfa0336c 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
@@ -20,17 +20,20 @@
 import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
 import static com.google.gerrit.extensions.client.ReviewerState.CC;
 import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
+import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.CHANGE_OWNER;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.server.project.Util.blockLabel;
 import static com.google.gerrit.server.project.Util.category;
 import static com.google.gerrit.server.project.Util.value;
+import static java.util.concurrent.TimeUnit.SECONDS;
 
 import com.google.common.base.Function;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestProjectInput;
@@ -63,17 +66,19 @@
 import com.google.gerrit.server.config.AnonymousCowardNameProvider;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.notedb.ChangeNoteUtil;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.Util;
 import com.google.gerrit.testutil.FakeEmailSender.Message;
 import com.google.gerrit.testutil.NoteDbMode;
+import com.google.gerrit.testutil.TestTimeUtil;
 
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.junit.After;
+import org.junit.Before;
 import org.junit.Test;
 
 import java.sql.Timestamp;
@@ -87,6 +92,18 @@
 
 @NoHttpd
 public class ChangeIT extends AbstractDaemonTest {
+  private String systemTimeZone;
+
+  @Before
+  public void setTimeForTesting() {
+    systemTimeZone = System.setProperty("user.timezone", "US/Eastern");
+  }
+
+  @After
+  public void resetTime() {
+    TestTimeUtil.useSystemTime();
+    System.setProperty("user.timezone", systemTimeZone);
+  }
 
   @Test
   public void get() throws Exception {
@@ -502,6 +519,7 @@
 
   @Test
   public void addReviewer() throws Exception {
+    TestTimeUtil.resetWithClockStep(1, SECONDS);
     PushOneCommit.Result r = createChange();
     ChangeResource rsrc = parseResource(r);
     String oldETag = rsrc.getETag();
@@ -537,10 +555,10 @@
     assertThat(reviewers.iterator().next()._accountId)
         .isEqualTo(user.getId().get());
 
-    // Ensure ETag is updated but lastUpdatedOn isn't.
+    // Ensure ETag and lastUpdatedOn are updated.
     rsrc = parseResource(r);
     assertThat(rsrc.getETag()).isNotEqualTo(oldETag);
-    assertThat(rsrc.getChange().getLastUpdatedOn()).isEqualTo(oldTs);
+    assertThat(rsrc.getChange().getLastUpdatedOn()).isNotEqualTo(oldTs);
   }
 
   @Test
@@ -983,16 +1001,20 @@
     createChange();
 
     setApiUserAnonymous(); // Identified user may async get stars from DB.
-    atrScope.disableDb();
-    assertThat(gApi.changes().query()
-          .withQuery(
-            "project:{" + project.get() + "} (status:open OR status:closed)")
-          // Options should match defaults in AccountDashboardScreen.
-          .withOption(ListChangesOption.LABELS)
-          .withOption(ListChangesOption.DETAILED_ACCOUNTS)
-          .withOption(ListChangesOption.REVIEWED)
-          .get())
-        .hasSize(2);
+    AcceptanceTestRequestScope.Context ctx = disableDb();
+    try {
+      assertThat(gApi.changes().query()
+            .withQuery(
+              "project:{" + project.get() + "} (status:open OR status:closed)")
+            // Options should match defaults in AccountDashboardScreen.
+            .withOption(ListChangesOption.LABELS)
+            .withOption(ListChangesOption.DETAILED_ACCOUNTS)
+            .withOption(ListChangesOption.REVIEWED)
+            .get())
+          .hasSize(2);
+    } finally {
+      enableDb(ctx);
+    }
   }
 
   @Test
@@ -1068,17 +1090,16 @@
 
   @Test
   public void noteDbCommitsOnPatchSetCreation() throws Exception {
-    assume().that(notesMigration.enabled()).isTrue();
+    assume().that(notesMigration.readChanges()).isTrue();
 
     PushOneCommit.Result r = createChange();
     pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT,
         "b.txt", "4711", r.getChangeId()).to("refs/for/master").assertOkStatus();
     ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
-    try (Repository repo = repoManager.openMetadataRepository(project);
+    try (Repository repo = repoManager.openRepository(project);
         RevWalk rw = new RevWalk(repo)) {
       RevCommit commitPatchSetCreation = rw.parseCommit(
-          repo.exactRef(ChangeNoteUtil.changeRefName(new Change.Id(c._number)))
-              .getObjectId());
+          repo.exactRef(changeMetaRef(new Change.Id(c._number))).getObjectId());
 
       assertThat(commitPatchSetCreation.getShortMessage())
           .isEqualTo("Create patch set 2");
@@ -1153,6 +1174,6 @@
     List<ChangeControl> ctls = changeFinder.find(
         r.getChangeId(), atrScope.get().getUser());
     assertThat(ctls).hasSize(1);
-    return new ChangeResource(ctls.get(0));
+    return changeResourceFactory.create(ctls.get(0));
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java
index f1aa062..9c064c7 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -21,7 +21,6 @@
 import com.google.common.base.Function;
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
 import com.google.common.collect.Ordering;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
@@ -49,6 +48,7 @@
 
 import java.sql.Timestamp;
 import java.util.Arrays;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 
@@ -125,7 +125,7 @@
     String p = createGroup("parent");
     String g1 = createGroup("newGroup1");
     String g2 = createGroup("newGroup2");
-    List<String> groups = Lists.newLinkedList();
+    List<String> groups = new LinkedList<>();
     groups.add(g1);
     groups.add(g2);
     gApi.groups().id(p).addGroups(g1, g2);
@@ -560,19 +560,6 @@
     return groupCache.get(new AccountGroup.NameKey(name));
   }
 
-  private String createGroup(String name) throws Exception {
-    return createGroup(name, "Administrators");
-  }
-
-  private String createGroup(String name, String owner) throws Exception {
-    name = name(name);
-    GroupInput in = new GroupInput();
-    in.name = name;
-    in.ownerId = owner;
-    gApi.groups().create(in);
-    return name;
-  }
-
   private String createAccount(String name, String group) throws Exception {
     name = name(name);
     accounts.create(name, group);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java
index e1b544f..b34951d 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java
@@ -171,7 +171,7 @@
         modifier.modifyFile(editUtil.byChange(change).get(), FILE_NAME,
             RawInputUtil.create(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED);
     Optional<ChangeEdit> edit = editUtil.byChange(change);
-    adminSession.post(urlPublish()).assertNoContent();
+    adminRestSession.post(urlPublish()).assertNoContent();
     edit = editUtil.byChange(change);
     assertThat(edit.isPresent()).isFalse();
     PatchSet newCurrentPatchSet = getCurrentPatchSet(changeId);
@@ -189,7 +189,7 @@
         modifier.modifyFile(editUtil.byChange(change).get(), FILE_NAME,
             RawInputUtil.create(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED);
     Optional<ChangeEdit> edit = editUtil.byChange(change);
-    adminSession.delete(urlEdit()).assertNoContent();
+    adminRestSession.delete(urlEdit()).assertNoContent();
     edit = editUtil.byChange(change);
     assertThat(edit.isPresent()).isFalse();
   }
@@ -203,9 +203,9 @@
     assertThat(
         modifier.modifyFile(editUtil.byChange(change).get(), FILE_NAME,
             RawInputUtil.create(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED);
-    adminSession.post(urlPublish()).assertForbidden();
+    adminRestSession.post(urlPublish()).assertForbidden();
     setUseContributorAgreements(InheritableBoolean.FALSE);
-    adminSession.post(urlPublish()).assertNoContent();
+    adminRestSession.post(urlPublish()).assertNoContent();
   }
 
   @Test
@@ -242,7 +242,7 @@
     assertThat(edit.getBasePatchSet().getPatchSetId()).isEqualTo(
         current.getPatchSetId() - 1);
     Date beforeRebase = edit.getEditCommit().getCommitterIdent().getWhen();
-    adminSession.post(urlRebase()).assertNoContent();
+    adminRestSession.post(urlRebase()).assertNoContent();
     edit = editUtil.byChange(change).get();
     assertByteArray(fileUtil.getContent(projectCache.get(edit.getChange().getProject()),
         ObjectId.fromString(edit.getRevision().get()), FILE_NAME), CONTENT_NEW);
@@ -268,7 +268,7 @@
         pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, FILE_NAME,
             new String(CONTENT_NEW2), changeId2);
     push.to("refs/for/master").assertOkStatus();
-    adminSession.post(urlRebase()).assertConflict();
+    adminRestSession.post(urlRebase()).assertConflict();
   }
 
   @Test
@@ -362,13 +362,13 @@
 
   @Test
   public void updateMessageRest() throws Exception {
-    adminSession.get(urlEditMessage()).assertNotFound();
+    adminRestSession.get(urlEditMessage()).assertNotFound();
     EditMessage.Input in = new EditMessage.Input();
     in.message = String.format("New commit message\n\n" +
         CONTENT_NEW2_STR + "\n\nChange-Id: %s\n",
         change.getKey());
-    adminSession.put(urlEditMessage(), in).assertNoContent();
-    RestResponse r = adminSession.getJsonAccept(urlEditMessage());
+    adminRestSession.put(urlEditMessage(), in).assertNoContent();
+    RestResponse r = adminRestSession.getJsonAccept(urlEditMessage());
     r.assertOK();
     assertThat(readContentFromJson(r)).isEqualTo(in.message);
     Optional<ChangeEdit> edit = editUtil.byChange(change);
@@ -376,7 +376,7 @@
         .isEqualTo(in.message);
     in.message = String.format("New commit message2\n\nChange-Id: %s\n",
         change.getKey());
-    adminSession.put(urlEditMessage(), in).assertNoContent();
+    adminRestSession.put(urlEditMessage(), in).assertNoContent();
     edit = editUtil.byChange(change);
     assertThat(edit.get().getEditCommit().getFullMessage())
         .isEqualTo(in.message);
@@ -389,7 +389,7 @@
 
   @Test
   public void retrieveEdit() throws Exception {
-    adminSession.get(urlEdit()).assertNoContent();
+    adminRestSession.get(urlEdit()).assertNoContent();
     assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
     Optional<ChangeEdit> edit = editUtil.byChange(change);
     assertThat(modifier.modifyFile(edit.get(), FILE_NAME, RawInputUtil.create(CONTENT_NEW)))
@@ -402,7 +402,7 @@
     edit = editUtil.byChange(change);
     editUtil.delete(edit.get());
 
-    adminSession.get(urlEdit()).assertNoContent();
+    adminRestSession.get(urlEdit()).assertNoContent();
   }
 
   @Test
@@ -447,7 +447,7 @@
 
   @Test
   public void createEditByDeletingExistingFileRest() throws Exception {
-    adminSession.delete(urlEditFile()).assertNoContent();
+    adminRestSession.delete(urlEditFile()).assertNoContent();
     Optional<ChangeEdit> edit = editUtil.byChange(change);
     exception.expect(ResourceNotFoundException.class);
     fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
@@ -456,13 +456,13 @@
 
   @Test
   public void deletingNonExistingEditRest() throws Exception {
-    adminSession.delete(urlEdit()).assertNotFound();
+    adminRestSession.delete(urlEdit()).assertNotFound();
   }
 
   @Test
   public void deleteExistingFileRest() throws Exception {
     assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
-    adminSession.delete(urlEditFile()).assertNoContent();
+    adminRestSession.delete(urlEditFile()).assertNoContent();
     Optional<ChangeEdit> edit = editUtil.byChange(change);
     exception.expect(ResourceNotFoundException.class);
     fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
@@ -511,7 +511,7 @@
     Post.Input in = new Post.Input();
     in.oldPath = FILE_NAME;
     in.newPath = FILE_NAME3;
-    adminSession.post(urlEdit(), in).assertNoContent();
+    adminRestSession.post(urlEdit(), in).assertNoContent();
     Optional<ChangeEdit> edit = editUtil.byChange(change);
     assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
         ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME3), CONTENT_OLD);
@@ -524,7 +524,7 @@
   public void restoreDeletedFileInPatchSetRest() throws Exception {
     Post.Input in = new Post.Input();
     in.restorePath = FILE_NAME;
-    adminSession.post(urlEdit2(), in).assertNoContent();
+    adminRestSession.post(urlEdit2(), in).assertNoContent();
     Optional<ChangeEdit> edit = editUtil.byChange(change2);
     assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
         ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_OLD);
@@ -550,12 +550,12 @@
   public void createAndChangeEditInOneRequestRest() throws Exception {
     Put.Input in = new Put.Input();
     in.content = RawInputUtil.create(CONTENT_NEW);
-    adminSession.putRaw(urlEditFile(), in.content).assertNoContent();
+    adminRestSession.putRaw(urlEditFile(), in.content).assertNoContent();
     Optional<ChangeEdit> edit = editUtil.byChange(change);
     assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
         ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_NEW);
     in.content = RawInputUtil.create(CONTENT_NEW2);
-    adminSession.putRaw(urlEditFile(), in.content).assertNoContent();
+    adminRestSession.putRaw(urlEditFile(), in.content).assertNoContent();
     edit = editUtil.byChange(change);
     assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
         ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_NEW2);
@@ -566,7 +566,7 @@
     assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
     Put.Input in = new Put.Input();
     in.content = RawInputUtil.create(CONTENT_NEW);
-    adminSession.putRaw(urlEditFile(), in.content).assertNoContent();
+    adminRestSession.putRaw(urlEditFile(), in.content).assertNoContent();
     Optional<ChangeEdit> edit = editUtil.byChange(change);
     assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
         ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_NEW);
@@ -575,7 +575,7 @@
   @Test
   public void emptyPutRequest() throws Exception {
     assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
-    adminSession.put(urlEditFile()).assertNoContent();
+    adminRestSession.put(urlEditFile()).assertNoContent();
     Optional<ChangeEdit> edit = editUtil.byChange(change);
     assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
         ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), "".getBytes());
@@ -583,7 +583,7 @@
 
   @Test
   public void createEmptyEditRest() throws Exception {
-    adminSession.post(urlEdit()).assertNoContent();
+    adminRestSession.post(urlEdit()).assertNoContent();
     Optional<ChangeEdit> edit = editUtil.byChange(change);
     assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
         ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_OLD);
@@ -593,23 +593,28 @@
   public void getFileContentRest() throws Exception {
     Put.Input in = new Put.Input();
     in.content = RawInputUtil.create(CONTENT_NEW);
-    adminSession.putRaw(urlEditFile(), in.content).assertNoContent();
+    adminRestSession.putRaw(urlEditFile(), in.content).assertNoContent();
     Optional<ChangeEdit> edit = editUtil.byChange(change);
     assertThat(modifier.modifyFile(edit.get(), FILE_NAME, RawInputUtil.create(CONTENT_NEW2)))
         .isEqualTo(RefUpdate.Result.FORCED);
     edit = editUtil.byChange(change);
-    RestResponse r = adminSession.getJsonAccept(urlEditFile());
+    RestResponse r = adminRestSession.getJsonAccept(urlEditFile());
     r.assertOK();
     assertThat(readContentFromJson(r)).isEqualTo(
         StringUtils.newStringUtf8(CONTENT_NEW2));
+
+    r = adminRestSession.getJsonAccept(urlEditFile(true));
+    r.assertOK();
+    assertThat(readContentFromJson(r)).isEqualTo(
+        StringUtils.newStringUtf8(CONTENT_OLD));
   }
 
   @Test
   public void getFileNotFoundRest() throws Exception {
     assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
-    adminSession.delete(urlEditFile()).assertNoContent();
+    adminRestSession.delete(urlEditFile()).assertNoContent();
     Optional<ChangeEdit> edit = editUtil.byChange(change);
-    adminSession.get(urlEditFile()).assertNoContent();
+    adminRestSession.get(urlEditFile()).assertNoContent();
     exception.expect(ResourceNotFoundException.class);
     fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
         ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME);
@@ -724,12 +729,12 @@
         .isEqualTo(RefUpdate.Result.FORCED);
     edit = editUtil.byChange(change).get();
 
-    RestResponse r = adminSession.getJsonAccept(urlRevisionFiles(edit));
+    RestResponse r = adminRestSession.getJsonAccept(urlRevisionFiles(edit));
     Map<String, FileInfo> files = readContentFromJson(
         r, new TypeToken<Map<String, FileInfo>>() {});
     assertThat(files).containsKey(FILE_NAME);
 
-    r = adminSession.getJsonAccept(urlRevisionFiles());
+    r = adminRestSession.getJsonAccept(urlRevisionFiles());
     files = readContentFromJson(r, new TypeToken<Map<String, FileInfo>>() {});
     assertThat(files).containsKey(FILE_NAME);
   }
@@ -742,11 +747,11 @@
         .isEqualTo(RefUpdate.Result.FORCED);
     edit = editUtil.byChange(change).get();
 
-    RestResponse r = adminSession.getJsonAccept(urlDiff(edit));
+    RestResponse r = adminRestSession.getJsonAccept(urlDiff(edit));
     DiffInfo diff = readContentFromJson(r, DiffInfo.class);
     assertThat(diff.diffHeader.get(0)).contains(FILE_NAME);
 
-    r = adminSession.getJsonAccept(urlDiff());
+    r = adminRestSession.getJsonAccept(urlDiff());
     diff = readContentFromJson(r, DiffInfo.class);
     assertThat(diff.diffHeader.get(0)).contains(FILE_NAME);
   }
@@ -811,9 +816,14 @@
   }
 
   private String urlEditFile() {
+    return urlEditFile(false);
+  }
+
+  private String urlEditFile(boolean base) {
     return urlEdit()
         + "/"
-        + FILE_NAME;
+        + FILE_NAME
+        + (base ? "?base" : "");
   }
 
   private String urlGetFiles() {
@@ -866,7 +876,7 @@
   }
 
   private EditInfo toEditInfo(boolean files) throws Exception {
-    RestResponse r = adminSession.get(files ? urlGetFiles() : urlEdit());
+    RestResponse r = adminRestSession.get(files ? urlGetFiles() : urlEdit());
     return readContentFromJson(r, EditInfo.class);
   }
 
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 638d20e..e63b28ba 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
@@ -39,7 +39,6 @@
 import com.google.gerrit.extensions.common.LabelInfo;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Change;
-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.Util;
@@ -76,7 +75,7 @@
 
   @Before
   public void setUp() throws Exception {
-    sshUrl = sshSession.getUrl();
+    sshUrl = adminSshSession.getUrl();
     ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
     patchSetLock = Util.patchSetLock();
     cfg.getLabelSections().put(patchSetLock.getName(), patchSetLock);
@@ -88,12 +87,6 @@
     grant(Permission.LABEL + "Patch-Set-Lock", project, "refs/heads/*");
   }
 
-  private void saveProjectConfig(ProjectConfig cfg) throws Exception {
-    try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
-      cfg.commit(md);
-    }
-  }
-
   protected void selectProtocol(Protocol p) throws Exception {
     String url;
     switch (p) {
@@ -406,9 +399,8 @@
 
   @Test
   public void testPushForMasterWithHashtags() throws Exception {
-
-    // Hashtags currently only work when noteDB is enabled
-    assume().that(notesMigration.enabled()).isTrue();
+    // Hashtags only work when reading from NoteDB is enabled
+    assume().that(notesMigration.readChanges()).isTrue();
 
     // specify a single hashtag as option
     String hashtag1 = "tag1";
@@ -434,9 +426,8 @@
 
   @Test
   public void testPushForMasterWithMultipleHashtags() throws Exception {
-
-    // Hashtags currently only work when noteDB is enabled
-    assume().that(notesMigration.enabled()).isTrue();
+    // Hashtags only work when reading from NoteDB is enabled
+    assume().that(notesMigration.readChanges()).isTrue();
 
     // specify multiple hashtags as options
     String hashtag1 = "tag1";
@@ -465,8 +456,8 @@
 
   @Test
   public void testPushForMasterWithHashtagsNoteDbDisabled() throws Exception {
-    // push with hashtags should fail when noteDb is disabled
-    assume().that(notesMigration.enabled()).isFalse();
+    // Push with hashtags should fail when reading from NoteDb is disabled.
+    assume().that(notesMigration.readChanges()).isFalse();
     PushOneCommit.Result r = pushTo("refs/for/master%hashtag=tag1");
     r.assertErrorStatus("cannot add hashtags; noteDb is disabled");
   }
@@ -495,4 +486,23 @@
     r.assertErrorStatus(
         "not Signed-off-by author/committer/uploader in commit message footer");
   }
+
+  @Test
+  public void testCreateNewChangeForAllNotInTarget() throws Exception {
+    ProjectConfig config = projectCache.checkedGet(project).getConfig();
+    config.getProject().setCreateNewChangeForAllNotInTarget(InheritableBoolean.TRUE);
+    saveProjectConfig(project, config);
+
+    PushOneCommit push =
+        pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT,
+            "a.txt", "content");
+    PushOneCommit.Result r = push.to("refs/for/master");
+    r.assertOkStatus();
+
+    push =
+        pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT,
+            "b.txt", "anotherContent");
+    r = push.to("refs/for/master");
+    r.assertOkStatus();
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
index 24ddbf2..bb1a656 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
@@ -127,14 +127,13 @@
     ObjectId commitId = repo.git().fetch().setRemote("origin").call()
         .getAdvertisedRef("refs/heads/" + branch).getObjectId();
 
-    try (RevWalk rw = repo.getRevWalk()) {
-      RevCommit c = rw.parseCommit(commitId);
-      rw.parseBody(c.getTree());
+    RevWalk rw = repo.getRevWalk();
+    RevCommit c = rw.parseCommit(commitId);
+    rw.parseBody(c.getTree());
 
-      RevTree tree = c.getTree();
-      RevObject actualId = repo.get(tree, submodule);
+    RevTree tree = c.getTree();
+    RevObject actualId = repo.get(tree, submodule);
 
-      assertThat(actualId).isEqualTo(expectedId);
-    }
+    assertThat(actualId).isEqualTo(expectedId);
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
index e69a647..ccaf4ac 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
@@ -17,8 +17,11 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.gerrit.acceptance.GerritConfig;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.testutil.ConfigSuite;
 
 import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevTree;
@@ -26,7 +29,12 @@
 import org.eclipse.jgit.transport.RefSpec;
 import org.junit.Test;
 
+@NoHttpd
 public class SubmoduleSubscriptionsIT extends AbstractSubmoduleSubscription {
+  @ConfigSuite.Config
+  public static Config submitWholeTopicEnabled() {
+    return submitWholeTopicEnabledConfig();
+  }
 
   @Test
   @GerritConfig(name = "submodule.enableSuperProjectSubscriptions", value = "false")
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/VisibleRefFilterIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/VisibleRefFilterIT.java
index 176cf49..cfe04a2 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/VisibleRefFilterIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/VisibleRefFilterIT.java
@@ -18,6 +18,7 @@
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.common.data.AccessSection;
@@ -77,7 +78,6 @@
         .getGroupUUID();
     setUpPermissions();
     setUpChanges();
-    atrScope.disableDb();
   }
 
   private void setUpPermissions() throws Exception {
@@ -269,6 +269,7 @@
       }
     }
 
+    AcceptanceTestRequestScope.Context ctx = disableDb();
     try (Repository repo = repoManager.openRepository(project)) {
       ProjectControl ctl = projectControlFactory.controlFor(project,
           identifiedUserFactory.create(Providers.of(db), user.getId()));
@@ -277,6 +278,8 @@
       Map<String, Ref> all = repo.getAllRefs();
       assertThat(filter.filter(all, false).keySet())
           .containsExactlyElementsIn(expected);
+    } finally {
+      enableDb(ctx);
     }
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/CapabilitiesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/CapabilitiesIT.java
index 768c923..ce82270 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/CapabilitiesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/CapabilitiesIT.java
@@ -51,7 +51,7 @@
     allowGlobalCapabilities(REGISTERED_USERS, all);
     try {
       RestResponse r =
-          userSession.get("/accounts/self/capabilities");
+          userRestSession.get("/accounts/self/capabilities");
       r.assertOK();
       CapabilityInfo info = (new Gson()).fromJson(r.getReader(),
           new TypeToken<CapabilityInfo>() {}.getType());
@@ -79,7 +79,7 @@
   @Test
   public void testCapabilitiesAdmin() throws Exception {
     RestResponse r =
-        adminSession.get("/accounts/self/capabilities");
+        adminRestSession.get("/accounts/self/capabilities");
     r.assertOK();
     CapabilityInfo info = (new Gson()).fromJson(r.getReader(),
         new TypeToken<CapabilityInfo>() {}.getType());
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetAccountDetailIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetAccountDetailIT.java
index da1d3ec..f48f9fa 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetAccountDetailIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetAccountDetailIT.java
@@ -27,7 +27,7 @@
 public class GetAccountDetailIT extends AbstractDaemonTest {
   @Test
   public void getDetail() throws Exception {
-    RestResponse r = adminSession.get("/accounts/" + admin.username + "/detail/");
+    RestResponse r = adminRestSession.get("/accounts/" + admin.username + "/detail/");
     AccountDetailInfo info = newGson().fromJson(r.getReader(), AccountDetailInfo.class);
     assertAccountInfo(admin, info);
     Account account = accountCache.get(admin.getId()).getAccount();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/PutUsernameIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/PutUsernameIT.java
index 47a3684..3297c60 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/PutUsernameIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/PutUsernameIT.java
@@ -38,7 +38,7 @@
     PutUsername.Input in = new PutUsername.Input();
     in.username = "myUsername";
     RestResponse r =
-        adminSession.put("/accounts/" + createUser().get() + "/username", in);
+        adminRestSession.put("/accounts/" + createUser().get() + "/username", in);
     r.assertOK();
     assertThat(newGson().fromJson(r.getReader(), String.class)).isEqualTo(
         in.username);
@@ -48,7 +48,7 @@
   public void setExisting_Conflict() throws Exception {
     PutUsername.Input in = new PutUsername.Input();
     in.username = admin.username;
-    adminSession
+    adminRestSession
         .put("/accounts/" + createUser().get() + "/username", in)
         .assertConflict();
   }
@@ -57,14 +57,14 @@
   public void setNew_MethodNotAllowed() throws Exception {
     PutUsername.Input in = new PutUsername.Input();
     in.username = "newUsername";
-    adminSession
+    adminRestSession
         .put("/accounts/" + admin.username + "/username", in)
         .assertMethodNotAllowed();
   }
 
   @Test
   public void delete_MethodNotAllowed() throws Exception {
-    adminSession
+    adminRestSession
         .put("/accounts/" + admin.username + "/username")
         .assertMethodNotAllowed();
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java
new file mode 100644
index 0000000..879430a
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java
@@ -0,0 +1,169 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.account;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.extensions.client.ProjectWatchInfo;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+public class WatchedProjectsIT extends AbstractDaemonTest {
+
+  private static final String NEW_PROJECT_NAME = "newProjectAccess";
+
+  @Test
+  public void setAndGetWatchedProjects() throws Exception {
+    String projectName1 = createProject(NEW_PROJECT_NAME).get();
+    String projectName2 = createProject(NEW_PROJECT_NAME + "2").get();
+
+    List<ProjectWatchInfo> projectsToWatch = new ArrayList<>(2);
+
+    ProjectWatchInfo pwi = new ProjectWatchInfo();
+    pwi.project = projectName1;
+    pwi.notifyAbandonedChanges = true;
+    pwi.notifyNewChanges = true;
+    pwi.notifyAllComments = true;
+    projectsToWatch.add(pwi);
+
+    pwi = new ProjectWatchInfo();
+    pwi.project = projectName2;
+    pwi.filter = "branch:master";
+    pwi.notifySubmittedChanges = true;
+    pwi.notifyNewPatchSets = true;
+    projectsToWatch.add(pwi);
+
+    List<ProjectWatchInfo> persistedWatchedProjects =
+        gApi.accounts().self().setWatchedProjects(projectsToWatch);
+    assertThat(persistedWatchedProjects)
+        .containsAllIn(projectsToWatch).inOrder();
+  }
+
+  @Test
+  public void setAndDeleteWatchedProjects() throws Exception {
+    String projectName1 = createProject(NEW_PROJECT_NAME).get();
+    String projectName2 = createProject(NEW_PROJECT_NAME + "2").get();
+
+    List<ProjectWatchInfo> projectsToWatch = new LinkedList<>();
+
+    ProjectWatchInfo pwi = new ProjectWatchInfo();
+    pwi.project = projectName1;
+    pwi.notifyAbandonedChanges = true;
+    pwi.notifyNewChanges = true;
+    pwi.notifyAllComments = true;
+    projectsToWatch.add(pwi);
+
+    pwi = new ProjectWatchInfo();
+    pwi.project = projectName2;
+    pwi.filter = "branch:master";
+    pwi.notifySubmittedChanges = true;
+    pwi.notifyNewPatchSets = true;
+    projectsToWatch.add(pwi);
+
+    // Persist watched projects
+    gApi.accounts().self().setWatchedProjects(projectsToWatch);
+
+    List<String> d = Lists.newArrayList(projectName2);
+    gApi.accounts().self().deleteWatchedProjects(d);
+    projectsToWatch.remove(pwi);
+
+    List<ProjectWatchInfo> persistedWatchedProjects =
+        gApi.accounts().self().getWatchedProjects();
+
+    assertThat(persistedWatchedProjects).doesNotContain(pwi);
+    assertThat(persistedWatchedProjects).containsAllIn(projectsToWatch);
+  }
+
+  @Test
+  public void watchNonExistingProject() throws Exception {
+    String projectName = NEW_PROJECT_NAME + "3";
+
+    List<ProjectWatchInfo> projectsToWatch = new ArrayList<>(2);
+
+    ProjectWatchInfo pwi = new ProjectWatchInfo();
+    pwi.project = projectName;
+    pwi.notifyAbandonedChanges = true;
+    pwi.notifyNewChanges = true;
+    pwi.notifyAllComments = true;
+    projectsToWatch.add(pwi);
+
+    exception.expect(UnprocessableEntityException.class);
+    gApi.accounts().self().setWatchedProjects(projectsToWatch);
+  }
+
+  @Test
+  public void deleteNonExistingProject() throws Exception {
+    String projectName = project.get();
+
+    // Let another user watch a project
+    setApiUser(admin);
+    List<ProjectWatchInfo> projectsToWatch = new LinkedList<>();
+
+    ProjectWatchInfo pwi = new ProjectWatchInfo();
+    pwi.project = projectName;
+    pwi.notifyAbandonedChanges = true;
+    pwi.notifyNewChanges = true;
+    pwi.notifyAllComments = true;
+    projectsToWatch.add(pwi);
+
+    gApi.accounts().self().setWatchedProjects(projectsToWatch);
+
+    // Try to delete a watched project using a different user
+    List<String> d = Lists.newArrayList(projectName);
+    gApi.accounts().self().deleteWatchedProjects(d);
+
+    setApiUser(user);
+    exception.expect(UnprocessableEntityException.class);
+    gApi.accounts().self().deleteWatchedProjects(d);
+  }
+
+  @Test
+  public void modifyProjectWatchUsingOmittedValues() throws Exception {
+    String projectName = project.get();
+
+    // Let another user watch a project
+    setApiUser(admin);
+    List<ProjectWatchInfo> projectsToWatch = new LinkedList<>();
+
+    ProjectWatchInfo pwi = new ProjectWatchInfo();
+    pwi.project = projectName;
+    pwi.notifyAbandonedChanges = true;
+    pwi.notifyNewChanges = true;
+    pwi.notifyAllComments = true;
+    projectsToWatch.add(pwi);
+
+    // Persist a defined state
+    gApi.accounts().self().setWatchedProjects(projectsToWatch);
+
+    // Omit previously set value - will set it to false on the server
+    // The response will not carry this field then as we omit sending
+    // false values in JSON
+    pwi.notifyNewChanges = null;
+
+    // Perform update
+    gApi.accounts().self().setWatchedProjects(projectsToWatch);
+
+    List<ProjectWatchInfo> watchedProjects =
+        gApi.accounts().self().getWatchedProjects();
+
+    assertThat(watchedProjects).containsAllIn(projectsToWatch);
+  }
+}
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 28a01fb..7fb91f1 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
@@ -26,7 +26,6 @@
 import com.google.common.base.Function;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
@@ -82,6 +81,7 @@
 import org.slf4j.LoggerFactory;
 
 import java.io.ByteArrayOutputStream;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -95,7 +95,7 @@
     return submitWholeTopicEnabledConfig();
   }
 
-  private Map<String, String> mergeResults;
+  private Map<String, String> changeMergedEvents;
 
   @Inject
   private ApprovalsUtil approvalsUtil;
@@ -127,7 +127,7 @@
 
   @Before
   public void setUp() throws Exception {
-    mergeResults = Maps.newHashMap();
+    changeMergedEvents = new HashMap<>();
     eventListenerRegistration =
         eventListeners.add(new UserScopedEventListener() {
           @Override
@@ -139,7 +139,7 @@
             ChangeAttribute c = e.change.get();
             PatchSetAttribute ps = e.patchSet.get();
             log.debug("Merged {},{} as {}", ps.number, c.number, e.newRev);
-            mergeResults.put(e.change.get().number, e.newRev);
+            changeMergedEvents.put(e.change.get().number, e.newRev);
           }
 
           @Override
@@ -305,8 +305,8 @@
     // newRev of the ChangeMergedEvent.
     BranchInfo branch = gApi.projects().name(change.project)
         .branch(change.branch).get();
-    assertThat(mergeResults).isNotEmpty();
-    String newRev = mergeResults.get(Integer.toString(change._number));
+    assertThat(changeMergedEvents).isNotEmpty();
+    String newRev = changeMergedEvents.get(Integer.toString(change._number));
     assertThat(newRev).isNotNull();
     assertThat(branch.revision).isEqualTo(newRev);
   }
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 91f2962..40b0391 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
@@ -81,13 +81,34 @@
     assertMessage(secondMessage, it.next().message);
   }
 
+  @Test
+  public void postMessageWithTag() throws Exception {
+    String changeId = createChange().getChangeId();
+    String tag = "jenkins";
+    String msg = "Message with tag.";
+    postMessage(changeId, msg, tag);
+    ChangeInfo c = get(changeId);
+    assertThat(c.messages).isNotNull();
+    assertThat(c.messages).hasSize(2);
+    Iterator<ChangeMessageInfo> it = c.messages.iterator();
+    assertThat(it.next().message).isEqualTo("Uploaded patch set 1.");
+    ChangeMessageInfo actual = it.next();
+    assertMessage(msg, actual.message);
+    assertThat(actual.tag).isEqualTo(tag);
+  }
+
   private void assertMessage(String expected, String actual) {
     assertThat(actual).isEqualTo("Patch Set 1:\n\n" + expected);
   }
 
   private void postMessage(String changeId, String msg) throws Exception {
+    postMessage(changeId, msg, null);
+  }
+
+  private void postMessage(String changeId, String msg, String tag) throws Exception {
     ReviewInput in = new ReviewInput();
     in.message = msg;
+    in.tag = tag;
     gApi.changes().id(changeId).current().review(in);
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java
index c4216cd..b8f0ec9 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java
@@ -103,7 +103,7 @@
       assertThat(e).hasMessage(
           "Failed to submit 1 change due to the following problems:\n"
           + "Change " + n + ": Change contains a project configuration that"
-          +" changes the parent project.\n"
+          + " changes the parent project.\n"
           + "The change must be submitted by a Gerrit administrator.");
     }
 
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
index a714f6e..f8640bd 100644
--- 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
@@ -16,6 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
 import static java.util.concurrent.TimeUnit.SECONDS;
 import static org.eclipse.jgit.lib.Constants.SIGNED_OFF_BY_TAG;
 
@@ -31,7 +32,6 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.config.AnonymousCowardNameProvider;
-import com.google.gerrit.server.notedb.ChangeNoteUtil;
 import com.google.gerrit.testutil.ConfigSuite;
 import com.google.gerrit.testutil.TestTimeUtil;
 
@@ -116,14 +116,13 @@
 
   @Test
   public void noteDbCommit() throws Exception {
-    assume().that(notesMigration.enabled()).isTrue();
+    assume().that(notesMigration.readChanges()).isTrue();
 
     ChangeInfo c = assertCreateSucceeds(newChangeInput(ChangeStatus.NEW));
-    try (Repository repo = repoManager.openMetadataRepository(project);
+    try (Repository repo = repoManager.openRepository(project);
         RevWalk rw = new RevWalk(repo)) {
       RevCommit commit = rw.parseCommit(
-          repo.exactRef(ChangeNoteUtil.changeRefName(new Change.Id(c._number)))
-              .getObjectId());
+          repo.exactRef(changeMetaRef(new Change.Id(c._number))).getObjectId());
 
       assertThat(commit.getShortMessage()).isEqualTo("Create change");
 
@@ -178,14 +177,14 @@
 
   // TODO(davido): Expose setting of account preferences in the API
   private void setSignedOffByFooter() throws Exception {
-    RestResponse r = adminSession.get("/accounts/" + admin.email
+    RestResponse r = adminRestSession.get("/accounts/" + admin.email
         + "/preferences");
     r.assertOK();
     GeneralPreferencesInfo i =
         newGson().fromJson(r.getReader(), GeneralPreferencesInfo.class);
     i.signedOffBy = true;
 
-    r = adminSession.put("/accounts/" + admin.email + "/preferences", i);
+    r = adminRestSession.put("/accounts/" + admin.email + "/preferences", i);
     r.assertOK();
     GeneralPreferencesInfo o = newGson().fromJson(r.getReader(),
         GeneralPreferencesInfo.class);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftPatchSetIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftPatchSetIT.java
index b5875bb..29874e1 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftPatchSetIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftPatchSetIT.java
@@ -16,18 +16,24 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.PushOneCommit.Result;
-import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.acceptance.RestSession;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.extensions.api.changes.DraftInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
 import com.google.gerrit.extensions.client.ChangeStatus;
+import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 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.RefNames;
 import com.google.gerrit.server.config.AllUsersName;
@@ -38,6 +44,9 @@
 import org.eclipse.jgit.lib.Repository;
 import org.junit.Test;
 
+import java.util.HashMap;
+
+@NoHttpd
 public class DeleteDraftPatchSetIT extends AbstractDaemonTest {
 
   @Inject
@@ -51,9 +60,11 @@
     ChangeInfo c = get(triplet);
     assertThat(c.id).isEqualTo(triplet);
     assertThat(c.status).isEqualTo(ChangeStatus.NEW);
-    RestResponse r = deletePatchSet(changeId, ps, adminSession);
-    assertThat(r.getEntityContent()).isEqualTo("Patch set is not a draft");
-    r.assertConflict();
+
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("Patch set is not a draft");
+    setApiUser(admin);
+    deletePatchSet(changeId, ps);
   }
 
   @Test
@@ -64,9 +75,11 @@
     ChangeInfo c = get(triplet);
     assertThat(c.id).isEqualTo(triplet);
     assertThat(c.status).isEqualTo(ChangeStatus.DRAFT);
-    RestResponse r = deletePatchSet(changeId, ps, userSession);
-    assertThat(r.getEntityContent()).isEqualTo("Not found: " + changeId);
-    r.assertNotFound();
+
+    exception.expect(ResourceNotFoundException.class);
+    exception.expectMessage("Not found: " + changeId);
+    setApiUser(user);
+    deletePatchSet(changeId, ps);
   }
 
   @Test
@@ -87,14 +100,14 @@
     assertThat(cd.patchSets()).hasSize(2);
     assertThat(cd.change().currentPatchSetId().get()).isEqualTo(2);
     assertThat(cd.change().getStatus()).isEqualTo(Change.Status.DRAFT);
-    deletePatchSet(changeId, ps, adminSession).assertNoContent();
+    deletePatchSet(changeId, ps);
 
     cd = getChange(changeId);
     assertThat(cd.patchSets()).hasSize(1);
     assertThat(cd.change().currentPatchSetId().get()).isEqualTo(1);
 
     ps = getCurrentPatchSet(changeId);
-    deletePatchSet(changeId, ps, adminSession).assertNoContent();
+    deletePatchSet(changeId, ps);
     assertThat(queryProvider.get().byKeyPrefix(changeId)).isEmpty();
 
     if (notesMigration.writeChanges()) {
@@ -105,10 +118,80 @@
     gApi.changes().id(ps.getId().getParentKey().get());
   }
 
+  @Test
+  public void deleteDraftPS1() throws Exception {
+    String changeId = createDraftChangeWith2PS();
+
+    ReviewInput rin = new ReviewInput();
+    rin.message = "Change message";
+    CommentInput cin = new CommentInput();
+    cin.line = 1;
+    cin.patchSet = 1;
+    cin.path = PushOneCommit.FILE_NAME;
+    cin.side = Side.REVISION;
+    cin.message = "Inline comment";
+    rin.comments = new HashMap<>();
+    rin.comments.put(cin.path, ImmutableList.of(cin));
+    gApi.changes().id(changeId).revision(1).review(rin);
+
+    ChangeData cd = getChange(changeId);
+    PatchSet.Id delPsId = new PatchSet.Id(cd.getId(), 1);
+    PatchSet ps = cd.patchSet(delPsId);
+    deletePatchSet(changeId, ps);
+
+    cd = getChange(changeId);
+    assertThat(cd.patchSets()).hasSize(1);
+    assertThat(Iterables.getOnlyElement(cd.patchSets()).getId().get())
+        .isEqualTo(2);
+
+    // Other entities based on deleted patch sets are also deleted.
+    for (ChangeMessage m : cd.messages()) {
+      assertThat(m.getPatchSetId()).named(m.toString()).isNotEqualTo(delPsId);
+    }
+    for (PatchLineComment c : cd.publishedComments()) {
+      assertThat(c.getPatchSetId()).named(c.toString()).isNotEqualTo(delPsId);
+    }
+  }
+
+  @Test
+  public void deleteDraftPS2() throws Exception {
+    String changeId = createDraftChangeWith2PS();
+
+    ReviewInput rin = new ReviewInput();
+    rin.message = "Change message";
+    CommentInput cin = new CommentInput();
+    cin.line = 1;
+    cin.patchSet = 1;
+    cin.path = PushOneCommit.FILE_NAME;
+    cin.side = Side.REVISION;
+    cin.message = "Inline comment";
+    rin.comments = new HashMap<>();
+    rin.comments.put(cin.path, ImmutableList.of(cin));
+    gApi.changes().id(changeId).revision(1).review(rin);
+
+    ChangeData cd = getChange(changeId);
+    PatchSet.Id delPsId = new PatchSet.Id(cd.getId(), 2);
+    PatchSet ps = cd.patchSet(delPsId);
+    deletePatchSet(changeId, ps);
+
+    cd = getChange(changeId);
+    assertThat(cd.patchSets()).hasSize(1);
+    assertThat(Iterables.getOnlyElement(cd.patchSets()).getId().get())
+        .isEqualTo(1);
+
+    // Other entities based on deleted patch sets are also deleted.
+    for (ChangeMessage m : cd.messages()) {
+      assertThat(m.getPatchSetId()).named(m.toString()).isNotEqualTo(delPsId);
+    }
+    for (PatchLineComment c : cd.publishedComments()) {
+      assertThat(c.getPatchSetId()).named(c.toString()).isNotEqualTo(delPsId);
+    }
+  }
+
   private Ref getDraftRef(TestAccount account, Change.Id changeId)
       throws Exception {
-    try (Repository repo = repoManager.openMetadataRepository(allUsers)) {
-      return repo.exactRef(RefNames.refsDraftComments(account.id, changeId));
+    try (Repository repo = repoManager.openRepository(allUsers)) {
+      return repo.exactRef(RefNames.refsDraftComments(changeId, account.id));
     }
   }
 
@@ -128,11 +211,7 @@
     return Iterables.getOnlyElement(queryProvider.get().byKeyPrefix(changeId));
   }
 
-  private static RestResponse deletePatchSet(String changeId,
-      PatchSet ps, RestSession s) throws Exception {
-    return s.delete("/changes/"
-        + changeId
-        + "/revisions/"
-        + ps.getRevision().get());
+  private void deletePatchSet(String changeId, PatchSet ps) throws Exception {
+    gApi.changes().id(changeId).revision(ps.getId().get()).delete();
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DraftChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DraftChangeIT.java
index bcbec9a..3228a22a 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DraftChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DraftChangeIT.java
@@ -53,7 +53,7 @@
     ChangeInfo c = get(triplet);
     assertThat(c.id).isEqualTo(triplet);
     assertThat(c.status).isEqualTo(ChangeStatus.NEW);
-    RestResponse response = deleteChange(changeId, adminSession);
+    RestResponse response = deleteChange(changeId, adminRestSession);
     assertThat(response.getEntityContent())
         .isEqualTo("Change is not a draft: " + c._number);
     response.assertConflict();
@@ -69,7 +69,7 @@
     ChangeInfo c = get(triplet);
     assertThat(c.id).isEqualTo(triplet);
     assertThat(c.status).isEqualTo(ChangeStatus.DRAFT);
-    deleteChange(changeId, adminSession).assertNoContent();
+    deleteChange(changeId, adminRestSession).assertNoContent();
 
     exception.expect(ResourceNotFoundException.class);
     get(triplet);
@@ -151,13 +151,13 @@
   }
 
   private RestResponse publishChange(String changeId) throws Exception {
-    return adminSession.post("/changes/" + changeId + "/publish");
+    return adminRestSession.post("/changes/" + changeId + "/publish");
   }
 
   private RestResponse publishPatchSet(String changeId) throws Exception {
     PatchSet patchSet = Iterables.getOnlyElement(
         queryProvider.get().byKeyPrefix(changeId)).currentPatchSet();
-    return adminSession.post("/changes/"
+    return adminRestSession.post("/changes/"
         + changeId
         + "/revisions/"
         + patchSet.getRevision().get()
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/HashtagsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
index 3193c5e..a044772 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
@@ -38,7 +38,7 @@
 public class HashtagsIT extends AbstractDaemonTest {
   @Before
   public void before() {
-    assume().that(notesMigration.enabled()).isTrue();
+    assume().that(notesMigration.readChanges()).isTrue();
   }
 
   @BeforeClass
@@ -237,6 +237,14 @@
     assertMessage(r, "Hashtag removed: tag3");
   }
 
+  @Test
+  public void testHashtagWithMixedCase() throws Exception {
+    PushOneCommit.Result r = createChange();
+    addHashtags(r, "MyHashtag");
+    assertThatGet(r).containsExactly("MyHashtag");
+    assertMessage(r, "Hashtag added: MyHashtag");
+  }
+
   private IterableSubject<
         ? extends IterableSubject<?, String, Iterable<String>>,
         String, Iterable<String>>
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
index 7035bf9..9dba788b 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
@@ -22,7 +22,7 @@
   @Test
   public void indexChange() throws Exception {
     String changeId = createChange().getChangeId();
-    adminSession
+    adminRestSession
         .post("/changes/" + changeId + "/index/")
         .assertNoContent();
   }
@@ -31,7 +31,7 @@
   public void indexChangeOnNonVisibleBranch() throws Exception {
     String changeId = createChange().getChangeId();
     blockRead("refs/heads/master");
-    userSession
+    userRestSession
         .post("/changes/" + changeId + "/index/")
         .assertNotFound();
   }
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 54eca70..b0d34f0 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
@@ -20,7 +20,6 @@
 import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
 
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Lists;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
@@ -29,6 +28,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import java.util.ArrayList;
 import java.util.List;
 
 @NoHttpd
@@ -39,7 +39,7 @@
 
   @Before
   public void setUp() throws Exception {
-    results = Lists.newArrayList();
+    results = new ArrayList<>();
     results.add(push("file contents", null));
     changeId = results.get(0).getChangeId();
     results.add(push("new contents 1", changeId));
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java
index df1ebfd..b66358f 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java
@@ -32,7 +32,6 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Branch;
-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.Util;
@@ -45,7 +44,7 @@
 @NoHttpd
 public class MoveChangeIT extends AbstractDaemonTest {
   @Test
-  public void moveChange_shortRef() throws Exception {
+  public void moveChangeWithShortRef() throws Exception {
     // Move change to a different branch using short ref name
     PushOneCommit.Result r = createChange();
     Branch.NameKey newBranch =
@@ -56,7 +55,7 @@
   }
 
   @Test
-  public void moveChange_fullRef() throws Exception {
+  public void moveChangeWithFullRef() throws Exception {
     // Move change to a different branch using full ref name
     PushOneCommit.Result r = createChange();
     Branch.NameKey newBranch =
@@ -94,7 +93,7 @@
   }
 
   @Test
-  public void moveChange_sameChangeId() throws Exception {
+  public void moveChangeToSameChangeId() throws Exception {
     // Move change to a branch with existing change with same change ID
     PushOneCommit.Result r = createChange();
     Branch.NameKey newBranch =
@@ -160,7 +159,7 @@
   }
 
   @Test
-  public void moveChangeToBranch_WithoutUploadPerms() throws Exception {
+  public void moveChangeToBranchWithoutUploadPerms() throws Exception {
     // Move change to a destination where user doesn't have upload permissions
     PushOneCommit.Result r = createChange();
     Branch.NameKey newBranch =
@@ -175,7 +174,7 @@
   }
 
   @Test
-  public void moveChangeFromBranch_WithoutAbandonPerms() throws Exception {
+  public void moveChangeFromBranchWithoutAbandonPerms() throws Exception {
     // Move change for which user does not have abandon permissions
     PushOneCommit.Result r = createChange();
     Branch.NameKey newBranch =
@@ -218,7 +217,7 @@
   }
 
   @Test
-  public void moveChange_WithCurrentPatchSetLocked() throws Exception {
+  public void moveChangeWithCurrentPatchSetLocked() throws Exception {
     // Move change that is locked
     PushOneCommit.Result r = createChange();
     Branch.NameKey newBranch =
@@ -241,12 +240,6 @@
     move(r.getChangeId(), newBranch.get());
   }
 
-  private void saveProjectConfig(ProjectConfig cfg) throws Exception {
-    try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
-      cfg.commit(md);
-    }
-  }
-
   private void move(int changeNum, String destination)
       throws RestApiException {
     gApi.changes().id(changeNum).move(destination);
@@ -260,7 +253,7 @@
   private void move(String changeId, String destination, String message)
       throws RestApiException {
     MoveInput in = new MoveInput();
-    in.destination_branch = destination;
+    in.destinationBranch = destination;
     in.message = message;
     gApi.changes().id(changeId).move(in);
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java
index 5a6c36a..d5b6f14 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java
@@ -92,10 +92,10 @@
     approve(g.getChangeId());
     approve(h.getChangeId());
 
-    assertMergeable(e.getChange(), true);
-    assertMergeable(f.getChange(), true);
-    assertMergeable(g.getChange(), false);
-    assertMergeable(h.getChange(), false);
+    assertMergeable(e.getChange());
+    assertMergeable(f.getChange());
+    assertNotMergeable(g.getChange());
+    assertNotMergeable(h.getChange());
 
     PushOneCommit.Result m = createChange("M", "new.txt", "Resolved conflict",
         ImmutableList.of(d.getCommit(), h.getCommit()));
@@ -103,7 +103,7 @@
 
     assertChangeSetMergeable(m.getChange(), true);
 
-    assertMergeable(m.getChange(), true);
+    assertMergeable(m.getChange());
     submit(m.getChangeId());
 
     assertMerged(e.getChangeId());
@@ -138,15 +138,15 @@
     PushOneCommit.Result g = createChange("G", "new.txt", "Conflicting line #2",
         ImmutableList.of(f.getCommit()));
 
-    assertMergeable(e.getChange(), true);
+    assertMergeable(e.getChange());
 
     approve(a.getChangeId());
     approve(b.getChangeId());
     submit(b.getChangeId());
 
-    assertMergeable(e.getChange(), false);
-    assertMergeable(f.getChange(), true);
-    assertMergeable(g.getChange(), true);
+    assertNotMergeable(e.getChange());
+    assertMergeable(f.getChange());
+    assertMergeable(g.getChange());
 
     approve(c.getChangeId());
     approve(d.getChangeId());
@@ -156,7 +156,7 @@
     approve(f.getChangeId());
     approve(g.getChangeId());
 
-    assertMergeable(g.getChange(), false);
+    assertNotMergeable(g.getChange());
     assertChangeSetMergeable(g.getChange(), false);
   }
 
@@ -278,7 +278,7 @@
     approve(c.getChangeId());
     approve(e.getChangeId());
     approve(d.getChangeId());
-    assertMergeable(d.getChange(), false);
+    assertNotMergeable(d.getChange());
     assertChangeSetMergeable(d.getChange(), false);
   }
 
@@ -297,10 +297,14 @@
     assertThat(submit.unmergeableChanges(cs).isEmpty()).isEqualTo(expected);
   }
 
-  private void assertMergeable(ChangeData change, boolean expected)
-      throws Exception {
+  private void assertMergeable(ChangeData change) throws Exception {
     change.setMergeable(null);
-    assertThat(change.isMergeable()).isEqualTo(expected);
+    assertThat(change.isMergeable()).isTrue();
+  }
+
+  private void assertNotMergeable(ChangeData change) throws Exception {
+    change.setMergeable(null);
+    assertThat(change.isMergeable()).isFalse();
   }
 
   private void assertMerged(String changeId) throws Exception {
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
index 7754a53..13f7070 100644
--- 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
@@ -33,28 +33,28 @@
 
   @Test
   public void flushAll() throws Exception {
-    RestResponse r = adminSession.get("/config/server/caches/project_list");
+    RestResponse r = adminRestSession.get("/config/server/caches/project_list");
     CacheInfo cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class);
     assertThat(cacheInfo.entries.mem).isGreaterThan((long) 0);
 
-    r = adminSession.post("/config/server/caches/", new PostCaches.Input(FLUSH_ALL));
+    r = adminRestSession.post("/config/server/caches/", new PostCaches.Input(FLUSH_ALL));
     r.assertOK();
     r.consume();
 
-    r = adminSession.get("/config/server/caches/project_list");
+    r = adminRestSession.get("/config/server/caches/project_list");
     cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class);
     assertThat(cacheInfo.entries.mem).isNull();
   }
 
   @Test
   public void flushAll_Forbidden() throws Exception {
-    userSession.post("/config/server/caches/",
+    userRestSession.post("/config/server/caches/",
         new PostCaches.Input(FLUSH_ALL)).assertForbidden();
   }
 
   @Test
   public void flushAll_BadRequest() throws Exception {
-    adminSession
+    adminRestSession
         .post("/config/server/caches/",
             new PostCaches.Input(FLUSH_ALL, Arrays.asList("projects")))
         .assertBadRequest();
@@ -62,31 +62,31 @@
 
   @Test
   public void flush() throws Exception {
-    RestResponse r = adminSession.get("/config/server/caches/project_list");
+    RestResponse r = adminRestSession.get("/config/server/caches/project_list");
     CacheInfo cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class);
     assertThat(cacheInfo.entries.mem).isGreaterThan((long)0);
 
-    r = adminSession.get("/config/server/caches/projects");
+    r = adminRestSession.get("/config/server/caches/projects");
     cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class);
     assertThat(cacheInfo.entries.mem).isGreaterThan((long)1);
 
-    r = adminSession.post("/config/server/caches/",
+    r = adminRestSession.post("/config/server/caches/",
         new PostCaches.Input(FLUSH, Arrays.asList("accounts", "project_list")));
     r.assertOK();
     r.consume();
 
-    r = adminSession.get("/config/server/caches/project_list");
+    r = adminRestSession.get("/config/server/caches/project_list");
     cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class);
     assertThat(cacheInfo.entries.mem).isNull();
 
-    r = adminSession.get("/config/server/caches/projects");
+    r = adminRestSession.get("/config/server/caches/projects");
     cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class);
     assertThat(cacheInfo.entries.mem).isGreaterThan((long)1);
   }
 
   @Test
   public void flush_Forbidden() throws Exception {
-    userSession
+    userRestSession
         .post("/config/server/caches/",
             new PostCaches.Input(FLUSH, Arrays.asList("projects")))
         .assertForbidden();
@@ -94,7 +94,7 @@
 
   @Test
   public void flush_BadRequest() throws Exception {
-    adminSession
+    adminRestSession
         .post("/config/server/caches/",
             new PostCaches.Input(FLUSH))
         .assertBadRequest();
@@ -102,16 +102,16 @@
 
   @Test
   public void flush_UnprocessableEntity() throws Exception {
-    RestResponse r = adminSession.get("/config/server/caches/projects");
+    RestResponse r = adminRestSession.get("/config/server/caches/projects");
     CacheInfo cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class);
     assertThat(cacheInfo.entries.mem).isGreaterThan((long)0);
 
-    r = adminSession.post("/config/server/caches/",
+    r = adminRestSession.post("/config/server/caches/",
         new PostCaches.Input(FLUSH, Arrays.asList("projects", "unprocessable")));
     r.assertUnprocessableEntity();
     r.consume();
 
-    r = adminSession.get("/config/server/caches/projects");
+    r = adminRestSession.get("/config/server/caches/projects");
     cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class);
     assertThat(cacheInfo.entries.mem).isGreaterThan((long)0);
   }
@@ -121,12 +121,12 @@
     allowGlobalCapabilities(REGISTERED_USERS,
         GlobalCapability.FLUSH_CACHES, GlobalCapability.VIEW_CACHES);
     try {
-      RestResponse r = userSession.post("/config/server/caches/",
+      RestResponse r = userRestSession.post("/config/server/caches/",
           new PostCaches.Input(FLUSH, Arrays.asList("projects")));
       r.assertOK();
       r.consume();
 
-      userSession
+      userRestSession
           .post("/config/server/caches/",
               new PostCaches.Input(FLUSH, Arrays.asList("web_sessions")))
           .assertForbidden();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ConfirmEmailIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ConfirmEmailIT.java
index 0638637..259a1b4 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ConfirmEmailIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ConfirmEmailIT.java
@@ -40,7 +40,7 @@
   public void confirm() throws Exception {
     ConfirmEmail.Input in = new ConfirmEmail.Input();
     in.token = emailTokenVerifier.encode(admin.getId(), "new.mail@example.com");
-    adminSession
+    adminRestSession
         .put("/config/server/email.confirm", in)
         .assertNoContent();
   }
@@ -49,7 +49,7 @@
   public void confirmForOtherUser_UnprocessableEntity() throws Exception {
     ConfirmEmail.Input in = new ConfirmEmail.Input();
     in.token = emailTokenVerifier.encode(user.getId(), "new.mail@example.com");
-    adminSession
+    adminRestSession
         .put("/config/server/email.confirm", in)
         .assertUnprocessableEntity();
   }
@@ -58,7 +58,7 @@
   public void confirmInvalidToken_UnprocessableEntity() throws Exception {
     ConfirmEmail.Input in = new ConfirmEmail.Input();
     in.token = "invalidToken";
-    adminSession
+    adminRestSession
         .put("/config/server/email.confirm", in)
         .assertUnprocessableEntity();
   }
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
index 0113672..149d05f 100644
--- 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
@@ -28,43 +28,43 @@
 
   @Test
   public void flushCache() throws Exception {
-    RestResponse r = adminSession.get("/config/server/caches/groups");
+    RestResponse r = adminRestSession.get("/config/server/caches/groups");
     CacheInfo result = newGson().fromJson(r.getReader(), CacheInfo.class);
     assertThat(result.entries.mem).isGreaterThan((long)0);
 
-    r = adminSession.post("/config/server/caches/groups/flush");
+    r = adminRestSession.post("/config/server/caches/groups/flush");
     r.assertOK();
     r.consume();
 
-    r = adminSession.get("/config/server/caches/groups");
+    r = adminRestSession.get("/config/server/caches/groups");
     result = newGson().fromJson(r.getReader(), CacheInfo.class);
     assertThat(result.entries.mem).isNull();
   }
 
   @Test
   public void flushCache_Forbidden() throws Exception {
-    userSession
+    userRestSession
         .post("/config/server/caches/accounts/flush")
         .assertForbidden();
   }
 
   @Test
   public void flushCache_NotFound() throws Exception {
-    adminSession
+    adminRestSession
         .post("/config/server/caches/nonExisting/flush")
         .assertNotFound();
   }
 
   @Test
   public void flushCacheWithGerritPrefix() throws Exception {
-    adminSession
+    adminRestSession
         .post("/config/server/caches/gerrit-accounts/flush")
         .assertOK();
   }
 
   @Test
   public void flushWebSessionsCache() throws Exception {
-    adminSession
+    adminRestSession
         .post("/config/server/caches/web_sessions/flush")
         .assertOK();
   }
@@ -74,11 +74,11 @@
     allowGlobalCapabilities(REGISTERED_USERS,
         GlobalCapability.VIEW_CACHES, GlobalCapability.FLUSH_CACHES);
     try {
-      RestResponse r = userSession.post("/config/server/caches/accounts/flush");
+      RestResponse r = userRestSession.post("/config/server/caches/accounts/flush");
       r.assertOK();
       r.consume();
 
-      userSession
+      userRestSession
           .post("/config/server/caches/web_sessions/flush")
           .assertForbidden();
     } finally {
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
index ff8a3b9..1a1ccd90 100644
--- 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
@@ -27,7 +27,7 @@
 
   @Test
   public void getCache() throws Exception {
-    RestResponse r = adminSession.get("/config/server/caches/accounts");
+    RestResponse r = adminRestSession.get("/config/server/caches/accounts");
     r.assertOK();
     CacheInfo result = newGson().fromJson(r.getReader(), CacheInfo.class);
 
@@ -42,8 +42,8 @@
     assertThat(result.hitRatio.mem).isAtMost(100);
     assertThat(result.hitRatio.disk).isNull();
 
-    userSession.get("/config/server/version").consume();
-    r = adminSession.get("/config/server/caches/accounts");
+    userRestSession.get("/config/server/version").consume();
+    r = adminRestSession.get("/config/server/caches/accounts");
     r.assertOK();
     result = newGson().fromJson(r.getReader(), CacheInfo.class);
     assertThat(result.entries.mem).isEqualTo(2);
@@ -51,21 +51,21 @@
 
   @Test
   public void getCache_Forbidden() throws Exception {
-    userSession
+    userRestSession
         .get("/config/server/caches/accounts")
         .assertForbidden();
   }
 
   @Test
   public void getCache_NotFound() throws Exception {
-    adminSession
+    adminRestSession
         .get("/config/server/caches/nonExisting")
         .assertNotFound();
   }
 
   @Test
   public void getCacheWithGerritPrefix() throws Exception {
-    adminSession
+    adminRestSession
         .get("/config/server/caches/gerrit-accounts")
         .assertOK();
   }
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
index 8455062..1321650 100644
--- 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
@@ -30,7 +30,7 @@
   @Test
   public void getTask() throws Exception {
     RestResponse r =
-        adminSession.get("/config/server/tasks/" + getLogFileCompressorTaskId());
+        adminRestSession.get("/config/server/tasks/" + getLogFileCompressorTaskId());
     r.assertOK();
     TaskInfo info =
         newGson().fromJson(r.getReader(),
@@ -43,13 +43,13 @@
 
   @Test
   public void getTask_NotFound() throws Exception {
-    userSession
+    userRestSession
         .get("/config/server/tasks/" + getLogFileCompressorTaskId())
         .assertNotFound();
   }
 
   private String getLogFileCompressorTaskId() throws Exception {
-    RestResponse r = adminSession.get("/config/server/tasks/");
+    RestResponse r = adminRestSession.get("/config/server/tasks/");
     List<TaskInfo> result =
         newGson().fromJson(r.getReader(),
             new TypeToken<List<TaskInfo>>() {}.getType());
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
index 88b93c8..306bb58 100644
--- 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
@@ -28,18 +28,18 @@
 public class KillTaskIT extends AbstractDaemonTest {
 
   private void killTask() throws Exception {
-    RestResponse r = adminSession.get("/config/server/tasks/");
+    RestResponse r = adminRestSession.get("/config/server/tasks/");
     List<TaskInfo> result = newGson().fromJson(r.getReader(),
         new TypeToken<List<TaskInfo>>() {}.getType());
     r.consume();
     int taskCount = result.size();
     assertThat(taskCount).isGreaterThan(0);
 
-    r = adminSession.delete("/config/server/tasks/" + result.get(0).id);
+    r = adminRestSession.delete("/config/server/tasks/" + result.get(0).id);
     r.assertNoContent();
     r.consume();
 
-    r = adminSession.get("/config/server/tasks/");
+    r = adminRestSession.get("/config/server/tasks/");
     result = newGson().fromJson(r.getReader(),
         new TypeToken<List<TaskInfo>>() {}.getType());
     r.consume();
@@ -47,13 +47,13 @@
   }
 
   private void killTask_NotFound() throws Exception {
-    RestResponse r = adminSession.get("/config/server/tasks/");
+    RestResponse r = adminRestSession.get("/config/server/tasks/");
     List<TaskInfo> result = newGson().fromJson(r.getReader(),
         new TypeToken<List<TaskInfo>>() {}.getType());
     r.consume();
     assertThat(result.size()).isGreaterThan(0);
 
-    userSession
+    userRestSession
         .delete("/config/server/tasks/" + result.get(0).id)
         .assertNotFound();
   }
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
index 964c759..d0a5070 100644
--- 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
@@ -35,7 +35,7 @@
 
   @Test
   public void listCaches() throws Exception {
-    RestResponse r = adminSession.get("/config/server/caches/");
+    RestResponse r = adminRestSession.get("/config/server/caches/");
     r.assertOK();
     Map<String, CacheInfo> result =
         newGson().fromJson(r.getReader(),
@@ -53,8 +53,8 @@
     assertThat(accountsCacheInfo.hitRatio.mem).isAtMost(100);
     assertThat(accountsCacheInfo.hitRatio.disk).isNull();
 
-    userSession.get("/config/server/version").consume();
-    r = adminSession.get("/config/server/caches/");
+    userRestSession.get("/config/server/version").consume();
+    r = adminRestSession.get("/config/server/caches/");
     r.assertOK();
     result = newGson().fromJson(r.getReader(),
         new TypeToken<Map<String, CacheInfo>>() {}.getType());
@@ -63,14 +63,14 @@
 
   @Test
   public void listCaches_Forbidden() throws Exception {
-    userSession
+    userRestSession
         .get("/config/server/caches/")
         .assertForbidden();
   }
 
   @Test
   public void listCacheNames() throws Exception {
-    RestResponse r = adminSession.get("/config/server/caches/?format=LIST");
+    RestResponse r = adminRestSession.get("/config/server/caches/?format=LIST");
     r.assertOK();
     List<String> result =
         newGson().fromJson(r.getReader(),
@@ -82,7 +82,7 @@
 
   @Test
   public void listCacheNamesTextList() throws Exception {
-    RestResponse r = adminSession.get("/config/server/caches/?format=TEXT_LIST");
+    RestResponse r = adminRestSession.get("/config/server/caches/?format=TEXT_LIST");
     r.assertOK();
     String result = new String(Base64.decode(r.getEntityContent()), UTF_8.name());
     List<String> list = Arrays.asList(result.split("\n"));
@@ -93,7 +93,7 @@
 
   @Test
   public void listCaches_BadRequest() throws Exception {
-    adminSession
+    adminRestSession
         .get("/config/server/caches/?format=NONSENSE")
         .assertBadRequest();
   }
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
index 0b2c6cc..c405ff2 100644
--- 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
@@ -29,7 +29,7 @@
 
   @Test
   public void listTasks() throws Exception {
-    RestResponse r = adminSession.get("/config/server/tasks/");
+    RestResponse r = adminRestSession.get("/config/server/tasks/");
     r.assertOK();
     List<TaskInfo> result =
         newGson().fromJson(r.getReader(),
@@ -50,7 +50,7 @@
 
   @Test
   public void listTasksWithoutViewQueueCapability() throws Exception {
-    RestResponse r = userSession.get("/config/server/tasks/");
+    RestResponse r = userRestSession.get("/config/server/tasks/");
     r.assertOK();
     List<TaskInfo> result =
         newGson().fromJson(r.getReader(),
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
index a656760..a6ea4d2 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.rest.config;
 
 import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GerritConfig;
@@ -29,6 +30,9 @@
 
 import org.junit.Test;
 
+import java.nio.file.Files;
+import java.nio.file.Path;
+
 public class ServerInfoIT extends AbstractDaemonTest {
 
   @Test
@@ -70,7 +74,7 @@
     @GerritConfig(name = "user.anonymousCoward", value = "Unnamed User"),
   })
   public void serverConfig() throws Exception {
-    RestResponse r = adminSession.get("/config/server/info/");
+    RestResponse r = adminRestSession.get("/config/server/info/");
     ServerInfo i = newGson().fromJson(r.getReader(), ServerInfo.class);
 
     // auth
@@ -107,6 +111,9 @@
     // gitweb
     assertThat(i.gitweb).isNull();
 
+    // plugin
+    assertThat(i.plugin.jsResourcePaths).isEmpty();
+
     // sshd
     assertThat(i.sshd).isNotNull();
 
@@ -118,8 +125,23 @@
   }
 
   @Test
+  @GerritConfig(name = "plugins.allowRemoteAdmin", value = "true")
+  public void serverConfigWithPlugin() throws Exception {
+    Path plugins = tempSiteDir.newFolder("plugins").toPath();
+    Path jsplugin = plugins.resolve("js-plugin-1.js");
+    Files.write(jsplugin, "Gerrit.install(function(self){});\n".getBytes(UTF_8));
+    adminSshSession.exec("gerrit plugin reload");
+
+    RestResponse r = adminRestSession.get("/config/server/info/");
+    ServerInfo i = newGson().fromJson(r.getReader(), ServerInfo.class);
+
+    // plugin
+    assertThat(i.plugin.jsResourcePaths).hasSize(1);
+  }
+
+  @Test
   public void serverConfigWithDefaults() throws Exception {
-    RestResponse r = adminSession.get("/config/server/info/");
+    RestResponse r = adminRestSession.get("/config/server/info/");
     ServerInfo i = newGson().fromJson(r.getReader(), ServerInfo.class);
 
     // auth
@@ -157,6 +179,9 @@
     // gitweb
     assertThat(i.gitweb).isNull();
 
+    // plugin
+    assertThat(i.plugin.jsResourcePaths).isEmpty();
+
     // sshd
     assertThat(i.sshd).isNotNull();
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/AddMemberIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/AddMemberIT.java
index 1fbebe8..e37567c 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/AddMemberIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/AddMemberIT.java
@@ -21,7 +21,7 @@
 public class AddMemberIT extends AbstractDaemonTest {
   @Test
   public void addNonExistingMember_NotFound() throws Exception {
-    adminSession
+    adminRestSession
         .put("/groups/Administrators/members/non-existing")
         .assertNotFound();
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/AccessIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/AccessIT.java
new file mode 100644
index 0000000..9a5dfeb
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/AccessIT.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.server.config.AllProjectsNameProvider;
+
+import org.junit.Test;
+
+public class AccessIT extends AbstractDaemonTest {
+  @Test
+  public void testGetDefaultInheritance() throws Exception {
+    String newProjectName = createProject("newProjectAccess").get();
+    String inheritedName = gApi.projects()
+      .name(newProjectName).access().inheritsFrom.name;
+    assertThat(inheritedName).isEqualTo(AllProjectsNameProvider.DEFAULT);
+  }
+}
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
index d5845b2..23fe562 100644
--- 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
@@ -37,7 +37,7 @@
         .create();
 
     RestResponse r =
-        adminSession.put("/projects/" + project.get() + "/ban/",
+        adminRestSession.put("/projects/" + project.get() + "/ban/",
             BanCommit.Input.fromCommits(c.name()));
     r.assertOK();
     BanResultInfo info = newGson().fromJson(r.getReader(), BanResultInfo.class);
@@ -55,11 +55,11 @@
   @Test
   public void banAlreadyBannedCommit() throws Exception {
     RestResponse r =
-        adminSession.put("/projects/" + project.get() + "/ban/",
+        adminRestSession.put("/projects/" + project.get() + "/ban/",
             BanCommit.Input.fromCommits("a8a477efffbbf3b44169bb9a1d3a334cbbd9aa96"));
     r.consume();
 
-    r = adminSession.put("/projects/" + project.get() + "/ban/",
+    r = adminRestSession.put("/projects/" + project.get() + "/ban/",
         BanCommit.Input.fromCommits("a8a477efffbbf3b44169bb9a1d3a334cbbd9aa96"));
     r.assertOK();
     BanResultInfo info = newGson().fromJson(r.getReader(), BanResultInfo.class);
@@ -71,7 +71,7 @@
 
   @Test
   public void banCommit_Forbidden() throws Exception {
-    userSession
+    userRestSession
         .put("/projects/" + project.get() + "/ban/", BanCommit.Input.fromCommits(
             "a8a477efffbbf3b44169bb9a1d3a334cbbd9aa96"))
         .assertForbidden();
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 ccf8aee..b8a0e4b 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
@@ -54,7 +54,7 @@
   @Test
   public void testCreateProjectHttp() throws Exception {
     String newProjectName = name("newProject");
-    RestResponse r = adminSession.put("/projects/" + newProjectName);
+    RestResponse r = adminRestSession.put("/projects/" + newProjectName);
     r.assertCreated();
     ProjectInfo p = newGson().fromJson(r.getReader(), ProjectInfo.class);
     assertThat(p.name).isEqualTo(newProjectName);
@@ -67,7 +67,7 @@
   @Test
   public void testCreateProjectHttpWhenProjectAlreadyExists_Conflict()
       throws Exception {
-    adminSession
+    adminRestSession
         .put("/projects/" + allProjects.get())
         .assertConflict();
   }
@@ -75,7 +75,7 @@
   @Test
   public void testCreateProjectHttpWhenProjectAlreadyExists_PreconditionFailed()
       throws Exception {
-    adminSession
+    adminRestSession
         .putWithHeader("/projects/" + allProjects.get(),
             new BasicHeader(HttpHeaders.IF_NONE_MATCH, "*"))
         .assertPreconditionFailed();
@@ -85,7 +85,7 @@
   @UseLocalDisk
   public void testCreateProjectHttpWithUnreasonableName_BadRequest()
       throws Exception {
-    adminSession
+    adminRestSession
         .put("/projects/" + Url.encode(name("invalid/../name")))
         .assertBadRequest();
   }
@@ -94,7 +94,7 @@
   public void testCreateProjectHttpWithNameMismatch_BadRequest() throws Exception {
     ProjectInput in = new ProjectInput();
     in.name = name("otherName");
-    adminSession
+    adminRestSession
         .put("/projects/" + name("someName"), in)
         .assertBadRequest();
   }
@@ -104,7 +104,7 @@
       throws Exception {
     ProjectInput in = new ProjectInput();
     in.branches = Collections.singletonList(name("invalid ref name"));
-    adminSession
+    adminRestSession
         .put("/projects/" + name("newProject"), in)
         .assertBadRequest();
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java
index b9ccc8a..8522a4d 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java
@@ -43,7 +43,7 @@
 
   @Test
   public void testGcNotAllowed_Forbidden() throws Exception {
-    userSession
+    userRestSession
         .post("/projects/" + allProjects.get() + "/gc")
         .assertForbidden();
   }
@@ -57,7 +57,7 @@
   }
 
   private RestResponse POST(String endPoint) throws Exception {
-    RestResponse r = adminSession.post(endPoint);
+    RestResponse r = adminRestSession.post(endPoint);
     r.consume();
     return r;
   }
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
index 54371f2..307d512 100644
--- 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
@@ -129,13 +129,13 @@
   }
 
   private void assertNotFound(ObjectId id) throws Exception {
-    userSession
+    userRestSession
         .get("/projects/" + project.get() + "/commits/" + id.name())
         .assertNotFound();
   }
 
   private CommitInfo getCommit(ObjectId id) throws Exception {
-    RestResponse r = userSession.get(
+    RestResponse r = userRestSession.get(
         "/projects/" + project.get() + "/commits/" + id.name());
     r.assertOK();
     CommitInfo result = newGson().fromJson(r.getReader(), CommitInfo.class);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/SetParentIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/SetParentIT.java
index b77fb01..b106e99 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/SetParentIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/SetParentIT.java
@@ -29,7 +29,7 @@
   public void setParent_Forbidden() throws Exception {
     String parent = createProject("parent", null, true).get();
     RestResponse r =
-        userSession.put("/projects/" + project.get() + "/parent",
+        userRestSession.put("/projects/" + project.get() + "/parent",
             newParentInput(parent));
     r.assertForbidden();
     r.consume();
@@ -39,12 +39,12 @@
   public void setParent() throws Exception {
     String parent = createProject("parent", null, true).get();
     RestResponse r =
-        adminSession.put("/projects/" + project.get() + "/parent",
+        adminRestSession.put("/projects/" + project.get() + "/parent",
             newParentInput(parent));
     r.assertOK();
     r.consume();
 
-    r = adminSession.get("/projects/" + project.get() + "/parent");
+    r = adminRestSession.get("/projects/" + project.get() + "/parent");
     r.assertOK();
     String newParent =
         newGson().fromJson(r.getReader(), String.class);
@@ -53,12 +53,12 @@
 
     // When the parent name is not explicitly set, it should be
     // set to "All-Projects".
-    r = adminSession.put("/projects/" + project.get() + "/parent",
+    r = adminRestSession.put("/projects/" + project.get() + "/parent",
           newParentInput(null));
     r.assertOK();
     r.consume();
 
-    r = adminSession.get("/projects/" + project.get() + "/parent");
+    r = adminRestSession.get("/projects/" + project.get() + "/parent");
     r.assertOK();
     newParent = newGson().fromJson(r.getReader(), String.class);
     assertThat(newParent).isEqualTo(AllProjectsNameProvider.DEFAULT);
@@ -68,7 +68,7 @@
   @Test
   public void setParentForAllProjects_Conflict() throws Exception {
     RestResponse r =
-        adminSession.put("/projects/" + allProjects.get() + "/parent",
+        adminRestSession.put("/projects/" + allProjects.get() + "/parent",
             newParentInput(project.get()));
     r.assertConflict();
     r.consume();
@@ -77,19 +77,19 @@
   @Test
   public void setInvalidParent_Conflict() throws Exception {
     RestResponse r =
-        adminSession.put("/projects/" + project.get() + "/parent",
+        adminRestSession.put("/projects/" + project.get() + "/parent",
             newParentInput(project.get()));
     r.assertConflict();
     r.consume();
 
     Project.NameKey child = createProject("child", project, true);
-    r = adminSession.put("/projects/" + project.get() + "/parent",
+    r = adminRestSession.put("/projects/" + project.get() + "/parent",
            newParentInput(child.get()));
     r.assertConflict();
     r.consume();
 
     String grandchild = createProject("grandchild", child, true).get();
-    r = adminSession.put("/projects/" + project.get() + "/parent",
+    r = adminRestSession.put("/projects/" + project.get() + "/parent",
            newParentInput(grandchild));
     r.assertConflict();
     r.consume();
@@ -98,7 +98,7 @@
   @Test
   public void setNonExistingParent_UnprocessibleEntity() throws Exception {
     RestResponse r =
-        adminSession.put("/projects/" + project.get() + "/parent",
+        adminRestSession.put("/projects/" + project.get() + "/parent",
             newParentInput("non-existing"));
     r.assertUnprocessableEntity();
     r.consume();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/TagsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/TagsIT.java
index ed67d66..3a3e74f 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/TagsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/TagsIT.java
@@ -41,7 +41,7 @@
 
   @Test
   public void listTagsOfNonExistingProject() throws Exception {
-    adminSession
+    adminRestSession
         .get("/projects/non-existing/tags")
         .assertNotFound();
   }
@@ -61,7 +61,7 @@
   @Test
   public void listTagsOfNonVisibleProject() throws Exception {
     blockRead("refs/*");
-    userSession
+    userRestSession
         .get("/projects/" + project.get() + "/tags")
         .assertNotFound();
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
index b7a6b93..dd337c8 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -19,7 +19,6 @@
 import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
 
 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.gerrit.acceptance.AbstractDaemonTest;
@@ -232,6 +231,8 @@
       PushOneCommit.Result r = push.to("refs/for/master");
       String changeId = r.getChangeId();
       String revId = r.getCommit().getName();
+      Timestamp origLastUpdated = r.getChange().change().getLastUpdatedOn();
+
       ReviewInput input = new ReviewInput();
       CommentInput comment = newComment(file, Side.REVISION, line, "comment 1");
       comment.updated = timestamp;
@@ -249,6 +250,10 @@
       assertCommentInfo(comment, actual);
       assertThat(actual.updated)
           .isEqualTo(gApi.changes().id(r.getChangeId()).info().created);
+
+      // Updating historic comments doesn't cause lastUpdatedOn to regress.
+      assertThat(r.getChange().change().getLastUpdatedOn())
+          .isEqualTo(origLastUpdated);
     }
   }
 
@@ -383,6 +388,10 @@
         newDraft(FILE_NAME, Side.REVISION, 1, "join lines"));
     addDraft(r2.getChangeId(), r2.getCommit().getName(),
         newDraft(FILE_NAME, Side.REVISION, 2, "typo: content"));
+    addDraft(r2.getChangeId(), r2.getCommit().getName(),
+        newDraft(FILE_NAME, Side.PARENT, 1, "comment 1 on base"));
+    addDraft(r2.getChangeId(), r2.getCommit().getName(),
+        newDraft(FILE_NAME, Side.PARENT, 2, "comment 2 on base"));
 
     PushOneCommit.Result other = createChange();
     // Drafts on other changes aren't returned.
@@ -431,9 +440,11 @@
         .comments();
     assertThat(ps2Map.keySet()).containsExactly(FILE_NAME);
     List<CommentInfo> ps2List = ps2Map.get(FILE_NAME);
-    assertThat(ps2List).hasSize(2);
-    assertThat(ps2List.get(0).message).isEqualTo("join lines");
-    assertThat(ps2List.get(1).message).isEqualTo("typo: content");
+    assertThat(ps2List).hasSize(4);
+    assertThat(ps2List.get(0).message).isEqualTo("comment 1 on base");
+    assertThat(ps2List.get(1).message).isEqualTo("comment 2 on base");
+    assertThat(ps2List.get(2).message).isEqualTo("join lines");
+    assertThat(ps2List.get(3).message).isEqualTo("typo: content");
 
     ImmutableList<Message> messages =
         email.getMessages(r2.getChangeId(), "comment");
@@ -443,7 +454,7 @@
     assertThat(extractComments(messages.get(0).body())).isEqualTo(
         "Patch Set 2:\n"
         + "\n"
-        + "(4 comments)\n"
+        + "(6 comments)\n"
         + "\n"
         + "comments\n"
         + "\n"
@@ -461,6 +472,14 @@
         + url + "#/c/" + c + "/2/a.txt\n"
         + "File a.txt:\n"
         + "\n"
+        + "PS2, Line 1: \n"
+        + "comment 1 on base\n"
+        + "\n"
+        + "\n"
+        + "PS2, Line 2: \n"
+        + "comment 2 on base\n"
+        + "\n"
+        + "\n"
         + "PS2, Line 1: ew\n"
         + "join lines\n"
         + "\n"
@@ -470,6 +489,36 @@
         + "\n");
   }
 
+  @Test
+  public void commentTags() throws Exception {
+    PushOneCommit.Result r = createChange();
+
+    CommentInput pub = new CommentInput();
+    pub.line = 1;
+    pub.message = "published comment";
+    pub.path = FILE_NAME;
+    ReviewInput rin = newInput(pub);
+    rin.tag = "tag1";
+    gApi.changes().id(r.getChangeId()).current().review(rin);
+
+    List<CommentInfo> comments =
+        gApi.changes().id(r.getChangeId()).current().commentsAsList();
+    assertThat(comments).hasSize(1);
+    assertThat(comments.get(0).tag).isEqualTo("tag1");
+
+    DraftInput draft = new DraftInput();
+    draft.line = 2;
+    draft.message = "draft comment";
+    draft.path = FILE_NAME;
+    draft.tag = "tag2";
+    addDraft(r.getChangeId(), r.getCommit().name(), draft);
+
+    List<CommentInfo> drafts =
+        gApi.changes().id(r.getChangeId()).current().draftsAsList();
+    assertThat(drafts).hasSize(1);
+    assertThat(drafts.get(0).tag).isEqualTo("tag2");
+  }
+
   private static String extractComments(String msg) {
     // Extract lines between start "....." and end "-- ".
     Pattern p = Pattern.compile(".*[.]{5}\n+(.*)\\n+-- \n.*", Pattern.DOTALL);
@@ -477,6 +526,13 @@
     return m.matches() ? m.group(1) : msg;
   }
 
+  private ReviewInput newInput(CommentInput c) {
+    ReviewInput in = new ReviewInput();
+    in.comments = new HashMap<>();
+    in.comments.put(c.path, Lists.newArrayList(c));
+    return in;
+  }
+
   private void addComment(PushOneCommit.Result r, String message)
       throws Exception {
     addComment(r, message, false);
@@ -488,9 +544,7 @@
     c.line = 1;
     c.message = message;
     c.path = FILE_NAME;
-    ReviewInput in = new ReviewInput();
-    in.comments = ImmutableMap.<String, List<CommentInput>> of(
-        FILE_NAME, ImmutableList.of(c));
+    ReviewInput in = newInput(c);
     in.omitDuplicateComments = omitDuplicateComments;
     gApi.changes()
         .id(r.getChangeId())
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
index 1436cc0..40ea296 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
@@ -654,7 +654,7 @@
       throws Exception {
     String url = String.format("/changes/%d/revisions/%d/related",
         changeId.get(), ps);
-    return newGson().fromJson(adminSession.get(url).getReader(),
+    return newGson().fromJson(adminRestSession.get(url).getReader(),
         RelatedInfo.class).changes;
   }
 
@@ -692,6 +692,7 @@
           PatchSet ps = psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
           psUtil.setGroups(ctx.getDb(), ctx.getUpdate(psId), ps,
               ImmutableList.<String> of());
+          ctx.bumpLastUpdatedOn(false);
           return true;
         }
       });
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/event/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/event/BUCK
new file mode 100644
index 0000000..49d998c
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/event/BUCK
@@ -0,0 +1,7 @@
+include_defs('//gerrit-acceptance-tests/tests.defs')
+
+acceptance_tests(
+  group = 'server-event',
+  srcs = glob(['*IT.java']),
+  labels = ['server'],
+)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/event/CommentAddedEventIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/event/CommentAddedEventIT.java
index 3c99dc3..33c7251 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/event/CommentAddedEventIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/event/CommentAddedEventIT.java
@@ -18,7 +18,6 @@
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.project.Util.category;
 import static com.google.gerrit.server.project.Util.value;
-import static org.junit.Assert.fail;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
@@ -105,6 +104,20 @@
     saveProjectConfig(project, cfg);
   }
 
+  /* Need to lookup info for the label under test since there can be multiple
+   * labels defined.  By default Gerrit already has a Code-Review label.
+   */
+  private ApprovalAttribute getApprovalAttribute(LabelType label) {
+    ApprovalAttribute[] aa = lastCommentAddedEvent.approvals.get();
+    ApprovalAttribute res = null;
+    for (int i=0; i < aa.length; i++) {
+      if (aa[i].description.equals(label.getName())) {
+        res = aa[i];
+      }
+    }
+    return res;
+  }
+
   @Test
   public void newChangeWithVote() throws Exception {
     saveLabelConfig();
@@ -114,10 +127,9 @@
     ReviewInput reviewInput = new ReviewInput().label(
         label.getName(), (short)-1);
     revision(r).review(reviewInput);
-    String newVote = lastCommentAddedEvent.approvals.get()[0].value;
-    String oldVote = lastCommentAddedEvent.approvals.get()[0].oldValue;
-    assertThat(oldVote).isEqualTo("0");
-    assertThat(newVote).isEqualTo("-1");
+    ApprovalAttribute attr = getApprovalAttribute(label);
+    assertThat(attr.oldValue).isEqualTo("0");
+    assertThat(attr.value).isEqualTo("-1");
     assertThat(lastCommentAddedEvent.comment).isEqualTo(
         String.format("Patch Set 1: %s-1", label.getName()));
   }
@@ -137,10 +149,9 @@
     reviewInput = new ReviewInput().label(
         label.getName(), (short)1);
     revision(r).review(reviewInput);
-    String newVote = lastCommentAddedEvent.approvals.get()[0].value;
-    String oldVote = lastCommentAddedEvent.approvals.get()[0].oldValue;
-    assertThat(oldVote).isEqualTo("0");
-    assertThat(newVote).isEqualTo("1");
+    ApprovalAttribute attr = getApprovalAttribute(label);
+    assertThat(attr.oldValue).isEqualTo("0");
+    assertThat(attr.value).isEqualTo("1");
     assertThat(lastCommentAddedEvent.comment).isEqualTo(
         String.format("Patch Set 2: %s+1", label.getName()));
   }
@@ -155,58 +166,55 @@
     // review with message only, do not apply votes
     ReviewInput reviewInput = new ReviewInput().message(label.getName());
     revision(r).review(reviewInput);
-    // reply message only so votes are excluded from comment
-    assertThat(lastCommentAddedEvent.approvals.get()).isNull();
+    // reply message only so vote is shown as 0
+    ApprovalAttribute attr = getApprovalAttribute(label);
+    assertThat(attr.oldValue).isNull();
+    assertThat(attr.value).isEqualTo("0");
     assertThat(lastCommentAddedEvent.comment).isEqualTo(
         String.format("Patch Set 1:\n\n%s", label.getName()));
 
     // transition from un-voted to -1 vote
     reviewInput = new ReviewInput().label(label.getName(), -1);
     revision(r).review(reviewInput);
-    String newVote = lastCommentAddedEvent.approvals.get()[0].value;
-    String oldVote = lastCommentAddedEvent.approvals.get()[0].oldValue;
-    assertThat(oldVote).isEqualTo("0");
-    assertThat(newVote).isEqualTo("-1");
+    attr = getApprovalAttribute(label);
+    assertThat(attr.oldValue).isEqualTo("0");
+    assertThat(attr.value).isEqualTo("-1");
     assertThat(lastCommentAddedEvent.comment).isEqualTo(
         String.format("Patch Set 1: %s-1", label.getName()));
 
     // transition vote from -1 to 0
     reviewInput = new ReviewInput().label(label.getName(), 0);
     revision(r).review(reviewInput);
-    newVote = lastCommentAddedEvent.approvals.get()[0].value;
-    oldVote = lastCommentAddedEvent.approvals.get()[0].oldValue;
-    assertThat(oldVote).isEqualTo("-1");
-    assertThat(newVote).isEqualTo("0");
+    attr = getApprovalAttribute(label);
+    assertThat(attr.oldValue).isEqualTo("-1");
+    assertThat(attr.value).isEqualTo("0");
     assertThat(lastCommentAddedEvent.comment).isEqualTo(
         String.format("Patch Set 1: -%s", label.getName()));
 
     // transition vote from 0 to 1
     reviewInput = new ReviewInput().label(label.getName(), 1);
     revision(r).review(reviewInput);
-    newVote = lastCommentAddedEvent.approvals.get()[0].value;
-    oldVote = lastCommentAddedEvent.approvals.get()[0].oldValue;
-    assertThat(oldVote).isEqualTo("0");
-    assertThat(newVote).isEqualTo("1");
+    attr = getApprovalAttribute(label);
+    assertThat(attr.oldValue).isEqualTo("0");
+    assertThat(attr.value).isEqualTo("1");
     assertThat(lastCommentAddedEvent.comment).isEqualTo(
         String.format("Patch Set 1: %s+1", label.getName()));
 
     // transition vote from 1 to -1
     reviewInput = new ReviewInput().label(label.getName(), -1);
     revision(r).review(reviewInput);
-    newVote = lastCommentAddedEvent.approvals.get()[0].value;
-    oldVote = lastCommentAddedEvent.approvals.get()[0].oldValue;
-    assertThat(oldVote).isEqualTo("1");
-    assertThat(newVote).isEqualTo("-1");
+    attr = getApprovalAttribute(label);
+    assertThat(attr.oldValue).isEqualTo("1");
+    assertThat(attr.value).isEqualTo("-1");
     assertThat(lastCommentAddedEvent.comment).isEqualTo(
         String.format("Patch Set 1: %s-1", label.getName()));
 
     // review with message only, do not apply votes
     reviewInput = new ReviewInput().message(label.getName());
     revision(r).review(reviewInput);
-    newVote = lastCommentAddedEvent.approvals.get()[0].value;
-    oldVote = lastCommentAddedEvent.approvals.get()[0].oldValue;
-    assertThat(oldVote).isEqualTo(null);  // no vote change so not included
-    assertThat(newVote).isEqualTo("-1");
+    attr = getApprovalAttribute(label);
+    assertThat(attr.oldValue).isEqualTo(null);  // no vote change so not included
+    assertThat(attr.value).isEqualTo("-1");
     assertThat(lastCommentAddedEvent.comment).isEqualTo(
         String.format("Patch Set 1:\n\n%s", label.getName()));
   }
@@ -222,10 +230,27 @@
     ChangeInfo c = get(r.getChangeId());
     LabelInfo q = c.labels.get(label.getName());
     assertThat(q.all).hasSize(1);
+    ApprovalAttribute labelAttr = getApprovalAttribute(label);
+    assertThat(labelAttr.oldValue).isEqualTo("0");
+    assertThat(labelAttr.value).isEqualTo("-1");
     assertThat(lastCommentAddedEvent.comment).isEqualTo(
         String.format("Patch Set 1: %s-1\n\n%s",
             label.getName(), label.getName()));
 
+    // there should be 3 approval labels (label, pLabel, and CRVV)
+    assertThat(lastCommentAddedEvent.approvals.get()).hasLength(3);
+
+    // check the approvals that were not voted on
+    ApprovalAttribute pLabelAttr = getApprovalAttribute(pLabel);
+    assertThat(pLabelAttr.oldValue).isNull();
+    assertThat(pLabelAttr.value).isEqualTo("0");
+
+    LabelType crLabel = LabelType.withDefaultValues("Code-Review");
+    ApprovalAttribute crlAttr = getApprovalAttribute(crLabel);
+    assertThat(crlAttr.oldValue).isNull();
+    assertThat(crlAttr.value).isEqualTo("0");
+
+    // update pLabel approval
     reviewInput = new ReviewInput().label(pLabel.getName(), 1);
     reviewInput.message = pLabel.getName();
     revision(r).review(reviewInput);
@@ -233,21 +258,20 @@
     c = get(r.getChangeId());
     q = c.labels.get(label.getName());
     assertThat(q.all).hasSize(1);
+    pLabelAttr = getApprovalAttribute(pLabel);
+    assertThat(pLabelAttr.oldValue).isEqualTo("0");
+    assertThat(pLabelAttr.value).isEqualTo("1");
     assertThat(lastCommentAddedEvent.comment).isEqualTo(
         String.format("Patch Set 1: %s+1\n\n%s",
             pLabel.getName(), pLabel.getName()));
 
-    assertThat(lastCommentAddedEvent.approvals.get()).hasLength(2);
-    for (ApprovalAttribute approval : lastCommentAddedEvent.approvals.get()) {
-      if (approval.type.equals(label.getName())) {
-        assertThat(approval.value).isEqualTo("-1");
-        assertThat(approval.oldValue).isNull();
-      } else if (approval.type.equals(pLabel.getName())) {
-        assertThat(approval.value).isEqualTo("1");
-        assertThat(approval.oldValue).isEqualTo("0");
-      } else {
-        fail("Unexpected label: " + approval.type);
-      }
-    }
+    // check the approvals that were not voted on
+    labelAttr = getApprovalAttribute(label);
+    assertThat(labelAttr.oldValue).isNull();
+    assertThat(labelAttr.value).isEqualTo("-1");
+
+    crlAttr = getApprovalAttribute(crLabel);
+    assertThat(crlAttr.oldValue).isNull();
+    assertThat(crlAttr.value).isEqualTo("0");
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
index 1df074e..80a349b 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
@@ -16,26 +16,42 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
 
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.api.changes.DraftInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+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.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.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.PatchLineCommentsUtil;
+import com.google.gerrit.server.change.PostReview;
 import com.google.gerrit.server.change.Rebuild;
+import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.RepoRefCache;
 import com.google.gerrit.server.notedb.ChangeBundle;
-import com.google.gerrit.server.notedb.ChangeNoteUtil;
+import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.NoteDbChangeState;
 import com.google.gerrit.server.schema.DisabledChangesReviewDbWrapper;
 import com.google.gerrit.testutil.NoteDbChecker;
 import com.google.gerrit.testutil.NoteDbMode;
+import com.google.gerrit.testutil.TestChanges;
 import com.google.gerrit.testutil.TestTimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -47,7 +63,10 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import java.sql.Timestamp;
 import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.concurrent.TimeUnit;
 
 public class ChangeRebuilderIT extends AbstractDaemonTest {
@@ -66,11 +85,14 @@
   @Inject
   private PatchLineCommentsUtil plcUtil;
 
+  @Inject
+  private Provider<PostReview> postReview;
+
   @Before
   public void setUp() {
     assume().that(NoteDbMode.readWrite()).isFalse();
     TestTimeUtil.resetWithClockStep(1, TimeUnit.SECONDS);
-    notesMigration.setAllEnabled(false);
+    setNotesMigration(false, false);
   }
 
   @After
@@ -78,6 +100,12 @@
     TestTimeUtil.useSystemTime();
   }
 
+  private void setNotesMigration(boolean writeChanges, boolean readChanges) {
+    notesMigration.setWriteChanges(writeChanges);
+    notesMigration.setReadChanges(readChanges);
+    db = atrScope.reopenDb().getReviewDbProvider().get();
+  }
+
   @Test
   public void changeFields() throws Exception {
     PushOneCommit.Result r = createChange();
@@ -95,12 +123,113 @@
   }
 
   @Test
+  public void publishedComment() throws Exception {
+    PushOneCommit.Result r = createChange();
+    Change.Id id = r.getPatchSetId().getParentKey();
+    putComment(user, id, 1, "comment");
+    checker.rebuildAndCheckChanges(id);
+  }
+
+  @Test
+  public void patchSetWithNullGroups() throws Exception {
+    Timestamp ts = TimeUtil.nowTs();
+    @SuppressWarnings("deprecation")
+    Change c = TestChanges.newChange(project, user.getId(), db.nextChangeId());
+    c.setCreatedOn(ts);
+    c.setLastUpdatedOn(ts);
+    PatchSet ps = TestChanges.newPatchSet(
+        c.currentPatchSetId(), "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
+        user.getId());
+    ps.setCreatedOn(ts);
+    db.changes().insert(Collections.singleton(c));
+    db.patchSets().insert(Collections.singleton(ps));
+
+    assertThat(ps.getGroups()).isEmpty();
+    checker.rebuildAndCheckChanges(c.getId());
+  }
+
+  @Test
+  public void draftComment() throws Exception {
+    PushOneCommit.Result r = createChange();
+    Change.Id id = r.getPatchSetId().getParentKey();
+    putDraft(user, id, 1, "comment");
+    checker.rebuildAndCheckChanges(id);
+  }
+
+  @Test
+  public void draftAndPublishedComment() throws Exception {
+    PushOneCommit.Result r = createChange();
+    Change.Id id = r.getPatchSetId().getParentKey();
+    putDraft(user, id, 1, "draft comment");
+    putComment(user, id, 1, "published comment");
+    checker.rebuildAndCheckChanges(id);
+  }
+
+  @Test
+  public void publishDraftComment() throws Exception {
+    PushOneCommit.Result r = createChange();
+    Change.Id id = r.getPatchSetId().getParentKey();
+    putDraft(user, id, 1, "draft comment");
+    publishDrafts(user, id);
+    checker.rebuildAndCheckChanges(id);
+  }
+
+  @Test
+  public void nullAccountId() throws Exception {
+    PushOneCommit.Result r = createChange();
+    PatchSet.Id psId = r.getPatchSetId();
+    Change.Id id = psId.getParentKey();
+
+    // Events need to be otherwise identical for the account ID to be compared.
+    ChangeMessage msg1 =
+        insertMessage(id, psId, user.getId(), TimeUtil.nowTs(), "message 1");
+    insertMessage(id, psId, null, msg1.getWrittenOn(), "message 2");
+
+    checker.rebuildAndCheckChanges(id);
+  }
+
+  @Test
+  public void nullPatchSetId() throws Exception {
+    PushOneCommit.Result r = createChange();
+    PatchSet.Id psId1 = r.getPatchSetId();
+    Change.Id id = psId1.getParentKey();
+
+    // Events need to be otherwise identical for the PatchSet.ID to be compared.
+    ChangeMessage msg1 =
+        insertMessage(id, null, user.getId(), TimeUtil.nowTs(), "message 1");
+    insertMessage(id, null, user.getId(), msg1.getWrittenOn(), "message 2");
+
+    PatchSet.Id psId2 = amendChange(r.getChangeId()).getPatchSetId();
+
+    ChangeMessage msg3 =
+        insertMessage(id, null, user.getId(), TimeUtil.nowTs(), "message 3");
+    insertMessage(id, null, user.getId(), msg3.getWrittenOn(), "message 4");
+
+    checker.rebuildAndCheckChanges(id);
+
+    setNotesMigration(true, true);
+
+    ChangeNotes notes = notesFactory.create(db, project, id);
+    Map<String, PatchSet.Id> psIds = new HashMap<>();
+    for (ChangeMessage msg : notes.getChangeMessages()) {
+      PatchSet.Id psId = msg.getPatchSetId();
+      assertThat(psId).named("patchset for " + msg).isNotNull();
+      psIds.put(msg.getMessage(), psId);
+    }
+    // Patch set IDs were replaced during conversion process.
+    assertThat(psIds).containsEntry("message 1", psId1);
+    assertThat(psIds).containsEntry("message 2", psId1);
+    assertThat(psIds).containsEntry("message 3", psId2);
+    assertThat(psIds).containsEntry("message 4", psId2);
+  }
+
+  @Test
   public void noWriteToNewRef() throws Exception {
     PushOneCommit.Result r = createChange();
     Change.Id id = r.getPatchSetId().getParentKey();
     checker.assertNoChangeRef(project, id);
 
-    notesMigration.setWriteChanges(true);
+    setNotesMigration(true, false);
     gApi.changes().id(id.get()).topic(name("a-topic"));
 
     // First write doesn't create the ref, but rebuilding works.
@@ -128,7 +257,7 @@
   public void rebuildViaRestApi() throws Exception {
     PushOneCommit.Result r = createChange();
     Change.Id id = r.getPatchSetId().getParentKey();
-    notesMigration.setWriteChanges(true);
+    setNotesMigration(true, false);
 
     checker.assertNoChangeRef(project, id);
     rebuildHandler.apply(
@@ -142,7 +271,7 @@
     PushOneCommit.Result r1 = createChange();
     Change.Id id1 = r1.getPatchSetId().getParentKey();
 
-    notesMigration.setWriteChanges(true);
+    setNotesMigration(true, false);
     gApi.changes().id(id1.get()).topic(name("a-topic"));
     PushOneCommit.Result r2 = createChange();
     Change.Id id2 = r2.getPatchSetId().getParentKey();
@@ -159,25 +288,24 @@
 
   @Test
   public void noteDbChangeState() throws Exception {
-    notesMigration.setAllEnabled(true);
+    setNotesMigration(true, true);
     PushOneCommit.Result r = createChange();
     Change.Id id = r.getPatchSetId().getParentKey();
 
-    ObjectId changeMetaId = getMetaRef(
-        project, ChangeNoteUtil.changeRefName(id));
+    ObjectId changeMetaId = getMetaRef(project, changeMetaRef(id));
     assertThat(unwrapDb().changes().get(id).getNoteDbState()).isEqualTo(
         changeMetaId.name());
 
     putDraft(user, id, 1, "comment by user");
     ObjectId userDraftsId = getMetaRef(
-        allUsers, RefNames.refsDraftComments(user.getId(), id));
+        allUsers, RefNames.refsDraftComments(id, user.getId()));
     assertThat(unwrapDb().changes().get(id).getNoteDbState()).isEqualTo(
         changeMetaId.name()
         + "," + user.getId() + "=" + userDraftsId.name());
 
     putDraft(admin, id, 2, "comment by admin");
     ObjectId adminDraftsId = getMetaRef(
-        allUsers, RefNames.refsDraftComments(admin.getId(), id));
+        allUsers, RefNames.refsDraftComments(id, admin.getId()));
     assertThat(admin.getId().get()).isLessThan(user.getId().get());
     assertThat(unwrapDb().changes().get(id).getNoteDbState()).isEqualTo(
         changeMetaId.name()
@@ -186,7 +314,7 @@
 
     putDraft(admin, id, 2, "revised comment by admin");
     adminDraftsId = getMetaRef(
-        allUsers, RefNames.refsDraftComments(admin.getId(), id));
+        allUsers, RefNames.refsDraftComments(id, admin.getId()));
     assertThat(unwrapDb().changes().get(id).getNoteDbState()).isEqualTo(
         changeMetaId.name()
         + "," + admin.getId() + "=" + adminDraftsId.name()
@@ -195,20 +323,20 @@
 
   @Test
   public void rebuildAutomaticallyWhenChangeOutOfDate() throws Exception {
-    notesMigration.setAllEnabled(true);
+    setNotesMigration(true, true);
 
     PushOneCommit.Result r = createChange();
     Change.Id id = r.getPatchSetId().getParentKey();
     assertChangeUpToDate(true, id);
 
     // Make a ReviewDb change behind NoteDb's back and ensure it's detected.
-    notesMigration.setAllEnabled(false);
+    setNotesMigration(false, false);
     gApi.changes().id(id.get()).topic(name("a-topic"));
     setInvalidNoteDbState(id);
     assertChangeUpToDate(false, id);
 
     // On next NoteDb read, the change is transparently rebuilt.
-    notesMigration.setAllEnabled(true);
+    setNotesMigration(true, true);
     assertThat(gApi.changes().id(id.get()).info().topic)
         .isEqualTo(name("a-topic"));
     assertChangeUpToDate(true, id);
@@ -222,7 +350,7 @@
 
   @Test
   public void rebuildAutomaticallyWhenDraftsOutOfDate() throws Exception {
-    notesMigration.setAllEnabled(true);
+    setNotesMigration(true, true);
     setApiUser(user);
 
     PushOneCommit.Result r = createChange();
@@ -231,18 +359,250 @@
     assertDraftsUpToDate(true, id, user);
 
     // Make a ReviewDb change behind NoteDb's back and ensure it's detected.
-    notesMigration.setAllEnabled(false);
+    setNotesMigration(false, false);
     putDraft(user, id, 1, "comment");
     setInvalidNoteDbState(id);
     assertDraftsUpToDate(false, id, user);
 
     // On next NoteDb read, the drafts are transparently rebuilt.
-    notesMigration.setAllEnabled(true);
+    setNotesMigration(true, true);
     assertThat(gApi.changes().id(id.get()).current().drafts())
         .containsKey(PushOneCommit.FILE_NAME);
     assertDraftsUpToDate(true, id, user);
   }
 
+  @Test
+  public void pushCert() throws Exception {
+    // We don't have the code in our test harness to do signed pushes, so just
+    // use a hard-coded cert. This cert was actually generated by C git 2.2.0
+    // (albeit not for sending to Gerrit).
+    String cert = "certificate version 0.1\n"
+        + "pusher Dave Borowitz <dborowitz@google.com> 1433954361 -0700\n"
+        + "pushee git://localhost/repo.git\n"
+        + "nonce 1433954361-bde756572d665bba81d8\n"
+        + "\n"
+        + "0000000000000000000000000000000000000000"
+        + "b981a177396fb47345b7df3e4d3f854c6bea7"
+        + "s/heads/master\n"
+        + "-----BEGIN PGP SIGNATURE-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "iQEcBAABAgAGBQJVeGg5AAoJEPfTicJkUdPkUggH/RKAeI9/i/LduuiqrL/SSdIa\n"
+        + "9tYaSqJKLbXz63M/AW4Sp+4u+dVCQvnAt/a35CVEnpZz6hN4Kn/tiswOWVJf4CO7\n"
+        + "htNubGs5ZMwvD6sLYqKAnrM3WxV/2TbbjzjZW6Jkidz3jz/WRT4SmjGYiEO7aA+V\n"
+        + "4ZdIS9f7sW5VsHHYlNThCA7vH8Uu48bUovFXyQlPTX0pToSgrWV3JnTxDNxfn3iG\n"
+        + "IL0zTY/qwVCdXgFownLcs6J050xrrBWIKqfcWr3u4D2aCLyR0v+S/KArr7ulZygY\n"
+        + "+SOklImn8TAZiNxhWtA6ens66IiammUkZYFv7SSzoPLFZT4dC84SmGPWgf94NoQ=\n"
+        + "=XFeC\n"
+        + "-----END PGP SIGNATURE-----\n";
+
+    PushOneCommit.Result r = createChange();
+    PatchSet.Id psId = r.getPatchSetId();
+    Change.Id id = psId.getParentKey();
+
+    PatchSet ps = db.patchSets().get(psId);
+    ps.setPushCertificate(cert);
+    db.patchSets().update(Collections.singleton(ps));
+    indexer.index(db, project, id);
+
+    checker.rebuildAndCheckChanges(id);
+  }
+
+  @Test
+  public void emptyTopic() throws Exception {
+    PushOneCommit.Result r = createChange();
+    Change.Id id = r.getPatchSetId().getParentKey();
+    Change c = db.changes().get(id);
+    assertThat(c.getTopic()).isNull();
+    c.setTopic("");
+    db.changes().update(Collections.singleton(c));
+
+    checker.rebuildAndCheckChanges(id);
+
+    setNotesMigration(true, true);
+
+    // Rebuild and check was successful, but NoteDb doesn't support storing an
+    // empty topic, so it comes out as null.
+    ChangeNotes notes = notesFactory.create(db, project, id);
+    assertThat(notes.getChange().getTopic()).isNull();
+  }
+
+  @Test
+  public void commentBeforeFirstPatchSet() throws Exception {
+    PushOneCommit.Result r = createChange();
+    PatchSet.Id psId = r.getPatchSetId();
+    Change.Id id = psId.getParentKey();
+
+    Change c = db.changes().get(id);
+    c.setCreatedOn(new Timestamp(c.getCreatedOn().getTime() - 5000));
+    db.changes().update(Collections.singleton(c));
+    indexer.index(db, project, id);
+
+    ReviewInput rin = new ReviewInput();
+    rin.message = "comment";
+
+    Timestamp ts = new Timestamp(c.getCreatedOn().getTime() + 2000);
+    RevisionResource revRsrc = parseCurrentRevisionResource(r.getChangeId());
+    postReview.get().apply(revRsrc, rin, ts);
+
+    checker.rebuildAndCheckChanges(id);
+  }
+
+  @Test
+  public void commentPredatingChangeBySomeoneOtherThanOwner() throws Exception {
+    PushOneCommit.Result r = createChange();
+    PatchSet.Id psId = r.getPatchSetId();
+    Change.Id id = psId.getParentKey();
+    Change c = db.changes().get(id);
+
+    ReviewInput rin = new ReviewInput();
+    rin.message = "comment";
+
+    Timestamp ts = new Timestamp(c.getCreatedOn().getTime() - 10000);
+    RevisionResource revRsrc = parseCurrentRevisionResource(r.getChangeId());
+    setApiUser(user);
+    postReview.get().apply(revRsrc, rin, ts);
+
+    checker.rebuildAndCheckChanges(id);
+  }
+
+  @Test
+  public void noteDbUsesOriginalSubjectFromPatchSetAndIgnoresChangeField()
+      throws Exception {
+    PushOneCommit.Result r = createChange();
+    String orig = r.getChange().change().getSubject();
+    r = pushFactory.create(
+            db, admin.getIdent(), testRepo, orig + " v2",
+            PushOneCommit.FILE_NAME, "new contents", r.getChangeId())
+        .to("refs/heads/master");
+    r.assertOkStatus();
+
+    PatchSet.Id psId = r.getPatchSetId();
+    Change.Id id = psId.getParentKey();
+    Change c = db.changes().get(id);
+
+    c.setCurrentPatchSet(psId, c.getSubject(), "Bogus original subject");
+    db.changes().update(Collections.singleton(c));
+
+    checker.rebuildAndCheckChanges(id);
+
+    setNotesMigration(true, true);
+    ChangeNotes notes = notesFactory.create(db, project, id);
+    Change nc = notes.getChange();
+    assertThat(nc.getSubject()).isEqualTo(c.getSubject());
+    assertThat(nc.getSubject()).isEqualTo(orig + " v2");
+    assertThat(nc.getOriginalSubject()).isNotEqualTo(c.getOriginalSubject());
+    assertThat(nc.getOriginalSubject()).isEqualTo(orig);
+  }
+
+  @Test
+  public void deleteDraftPS1WithNoOtherEntities() throws Exception {
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit.Result r = push.to("refs/drafts/master");
+    push = pushFactory.create(db, admin.getIdent(), testRepo,
+        PushOneCommit.SUBJECT, "b.txt", "4711", r.getChangeId());
+    r = push.to("refs/drafts/master");
+    PatchSet.Id psId = r.getPatchSetId();
+    Change.Id id = psId.getParentKey();
+
+    gApi.changes().id(r.getChangeId()).revision(1).delete();
+
+    checker.rebuildAndCheckChanges(id);
+
+    setNotesMigration(true, true);
+    ChangeNotes notes = notesFactory.create(db, project, id);
+    assertThat(notes.getPatchSets().keySet()).containsExactly(psId);
+  }
+
+  @Test
+  public void ignorePatchLineCommentsOnPatchSet0() throws Exception {
+    PushOneCommit.Result r = createChange();
+    Change change = r.getChange().change();
+    Change.Id id = change.getId();
+
+    PatchLineComment comment = new PatchLineComment(
+        new PatchLineComment.Key(
+            new Patch.Key(new PatchSet.Id(id, 0), PushOneCommit.FILE_NAME),
+            "uuid"),
+        0, user.getId(), null, TimeUtil.nowTs());
+    comment.setSide((short) 1);
+    comment.setMessage("message");
+    comment.setStatus(PatchLineComment.Status.PUBLISHED);
+    db.patchComments().insert(Collections.singleton(comment));
+    indexer.index(db, change.getProject(), id);
+
+    checker.rebuildAndCheckChanges(id);
+
+    setNotesMigration(true, true);
+    ChangeNotes notes = notesFactory.create(db, project, id);
+    assertThat(notes.getComments()).isEmpty();
+  }
+
+  @Test
+  public void skipPatchSetsGreaterThanCurrentPatchSet() throws Exception {
+    PushOneCommit.Result r = createChange();
+    Change change = r.getChange().change();
+    Change.Id id = change.getId();
+
+    PatchSet badPs =
+        new PatchSet(new PatchSet.Id(id, change.currentPatchSetId().get() + 1));
+    badPs.setCreatedOn(TimeUtil.nowTs());
+    badPs.setUploader(new Account.Id(12345));
+    badPs.setRevision(new RevId("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
+    db.patchSets().insert(Collections.singleton(badPs));
+    indexer.index(db, change.getProject(), id);
+
+    checker.rebuildAndCheckChanges(id);
+
+    setNotesMigration(true, true);
+    ChangeNotes notes = notesFactory.create(db, project, id);
+    assertThat(notes.getPatchSets().keySet())
+        .containsExactly(change.currentPatchSetId());
+  }
+
+  @Test
+  public void leadingSpacesInSubject() throws Exception {
+    String subj = "   " + PushOneCommit.SUBJECT;
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo,
+        subj, PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT);
+    PushOneCommit.Result r = push.to("refs/for/master");
+    r.assertOkStatus();
+    Change change = r.getChange().change();
+    assertThat(change.getSubject()).isEqualTo(subj);
+    Change.Id id = r.getPatchSetId().getParentKey();
+
+    checker.rebuildAndCheckChanges(id);
+
+    setNotesMigration(true, true);
+    ChangeNotes notes = notesFactory.create(db, project, id);
+    assertThat(notes.getChange().getSubject()).isNotEqualTo(subj);
+    assertThat(notes.getChange().getSubject()).isEqualTo(PushOneCommit.SUBJECT);
+  }
+
+  @Test
+  public void createWithAutoRebuildingDisabled() throws Exception {
+    ReviewDb oldDb = db;
+    setNotesMigration(true, true);
+
+    PushOneCommit.Result r = createChange();
+    Change.Id id = r.getPatchSetId().getParentKey();
+    ChangeNotes oldNotes = notesFactory.create(db, project, id);
+
+    // Make a ReviewDb change behind NoteDb's back.
+    Change c = oldDb.changes().get(id);
+    assertThat(c.getTopic()).isNull();
+    String topic = name("a-topic");
+    c.setTopic(topic);
+    oldDb.changes().update(Collections.singleton(c));
+
+    c = oldDb.changes().get(c.getId());
+    ChangeNotes newNotes =
+        notesFactory.createWithAutoRebuildingDisabled(c, null);
+    assertThat(newNotes.getChange().getTopic()).isNotEqualTo(topic);
+    assertThat(newNotes.getChange().getTopic())
+        .isEqualTo(oldNotes.getChange().getTopic());
+  }
+
   private void setInvalidNoteDbState(Change.Id id) throws Exception {
     ReviewDb db = unwrapDb();
     Change c = db.changes().get(id);
@@ -256,29 +616,31 @@
 
   private void assertChangeUpToDate(boolean expected, Change.Id id)
       throws Exception {
-    try (Repository repo = repoManager.openMetadataRepository(project)) {
+    try (Repository repo = repoManager.openRepository(project)) {
       Change c = unwrapDb().changes().get(id);
       assertThat(c).isNotNull();
       assertThat(c.getNoteDbState()).isNotNull();
-      assertThat(NoteDbChangeState.parse(c).isChangeUpToDate(repo))
+      assertThat(NoteDbChangeState.parse(c).isChangeUpToDate(
+              new RepoRefCache(repo)))
           .isEqualTo(expected);
     }
   }
 
   private void assertDraftsUpToDate(boolean expected, Change.Id changeId,
       TestAccount account) throws Exception {
-    try (Repository repo = repoManager.openMetadataRepository(allUsers)) {
+    try (Repository repo = repoManager.openRepository(allUsers)) {
       Change c = unwrapDb().changes().get(changeId);
       assertThat(c).isNotNull();
       assertThat(c.getNoteDbState()).isNotNull();
       NoteDbChangeState state = NoteDbChangeState.parse(c);
-      assertThat(state.areDraftsUpToDate(repo, account.getId()))
+      assertThat(state.areDraftsUpToDate(
+              new RepoRefCache(repo), account.getId()))
           .isEqualTo(expected);
     }
   }
 
   private ObjectId getMetaRef(Project.NameKey p, String name) throws Exception {
-    try (Repository repo = repoManager.openMetadataRepository(p)) {
+    try (Repository repo = repoManager.openRepository(p)) {
       Ref ref = repo.exactRef(name);
       return ref != null ? ref.getObjectId() : null;
     }
@@ -298,6 +660,52 @@
     }
   }
 
+  private void putComment(TestAccount account, Change.Id id, int line, String msg)
+      throws Exception {
+    CommentInput in = new CommentInput();
+    in.line = line;
+    in.message = msg;
+    ReviewInput rin = new ReviewInput();
+    rin.comments = new HashMap<>();
+    rin.comments.put(PushOneCommit.FILE_NAME, ImmutableList.of(in));
+    rin.drafts = ReviewInput.DraftHandling.KEEP;
+    AcceptanceTestRequestScope.Context old = setApiUser(account);
+    try {
+      gApi.changes().id(id.get()).current().review(rin);
+    } finally {
+      atrScope.set(old);
+    }
+  }
+
+  private void publishDrafts(TestAccount account, Change.Id id)
+      throws Exception {
+    ReviewInput rin = new ReviewInput();
+    rin.drafts = ReviewInput.DraftHandling.PUBLISH_ALL_REVISIONS;
+    AcceptanceTestRequestScope.Context old = setApiUser(account);
+    try {
+      gApi.changes().id(id.get()).current().review(rin);
+    } finally {
+      atrScope.set(old);
+    }
+  }
+
+  private ChangeMessage insertMessage(Change.Id id, PatchSet.Id psId,
+      Account.Id author, Timestamp ts, String message) throws Exception {
+    ChangeMessage msg = new ChangeMessage(
+        new ChangeMessage.Key(id, ChangeUtil.messageUUID(db)),
+        author, ts, psId);
+    msg.setMessage(message);
+    db.changeMessages().insert(Collections.singleton(msg));
+
+    Change c = db.changes().get(id);
+    if (ts.compareTo(c.getLastUpdatedOn()) > 0) {
+      c.setLastUpdatedOn(ts);
+      db.changes().update(Collections.singleton(c));
+    }
+
+    return msg;
+  }
+
   private ReviewDb unwrapDb() {
     ReviewDb db = dbProvider.get();
     if (db instanceof DisabledChangesReviewDbWrapper) {
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 a22b09d..a69bb19 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
@@ -26,7 +26,6 @@
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.LabelInfo;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.project.Util;
 
@@ -416,12 +415,6 @@
     saveProjectConfig(cfg);
   }
 
-  private void saveProjectConfig(ProjectConfig cfg) throws Exception {
-    try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
-      cfg.commit(md);
-    }
-  }
-
   @Override
   protected void merge(PushOneCommit.Result r) throws Exception {
     super.merge(r);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/AbandonRestoreIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/AbandonRestoreIT.java
index e07405f..56a56ee 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/AbandonRestoreIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/AbandonRestoreIT.java
@@ -66,10 +66,10 @@
     if (message != null) {
       command.append(" --message ").append(message);
     }
-    String response = sshSession.exec(command.toString());
+    String response = adminSshSession.exec(command.toString());
     assert_()
-      .withFailureMessage(sshSession.getError())
-      .that(sshSession.hasError())
+      .withFailureMessage(adminSshSession.getError())
+      .that(adminSshSession.hasError())
       .isFalse();
     assertThat(response.toLowerCase(Locale.US)).doesNotContain("error");
   }
@@ -86,4 +86,4 @@
     }
     assertThat(actual).containsExactlyElementsIn(expected);
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BanCommitIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BanCommitIT.java
index ab2dd81..025fcfa 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BanCommitIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BanCommitIT.java
@@ -38,9 +38,9 @@
         .create();
 
     String response =
-        sshSession.exec("gerrit ban-commit " + project.get() + " " + c.name());
-    assert_().withFailureMessage(sshSession.getError())
-        .that(sshSession.hasError()).isFalse();
+        adminSshSession.exec("gerrit ban-commit " + project.get() + " " + c.name());
+    assert_().withFailureMessage(adminSshSession.getError())
+        .that(adminSshSession.hasError()).isFalse();
     assertThat(response.toLowerCase(Locale.US)).doesNotContain("error");
 
     RemoteRefUpdate u = pushHead(testRepo, "refs/heads/master", false)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/CreateGroupIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/CreateGroupIT.java
index 3fb7456..a090538 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/CreateGroupIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/CreateGroupIT.java
@@ -28,10 +28,10 @@
   public void withDuplicateInternalGroupCaseSensitiveName_Conflict()
       throws Exception {
     String newGroupName = "dupGroupA";
-    adminSession.put("/groups/" + newGroupName);
-    sshSession.exec("gerrit create-group " + newGroupName);
-    assert_().withFailureMessage(sshSession.getError())
-        .that(sshSession.hasError()).isTrue();
+    adminRestSession.put("/groups/" + newGroupName);
+    adminSshSession.exec("gerrit create-group " + newGroupName);
+    assert_().withFailureMessage(adminSshSession.getError())
+        .that(adminSshSession.hasError()).isTrue();
   }
 
   @Test
@@ -40,10 +40,10 @@
     String newGroupName = "dupGroupB";
     String newGroupNameLowerCase = newGroupName.toLowerCase();
 
-    adminSession.put("/groups/" + newGroupName);
-    sshSession.exec("gerrit create-group " + newGroupNameLowerCase);
-    assert_().withFailureMessage(sshSession.getError())
-        .that(sshSession.hasError()).isFalse();
+    adminRestSession.put("/groups/" + newGroupName);
+    adminSshSession.exec("gerrit create-group " + newGroupNameLowerCase);
+    assert_().withFailureMessage(adminSshSession.getError())
+        .that(adminSshSession.hasError()).isFalse();
     assertThat(groupCache.get(new AccountGroup.NameKey(newGroupName)))
       .isNotNull();
     assertThat(groupCache.get(new AccountGroup.NameKey(newGroupNameLowerCase)))
@@ -54,26 +54,26 @@
   public void withDuplicateSystemGroupCaseSensitiveName_Conflict()
       throws Exception {
     String newGroupName = "Registered Users";
-    sshSession.exec("gerrit create-group " + newGroupName);
-    assert_().withFailureMessage(sshSession.getError())
-        .that(sshSession.hasError()).isTrue();
+    adminSshSession.exec("gerrit create-group " + newGroupName);
+    assert_().withFailureMessage(adminSshSession.getError())
+        .that(adminSshSession.hasError()).isTrue();
   }
 
   @Test
   public void withDuplicateSystemGroupCaseInsensitiveName_Conflict()
       throws Exception {
     String newGroupName = "Registered Users";
-    sshSession.exec("gerrit create-group " + newGroupName);
-    assert_().withFailureMessage(sshSession.getError())
-        .that(sshSession.hasError()).isTrue();
+    adminSshSession.exec("gerrit create-group " + newGroupName);
+    assert_().withFailureMessage(adminSshSession.getError())
+        .that(adminSshSession.hasError()).isTrue();
   }
 
   @Test
   public void withNonDuplicateGroupName() throws Exception {
     String newGroupName = "newGroupB";
-    sshSession.exec("gerrit create-group " + newGroupName);
-    assert_().withFailureMessage(sshSession.getError())
-        .that(sshSession.hasError()).isFalse();
+    adminSshSession.exec("gerrit create-group " + newGroupName);
+    assert_().withFailureMessage(adminSshSession.getError())
+        .that(adminSshSession.hasError()).isFalse();
     AccountGroup accountGroup =
         groupCache.get(new AccountGroup.NameKey(newGroupName));
     assertThat(accountGroup).isNotNull();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/CreateProjectIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/CreateProjectIT.java
index a779136..85d460e 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/CreateProjectIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/CreateProjectIT.java
@@ -28,12 +28,12 @@
   @Test
   public void withValidGroupName() throws Exception {
     String newGroupName = "newGroup";
-    adminSession.put("/groups/" + newGroupName);
+    adminRestSession.put("/groups/" + newGroupName);
     String newProjectName = "newProject";
-    sshSession.exec("gerrit create-project --branch master --owner "
+    adminSshSession.exec("gerrit create-project --branch master --owner "
         + newGroupName + " " + newProjectName);
-    assert_().withFailureMessage(sshSession.getError())
-        .that(sshSession.hasError()).isFalse();
+    assert_().withFailureMessage(adminSshSession.getError())
+        .that(adminSshSession.hasError()).isFalse();
     ProjectState projectState =
         projectCache.get(new Project.NameKey(newProjectName));
     assertThat(projectState).isNotNull();
@@ -42,13 +42,13 @@
   @Test
   public void withInvalidGroupName() throws Exception {
     String newGroupName = "newGroup";
-    adminSession.put("/groups/" + newGroupName);
+    adminRestSession.put("/groups/" + newGroupName);
     String wrongGroupName = "newG";
     String newProjectName = "newProject";
-    sshSession.exec("gerrit create-project --branch master --owner "
+    adminSshSession.exec("gerrit create-project --branch master --owner "
         + wrongGroupName + " " + newProjectName);
-    assert_().withFailureMessage(sshSession.getError())
-        .that(sshSession.hasError()).isTrue();
+    assert_().withFailureMessage(adminSshSession.getError())
+        .that(adminSshSession.hasError()).isTrue();
     ProjectState projectState =
         projectCache.get(new Project.NameKey(newProjectName));
     assertThat(projectState).isNull();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java
index b2c4df8..7176254 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GcAssert;
 import com.google.gerrit.acceptance.NoHttpd;
-import com.google.gerrit.acceptance.SshSession;
 import com.google.gerrit.acceptance.UseLocalDisk;
 import com.google.gerrit.common.data.GarbageCollectionResult;
 import com.google.gerrit.reviewdb.client.Project;
@@ -59,10 +58,10 @@
   @UseLocalDisk
   public void testGc() throws Exception {
     String response =
-        sshSession.exec("gerrit gc \"" + project.get() + "\" \""
+        adminSshSession.exec("gerrit gc \"" + project.get() + "\" \""
             + project2.get() + "\"");
-    assert_().withFailureMessage(sshSession.getError())
-        .that(sshSession.hasError()).isFalse();
+    assert_().withFailureMessage(adminSshSession.getError())
+        .that(adminSshSession.hasError()).isFalse();
     assertNoError(response);
     gcAssert.assertHasPackFile(project, project2);
     gcAssert.assertHasNoPackFile(allProjects, project3);
@@ -71,20 +70,21 @@
   @Test
   @UseLocalDisk
   public void testGcAll() throws Exception {
-    String response = sshSession.exec("gerrit gc --all");
-    assert_().withFailureMessage(sshSession.getError())
-        .that(sshSession.hasError()).isFalse();
+    String response = adminSshSession.exec("gerrit gc --all");
+    assert_().withFailureMessage(adminSshSession.getError())
+        .that(adminSshSession.hasError()).isFalse();
     assertNoError(response);
     gcAssert.assertHasPackFile(allProjects, project, project2, project3);
   }
 
   @Test
   public void testGcWithoutCapability_Error() throws Exception {
-    SshSession s = new SshSession(server, user);
-    s.exec("gerrit gc --all");
+    userSshSession.exec("gerrit gc --all");
+    assertThat(userSshSession.hasError()).isTrue();
+    String error = userSshSession.getError();
+    assertThat(error).isNotNull();
     assertError("One of the following capabilities is required to access this"
-        + " resource: [runGC, maintainServer]", s.getError());
-    s.close();
+        + " resource: [runGC, maintainServer]", error);
   }
 
   @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/QueryIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/QueryIT.java
index 066d362..2865ff87 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/QueryIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/QueryIT.java
@@ -16,11 +16,13 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.acceptance.GitUtil.initSsh;
 
 import com.google.common.collect.Lists;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.SshSession;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.client.Side;
@@ -150,9 +152,9 @@
   public void shouldFailWithFilesWithoutPatchSetsOrCurrentPatchSetsOption()
       throws Exception {
     String changeId = createChange().getChangeId();
-    sshSession.exec("gerrit query --files " + changeId);
-    assertThat(sshSession.hasError()).isTrue();
-    assertThat(sshSession.getError()).contains(
+    adminSshSession.exec("gerrit query --files " + changeId);
+    assertThat(adminSshSession.hasError()).isTrue();
+    assertThat(adminSshSession.getError()).contains(
         "needs --patch-sets or --current-patch-set");
   }
 
@@ -300,13 +302,39 @@
     assertThat(changes.get(0).submitRecords.size()).isEqualTo(1);
   }
 
+  @Test
+  public void testQueryWithNonVisibleCurrentPatchSet() throws Exception {
+    String changeId = createChange().getChangeId();
+    amendChangeAsDraft(changeId);
+    String query = "--current-patch-set --patch-sets " + changeId;
+    List<ChangeAttribute> changes = executeSuccessfulQuery(query);
+    assertThat(changes.size()).isEqualTo(1);
+    assertThat(changes.get(0).patchSets).isNotNull();
+    assertThat(changes.get(0).patchSets).hasSize(2);
+    assertThat(changes.get(0).currentPatchSet).isNotNull();
+
+    SshSession userSession = new SshSession(server, user);
+    initSsh(user);
+    userSession.open();
+    changes = executeSuccessfulQuery(query, userSession);
+    assertThat(changes.size()).isEqualTo(1);
+    assertThat(changes.get(0).patchSets).hasSize(1);
+    assertThat(changes.get(0).currentPatchSet).isNull();
+    userSession.close();
+  }
+
+  private List<ChangeAttribute> executeSuccessfulQuery(String params,
+      SshSession session) throws Exception {
+    String rawResponse =
+        session.exec("gerrit query --format=JSON " + params);
+    assert_().withFailureMessage(session.getError())
+        .that(session.hasError()).isFalse();
+    return getChanges(rawResponse);
+  }
+
   private List<ChangeAttribute> executeSuccessfulQuery(String params)
       throws Exception {
-    String rawResponse =
-        sshSession.exec("gerrit query --format=JSON " + params);
-    assert_().withFailureMessage(sshSession.getError())
-        .that(sshSession.hasError()).isFalse();
-    return getChanges(rawResponse);
+    return executeSuccessfulQuery(params, adminSshSession);
   }
 
   private static List<ChangeAttribute> getChanges(String rawResponse) {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java
index dc1266e..32a0175 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.ssh;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
 
 import com.google.common.base.Splitter;
 import com.google.common.collect.Iterables;
@@ -22,12 +23,14 @@
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.testutil.NoteDbMode;
 
 import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
 import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
 import org.eclipse.jgit.transport.PacketLineIn;
 import org.eclipse.jgit.transport.PacketLineOut;
 import org.eclipse.jgit.util.IO;
+import org.junit.Before;
 import org.junit.Test;
 
 import java.io.ByteArrayInputStream;
@@ -39,6 +42,13 @@
 @NoHttpd
 public class UploadArchiveIT extends AbstractDaemonTest {
 
+  @Before
+  public void setUp() {
+    // There is some Guice request scoping problem preventing this test from
+    // passing in CHECK mode.
+    assume().that(NoteDbMode.get()).isNotEqualTo(NoteDbMode.CHECK);
+  }
+
   @Test
   @GerritConfig(name = "download.archive", value = "off")
   public void archiveFeatureOff() throws Exception {
@@ -58,7 +68,7 @@
     String c = command(r, abbreviated);
 
     InputStream out =
-        sshSession.exec2("git-upload-archive " + project.get(),
+        adminSshSession.exec2("git-upload-archive " + project.get(),
             argumentsToInputStream(c));
 
     // Wrap with PacketLineIn to read ACK bytes from output stream
@@ -101,7 +111,7 @@
     String c = command(r, abbreviated);
 
     InputStream out =
-        sshSession.exec2("git-upload-archive " + project.get(),
+        adminSshSession.exec2("git-upload-archive " + project.get(),
             argumentsToInputStream(c));
 
     // Wrap with PacketLineIn to read ACK bytes from output stream
diff --git a/gerrit-cache-h2/BUCK b/gerrit-cache-h2/BUCK
index 36f91ec..0bc1cb12 100644
--- a/gerrit-cache-h2/BUCK
+++ b/gerrit-cache-h2/BUCK
@@ -8,8 +8,8 @@
     '//lib:guava',
     '//lib:h2',
     '//lib/guice:guice',
+    '//lib/jgit/org.eclipse.jgit:jgit',
     '//lib/log:api',
-    '@jgit//org.eclipse.jgit:jgit',
   ],
   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 3ffb76c..0b18106 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
@@ -17,7 +17,6 @@
 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;
@@ -40,6 +39,7 @@
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ExecutorService;
@@ -49,7 +49,8 @@
 
 @Singleton
 class H2CacheFactory implements PersistentCacheFactory, LifecycleListener {
-  static final Logger log = LoggerFactory.getLogger(H2CacheFactory.class);
+  private static final Logger log =
+      LoggerFactory.getLogger(H2CacheFactory.class);
 
   private final DefaultCacheFactory defaultFactory;
   private final Config config;
@@ -70,7 +71,7 @@
     config = cfg;
     cacheDir = getCacheDir(site, cfg.getString("cache", null, "directory"));
     h2CacheSize = cfg.getLong("cache", null, "h2CacheSize", -1);
-    caches = Lists.newLinkedList();
+    caches = new LinkedList<>();
     this.cacheMap = cacheMap;
 
     if (cacheDir != null) {
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 289c8cc..726a5f1 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
@@ -490,7 +490,7 @@
 
     private void touch(SqlHandle c, K key) throws SQLException {
       if (c.touch == null) {
-        c.touch =c.conn.prepareStatement("UPDATE data SET accessed=? WHERE k=?");
+        c.touch = c.conn.prepareStatement("UPDATE data SET accessed=? WHERE k=?");
       }
       try {
         c.touch.setTimestamp(1, TimeUtil.nowTs());
diff --git a/gerrit-cache-h2/src/test/java/com/google/gerrit/server/cache/h2/H2CacheTest.java b/gerrit-cache-h2/src/test/java/com/google/gerrit/server/cache/h2/H2CacheTest.java
index 3b7e436..c999d71 100644
--- a/gerrit-cache-h2/src/test/java/com/google/gerrit/server/cache/h2/H2CacheTest.java
+++ b/gerrit-cache-h2/src/test/java/com/google/gerrit/server/cache/h2/H2CacheTest.java
@@ -79,4 +79,4 @@
     }));
     assertFalse("did not invoke Callable", called.get());
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-common/BUCK b/gerrit-common/BUCK
index df6237c..530592c 100644
--- a/gerrit-common/BUCK
+++ b/gerrit-common/BUCK
@@ -6,16 +6,6 @@
   SRC + 'common/auth/SignInRequired.java',
 ]
 
-EXCLUDES = [
-  SRC + 'common/SiteLibraryLoaderUtil.java',
-  SRC + 'common/PluginData.java',
-  SRC + 'common/FileUtil.java',
-  SRC + 'common/IoUtil.java',
-  SRC + 'common/RawInputUtil.java',
-  SRC + 'common/TimeUtil.java',
-  SRC + 'common/data/SubscribeSection.java',
-]
-
 java_library(
   name = 'annotations',
   srcs = ANNOTATIONS,
@@ -24,13 +14,18 @@
 
 gwt_module(
   name = 'client',
-  srcs = glob([SRC + 'common/**/*.java'], excludes = EXCLUDES),
+  srcs = glob([SRC + 'common/**/*.java']),
   gwt_xml = SRC + 'Common.gwt.xml',
   exported_deps = [
-    '//gerrit-extension-api:client',
+    '//gerrit-extension-api:api',
     '//gerrit-prettify:client',
+    '//lib:guava',
     '//lib:gwtorm_client',
+    '//lib/jgit/org.eclipse.jgit:jgit',
+    '//lib/joda:joda-time',
+    '//lib/log:api',
   ],
+  provided_deps = ['//lib:servlet-api-3_1'],
   visibility = ['PUBLIC'],
 )
 
@@ -43,12 +38,12 @@
     '//gerrit-patch-jgit:server',
     '//gerrit-prettify:server',
     '//gerrit-reviewdb:server',
+    '//lib:guava',
     '//lib:gwtjsonrpc',
     '//lib:gwtorm',
-    '//lib:guava',
+    '//lib/jgit/org.eclipse.jgit:jgit',
     '//lib/joda:joda-time',
     '//lib/log:api',
-    '@jgit//org.eclipse.jgit:jgit',
   ],
   provided_deps = ['//lib:servlet-api-3_1'],
   visibility = ['PUBLIC'],
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
index ed28ec0..83dc4d8 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/FileUtil.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/FileUtil.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.common;
 
+import com.google.common.annotations.GwtIncompatible;
+
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
 import org.eclipse.jgit.util.IO;
@@ -25,6 +27,7 @@
 import java.nio.file.Path;
 import java.util.Arrays;
 
+@GwtIncompatible("Unemulated classes in java.io, java.nio and JGit")
 public class FileUtil {
   public static boolean modified(FileBasedConfig cfg) throws IOException {
     byte[] curVers;
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/IoUtil.java b/gerrit-common/src/main/java/com/google/gerrit/common/IoUtil.java
index 9bc2ea5..3422a78 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/IoUtil.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/IoUtil.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.common;
 
+import com.google.common.annotations.GwtIncompatible;
 import com.google.common.collect.Sets;
 
 import java.io.IOException;
@@ -29,6 +30,7 @@
 import java.util.Collections;
 import java.util.Set;
 
+@GwtIncompatible("Unemulated methods in Class and OutputStream")
 public final class IoUtil {
   public static void copyWithThread(final InputStream src,
       final OutputStream dst) {
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 2fce1cd..3957a30 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
@@ -107,11 +107,6 @@
     return QUERY + KeyUtil.encode(query);
   }
 
-  public static String toChangeQuery(String query, String start) {
-    int s = Integer.parseInt(start);
-    return QUERY + KeyUtil.encode(query) + (s > 0 ? "," + s : "");
-  }
-
   public static String toProjectDashboard(Project.NameKey name, String id) {
     return PROJECTS + name.get() + DASHBOARDS + id;
   }
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
index 27dc639..4645158 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/PluginData.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/PluginData.java
@@ -14,9 +14,12 @@
 
 package com.google.gerrit.common;
 
+import com.google.common.annotations.GwtIncompatible;
+
 import java.nio.file.Path;
 import java.util.Objects;
 
+@GwtIncompatible("Unemulated java.nio.file.Path")
 public class PluginData {
   public final String name;
   public final String version;
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/RawInputUtil.java b/gerrit-common/src/main/java/com/google/gerrit/common/RawInputUtil.java
index dfd8453..edcd111 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/RawInputUtil.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/RawInputUtil.java
@@ -16,6 +16,7 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.common.annotations.GwtIncompatible;
 import com.google.common.base.Preconditions;
 import com.google.gerrit.extensions.restapi.RawInput;
 
@@ -25,6 +26,7 @@
 
 import javax.servlet.http.HttpServletRequest;
 
+@GwtIncompatible("Unemulated classes in java.io and javax.servlet")
 public class RawInputUtil {
   public static RawInput create(String content) {
     return create(content.getBytes(UTF_8));
@@ -52,7 +54,7 @@
   }
 
   public static RawInput create(final byte[] bytes) {
-    return create (bytes, "application/octet-stream");
+    return create(bytes, "application/octet-stream");
   }
 
   public static RawInput create(final HttpServletRequest req) {
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/SiteLibraryLoaderUtil.java b/gerrit-common/src/main/java/com/google/gerrit/common/SiteLibraryLoaderUtil.java
index 2511a51..bf87d7b 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/SiteLibraryLoaderUtil.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/SiteLibraryLoaderUtil.java
@@ -16,6 +16,7 @@
 
 import static com.google.gerrit.common.FileUtil.lastModified;
 
+import com.google.common.annotations.GwtIncompatible;
 import com.google.common.collect.ComparisonChain;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Ordering;
@@ -30,6 +31,7 @@
 import java.nio.file.Path;
 import java.util.List;
 
+@GwtIncompatible("Unemulated classes in java.nio and Guava")
 public final class SiteLibraryLoaderUtil {
   private static final Logger log =
       LoggerFactory.getLogger(SiteLibraryLoaderUtil.class);
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/TimeUtil.java b/gerrit-common/src/main/java/com/google/gerrit/common/TimeUtil.java
index 4274b5a..ec91a81 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/TimeUtil.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/TimeUtil.java
@@ -14,11 +14,14 @@
 
 package com.google.gerrit.common;
 
+import com.google.common.annotations.GwtIncompatible;
+
 import org.joda.time.DateTimeUtils;
 
 import java.sql.Timestamp;
 
 /** Static utility methods for dealing with dates and times. */
+@GwtIncompatible("Unemulated org.joda.time.DateTimeUtils")
 public class TimeUtil {
   public static long nowMs() {
     return DateTimeUtils.currentTimeMillis();
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 79c17f4..713fd4d 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
@@ -15,7 +15,6 @@
 package com.google.gerrit.common.auth.openid;
 
 public class OpenIdUrls {
-  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";
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java
index 75872f4..55073d5 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.common.audit.Audit;
 import com.google.gerrit.common.auth.SignInRequired;
-import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtjsonrpc.common.RemoteJsonService;
@@ -29,11 +28,6 @@
 
 @RpcImpl(version = Version.V2_0)
 public interface AccountService extends RemoteJsonService {
-  @Audit
-  @SignInRequired
-  void changeDiffPreferences(DiffPreferencesInfo diffPref,
-      AsyncCallback<VoidResult> callback);
-
   @SignInRequired
   void myProjectWatch(AsyncCallback<List<AccountProjectWatchInfo>> callback);
 
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetailService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetailService.java
deleted file mode 100644
index 6d9c2cd..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetailService.java
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.common.data;
-
-import com.google.gerrit.common.audit.Audit;
-import com.google.gerrit.extensions.client.DiffPreferencesInfo;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gwtjsonrpc.common.AsyncCallback;
-import com.google.gwtjsonrpc.common.RemoteJsonService;
-import com.google.gwtjsonrpc.common.RpcImpl;
-import com.google.gwtjsonrpc.common.RpcImpl.Version;
-
-@RpcImpl(version = Version.V2_0)
-public interface ChangeDetailService extends RemoteJsonService {
-  @Audit
-  void patchSetDetail(PatchSet.Id key, AsyncCallback<PatchSetDetail> callback);
-
-  @Audit
-  void patchSetDetail2(PatchSet.Id baseId, PatchSet.Id key,
-      DiffPreferencesInfo diffPrefs, AsyncCallback<PatchSetDetail> callback);
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GarbageCollectionResult.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GarbageCollectionResult.java
index a052f4f..0156b7d 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GarbageCollectionResult.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GarbageCollectionResult.java
@@ -39,7 +39,7 @@
   }
 
   public static class Error {
-    public static enum Type {
+    public enum Type {
       /** Git garbage collection was already scheduled for this project */
       GC_ALREADY_SCHEDULED,
 
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchDetailService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchDetailService.java
deleted file mode 100644
index d9601f0..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchDetailService.java
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.common.data;
-
-import com.google.gerrit.common.audit.Audit;
-import com.google.gerrit.extensions.client.DiffPreferencesInfo;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gwtjsonrpc.common.AsyncCallback;
-import com.google.gwtjsonrpc.common.RemoteJsonService;
-import com.google.gwtjsonrpc.common.RpcImpl;
-import com.google.gwtjsonrpc.common.RpcImpl.Version;
-
-@RpcImpl(version = Version.V2_0)
-public interface PatchDetailService extends RemoteJsonService {
-  @Audit
-  void patchScript(Patch.Key key, PatchSet.Id a, PatchSet.Id b,
-      DiffPreferencesInfo diffPrefs, AsyncCallback<PatchScript> callback);
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
index f23afb1..5777396 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
@@ -27,38 +27,38 @@
 import java.util.List;
 
 public class PatchScript {
-  public static enum DisplayMethod {
+  public enum DisplayMethod {
     NONE, DIFF, IMG
   }
 
-  public static enum FileMode {
+  public enum FileMode {
     FILE, SYMLINK, GITLINK
   }
 
-  protected Change.Key changeId;
-  protected ChangeType changeType;
-  protected String oldName;
-  protected String newName;
-  protected FileMode oldMode;
-  protected FileMode newMode;
-  protected List<String> header;
-  protected DiffPreferencesInfo diffPrefs;
-  protected SparseFileContent a;
-  protected SparseFileContent b;
-  protected List<Edit> edits;
-  protected DisplayMethod displayMethodA;
-  protected DisplayMethod displayMethodB;
-  protected transient String mimeTypeA;
-  protected transient String mimeTypeB;
-  protected CommentDetail comments;
-  protected List<Patch> history;
-  protected boolean hugeFile;
-  protected boolean intralineDifference;
-  protected boolean intralineFailure;
-  protected boolean intralineTimeout;
-  protected boolean binary;
-  protected transient String commitIdA;
-  protected transient String commitIdB;
+  private Change.Key changeId;
+  private ChangeType changeType;
+  private String oldName;
+  private String newName;
+  private FileMode oldMode;
+  private FileMode newMode;
+  private List<String> header;
+  private DiffPreferencesInfo diffPrefs;
+  private SparseFileContent a;
+  private SparseFileContent b;
+  private List<Edit> edits;
+  private DisplayMethod displayMethodA;
+  private DisplayMethod displayMethodB;
+  private transient String mimeTypeA;
+  private transient String mimeTypeB;
+  private CommentDetail comments;
+  private List<Patch> history;
+  private boolean hugeFile;
+  private boolean intralineDifference;
+  private boolean intralineFailure;
+  private boolean intralineTimeout;
+  private boolean binary;
+  private transient String commitIdA;
+  private transient String commitIdB;
 
   public PatchScript(final Change.Key ck, final ChangeType ct, final String on,
       final String nn, final FileMode om, final FileMode nm,
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetDetail.java
deleted file mode 100644
index 39f5cb0..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetDetail.java
+++ /dev/null
@@ -1,64 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.common.data;
-
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetInfo;
-import com.google.gerrit.reviewdb.client.Project;
-
-import java.util.List;
-
-public class PatchSetDetail {
-  protected PatchSet patchSet;
-  protected PatchSetInfo info;
-  protected List<Patch> patches;
-  protected Project.NameKey project;
-
-  public PatchSetDetail() {
-  }
-
-  public PatchSet getPatchSet() {
-    return patchSet;
-  }
-
-  public void setPatchSet(final PatchSet ps) {
-    patchSet = ps;
-  }
-
-  public PatchSetInfo getInfo() {
-    return info;
-  }
-
-  public void setInfo(final PatchSetInfo i) {
-    info = i;
-  }
-
-  public List<Patch> getPatches() {
-    return patches;
-  }
-
-  public void setPatches(final List<Patch> p) {
-    patches = p;
-  }
-
-  public Project.NameKey getProject() {
-    return project;
-  }
-
-  public void setProject(final Project.NameKey p) {
-    project = p;
-  }
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRule.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRule.java
index 5aeb797..6511d69 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRule.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRule.java
@@ -17,7 +17,7 @@
 public class PermissionRule implements Comparable<PermissionRule> {
   public static final String FORCE_PUSH = "Force Push";
   public static final String FORCE_EDIT = "Force Edit";
-  public static enum Action {
+  public enum Action {
     ALLOW, DENY, BLOCK,
 
     INTERACTIVE, BATCH
@@ -264,6 +264,11 @@
     return rule;
   }
 
+  public boolean hasRange() {
+    return (!(getMin() == null || getMin() == 0))
+      || (!(getMax() == null || getMax() == 0));
+  }
+
   public static int parseInt(String value) {
     if (value.startsWith("+")) {
       value = value.substring(1);
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitRecord.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitRecord.java
index 5a32fcf..9dccf0c 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitRecord.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitRecord.java
@@ -23,7 +23,7 @@
  * Describes the state required to submit a change.
  */
 public class SubmitRecord {
-  public static enum Status {
+  public enum Status {
     /** The change is ready for submission. */
     OK,
 
@@ -49,7 +49,7 @@
   public String errorMessage;
 
   public static class Label {
-    public static enum Status {
+    public enum Status {
       /**
        * This label provides what is necessary for submission.
        * <p>
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 cd226ef..b6ce797 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
@@ -20,7 +20,7 @@
  * Describes the submit type for a change.
  */
 public class SubmitTypeRecord {
-  public static enum Status {
+  public enum Status {
     /** The type was computed successfully */
     OK,
 
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/SubscribeSection.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SubscribeSection.java
index 7ec1eda..b05f335 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/SubscribeSection.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/SubscribeSection.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.common.data;
 
+import com.google.common.annotations.GwtIncompatible;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
 
@@ -25,6 +26,7 @@
 import java.util.List;
 
 /** Portion of a {@link Project} describing superproject subscription rules. */
+@GwtIncompatible("Unemulated org.eclipse.jgit.transport.RefSpec")
 public class SubscribeSection {
 
   private final List<RefSpec> refSpecs;
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/errors/CorruptEntityException.java b/gerrit-common/src/main/java/com/google/gerrit/common/errors/CorruptEntityException.java
deleted file mode 100644
index f2b3e98..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/errors/CorruptEntityException.java
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.common.errors;
-
-import com.google.gwtorm.client.Key;
-
-/** Error indicating the entity's database records are invalid. */
-public class CorruptEntityException extends Exception {
-  private static final long serialVersionUID = 1L;
-
-  public static final String MESSAGE_PREFIX = "Corrupt Database Entity: ";
-
-  public CorruptEntityException(final Key<?> key) {
-    super(MESSAGE_PREFIX + key);
-  }
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/errors/InactiveAccountException.java b/gerrit-common/src/main/java/com/google/gerrit/common/errors/InactiveAccountException.java
deleted file mode 100644
index 6ae5eb6..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/errors/InactiveAccountException.java
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.common.errors;
-
-/** Error indicating the account is currently inactive. */
-public class InactiveAccountException extends Exception {
-  private static final long serialVersionUID = 1L;
-
-  public static final String MESSAGE = "Account Inactive: ";
-
-  public InactiveAccountException(String who) {
-    super(MESSAGE + who);
-  }
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/errors/InvalidQueryException.java b/gerrit-common/src/main/java/com/google/gerrit/common/errors/InvalidQueryException.java
index 53792e4..4a66a416a 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/errors/InvalidQueryException.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/errors/InvalidQueryException.java
@@ -18,10 +18,6 @@
 public class InvalidQueryException extends Exception {
   private static final long serialVersionUID = 1L;
 
-  public InvalidQueryException(String message) {
-    super(message);
-  }
-
   public InvalidQueryException(String message, String query) {
     super("Invalid query: " + query + "\n\n" + message);
   }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/errors/NoSuchGroupException.java b/gerrit-common/src/main/java/com/google/gerrit/common/errors/NoSuchGroupException.java
index ade0d98..8b740c3 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/errors/NoSuchGroupException.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/errors/NoSuchGroupException.java
@@ -38,10 +38,6 @@
     super(MESSAGE + key.toString(), why);
   }
 
-  public NoSuchGroupException(final AccountGroup.NameKey k) {
-    this(k, null);
-  }
-
   public NoSuchGroupException(final AccountGroup.NameKey k, final Throwable why) {
     super(MESSAGE + k.toString(), why);
   }
diff --git a/gerrit-extension-api/BUCK b/gerrit-extension-api/BUCK
index 307aefa..67bfdc1 100644
--- a/gerrit-extension-api/BUCK
+++ b/gerrit-extension-api/BUCK
@@ -70,7 +70,8 @@
   srcs = SRCS,
   deps = [
     '//lib/guice:javax-inject',
-    '//lib/guice:guice_library'
+    '//lib/guice:guice_library',
+    '//lib/guice:guice-assistedinject',
   ],
   visibility = ['PUBLIC'],
 )
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 4404f94..c2e3787 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
@@ -32,7 +32,7 @@
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
    **/
-  public class NotImplemented implements GerritApi {
+  class NotImplemented implements GerritApi {
     @Override
     public Accounts accounts() {
       throw new NotImplementedException();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorContainer.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/access/AccessSectionInfo.java
similarity index 71%
copy from gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorContainer.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/access/AccessSectionInfo.java
index b1381ca..b97fd3b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorContainer.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/access/AccessSectionInfo.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 The Android Open Source Project
+// Copyright (C) 2016 The Android Open 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,11 +11,10 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF 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.access;
 
-package com.google.gerrit.client.patches;
+import java.util.Map;
 
-public interface CommentEditorContainer {
-  void notifyDraftDelta(int delta);
-
-  void remove(CommentEditorPanel panel);
+public class AccessSectionInfo {
+  public Map<String, PermissionInfo> permissions;
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/access/PermissionInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/access/PermissionInfo.java
new file mode 100644
index 0000000..c6d25b3
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/access/PermissionInfo.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.access;
+
+import java.util.Map;
+
+public class PermissionInfo {
+  public String label;
+  public Boolean exclusive;
+  public Map<String, PermissionRuleInfo> rules;
+
+  public PermissionInfo(String label, Boolean exclusive) {
+    this.label = label;
+    this.exclusive = exclusive;
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/access/PermissionRuleInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/access/PermissionRuleInfo.java
new file mode 100644
index 0000000..93990dc
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/access/PermissionRuleInfo.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.access;
+
+public class PermissionRuleInfo {
+  public enum Action {
+    ALLOW,
+    DENY,
+    BLOCK,
+    INTERACTIVE,
+    BATCH
+  }
+
+  public Action action;
+  public Boolean force;
+  public Integer min;
+  public Integer max;
+
+  public PermissionRuleInfo(Action action, Boolean force) {
+    this.action = action;
+    this.force = force;
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/access/ProjectAccessInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/access/ProjectAccessInfo.java
new file mode 100644
index 0000000..3274313a
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/access/ProjectAccessInfo.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.access;
+
+import com.google.gerrit.extensions.common.ProjectInfo;
+
+import java.util.Map;
+import java.util.Set;
+
+public class ProjectAccessInfo {
+  public String revision;
+  public ProjectInfo inheritsFrom;
+  public Map<String, AccessSectionInfo> local;
+  public Boolean isOwner;
+  public Set<String> ownerOf;
+  public Boolean canUpload;
+  public Boolean canAdd;
+  public Boolean configVisible;
+}
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
index 836d719..d17beca 100644
--- 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
@@ -14,10 +14,13 @@
 
 package com.google.gerrit.extensions.api.accounts;
 
+import com.google.gerrit.extensions.api.changes.StarsInput;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.client.EditPreferencesInfo;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
+import com.google.gerrit.extensions.client.ProjectWatchInfo;
 import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.GpgKeyInfo;
 import com.google.gerrit.extensions.common.SshKeyInfo;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
@@ -25,6 +28,7 @@
 
 import java.util.List;
 import java.util.Map;
+import java.util.SortedSet;
 
 public interface AccountApi {
   AccountInfo get() throws RestApiException;
@@ -43,12 +47,23 @@
   EditPreferencesInfo setEditPreferences(EditPreferencesInfo in)
       throws RestApiException;
 
-  void starChange(String id) throws RestApiException;
-  void unstarChange(String id) throws RestApiException;
+  List<ProjectWatchInfo> getWatchedProjects() throws RestApiException;
+  List<ProjectWatchInfo> setWatchedProjects(List<ProjectWatchInfo> in)
+      throws RestApiException;
+  void deleteWatchedProjects(List<String> in)
+      throws RestApiException;
+
+  void starChange(String changeId) throws RestApiException;
+  void unstarChange(String changeId) throws RestApiException;
+  void setStars(String changeId, StarsInput input) throws RestApiException;
+  SortedSet<String> getStars(String changeId) throws RestApiException;
+  List<ChangeInfo> getStarredChanges() throws RestApiException;
+
   void addEmail(EmailInput input) throws RestApiException;
 
   List<SshKeyInfo> listSshKeys() throws RestApiException;
   SshKeyInfo addSshKey(String key) throws RestApiException;
+  void deleteSshKey(int seq) throws RestApiException;
 
   Map<String, GpgKeyInfo> listGpgKeys() throws RestApiException;
   Map<String, GpgKeyInfo> putGpgKeys(List<String> add, List<String> remove)
@@ -59,7 +74,7 @@
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
    **/
-  public class NotImplemented implements AccountApi {
+  class NotImplemented implements AccountApi {
     @Override
     public AccountInfo get() throws RestApiException {
       throw new NotImplementedException();
@@ -104,12 +119,46 @@
     }
 
     @Override
-    public void starChange(String id) throws RestApiException {
+    public List<ProjectWatchInfo> getWatchedProjects()
+        throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void unstarChange(String id) throws RestApiException {
+    public List<ProjectWatchInfo> setWatchedProjects(
+        List<ProjectWatchInfo> in) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void deleteWatchedProjects(List<String> in)
+        throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void starChange(String changeId) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void unstarChange(String changeId) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void setStars(String changeId, StarsInput input)
+        throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public SortedSet<String> getStars(String changeId) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public List<ChangeInfo> getStarredChanges() throws RestApiException {
       throw new NotImplementedException();
     }
 
@@ -129,6 +178,11 @@
     }
 
     @Override
+    public void deleteSshKey(int seq) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public Map<String, GpgKeyInfo> putGpgKeys(List<String> add,
         List<String> remove) 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
index 9cddda9..0d2e025 100644
--- 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
@@ -75,7 +75,7 @@
    *
    * @see #suggestAccounts()
    */
-  public abstract class SuggestAccountsRequest {
+  abstract class SuggestAccountsRequest {
     private String query;
     private int limit;
 
@@ -116,7 +116,7 @@
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
    **/
-  public class NotImplemented implements Accounts {
+  class NotImplemented implements Accounts {
     @Override
     public AccountApi id(String id) throws RestApiException {
       throw new NotImplementedException();
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/GpgKeyApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/GpgKeyApi.java
index ffdcf87..6f87e8b 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/GpgKeyApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/GpgKeyApi.java
@@ -26,7 +26,7 @@
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
    */
-  public class NotImplemented implements GpgKeyApi {
+  class NotImplemented implements GpgKeyApi {
     @Override
     public GpgKeyInfo get() 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 ae5f0b8..8f04422 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
@@ -156,8 +156,9 @@
 
   ChangeInfo check() throws RestApiException;
   ChangeInfo check(FixInput fix) throws RestApiException;
+  void index() throws RestApiException;
 
-  public abstract class SuggestedReviewersRequest {
+  abstract class SuggestedReviewersRequest {
     private String query;
     private int limit;
 
@@ -186,7 +187,7 @@
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
    **/
-  public class NotImplemented implements ChangeApi {
+  class NotImplemented implements ChangeApi {
     @Override
     public String id() {
       throw new NotImplementedException();
@@ -343,6 +344,11 @@
     }
 
     @Override
+    public void index() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public List<ChangeInfo> submittedTogether() 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 b1381e7..aa67473 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
@@ -64,7 +64,7 @@
   QueryRequest query();
   QueryRequest query(String query);
 
-  public abstract class QueryRequest {
+  abstract class QueryRequest {
     private String query;
     private int limit;
     private int start;
@@ -140,7 +140,7 @@
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
    **/
-  public class NotImplemented implements Changes {
+  class NotImplemented implements Changes {
     @Override
     public ChangeApi id(int id) throws RestApiException {
       throw new NotImplementedException();
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/CommentApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/CommentApi.java
index d0c5633..adac284 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/CommentApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/CommentApi.java
@@ -25,7 +25,7 @@
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
    **/
-  public class NotImplemented implements CommentApi {
+  class NotImplemented implements CommentApi {
     @Override
     public CommentInfo get() throws RestApiException {
       throw new NotImplementedException();
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DraftApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DraftApi.java
index 80a71f8..50335db 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DraftApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DraftApi.java
@@ -26,7 +26,7 @@
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
    **/
-  public class NotImplemented extends CommentApi.NotImplemented
+  class NotImplemented extends CommentApi.NotImplemented
       implements DraftApi {
     @Override
     public CommentInfo update(DraftInput in) throws RestApiException {
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DraftInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DraftInput.java
index dd8f488..8b626b7 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DraftInput.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DraftInput.java
@@ -17,4 +17,5 @@
 import com.google.gerrit.extensions.client.Comment;
 
 public class DraftInput extends Comment {
+  public String tag;
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/FileApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/FileApi.java
index 5e995e2..3641ac5 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/FileApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/FileApi.java
@@ -40,7 +40,7 @@
    */
   DiffRequest diffRequest() throws RestApiException;
 
-  public abstract class DiffRequest {
+  abstract class DiffRequest {
     private String base;
     private Integer context;
     private Boolean intraline;
@@ -89,7 +89,7 @@
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
    **/
-  public class NotImplemented implements FileApi {
+  class NotImplemented implements FileApi {
     @Override
     public BinaryResult content() throws RestApiException {
       throw new NotImplementedException();
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/MoveInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/MoveInput.java
index 7e4e5e9..795642a 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/MoveInput.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/MoveInput.java
@@ -16,5 +16,5 @@
 
 public class MoveInput {
   public String message;
-  public String destination_branch;
+  public String destinationBranch;
 }
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 e6ce6b2..c1610c3 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
@@ -26,6 +26,8 @@
   @DefaultInput
   public String message;
 
+  public String tag;
+
   public Map<String, Short> labels;
   public Map<String, List<CommentInput>> comments;
 
@@ -66,7 +68,7 @@
    */
   public String onBehalfOf;
 
-  public static enum DraftHandling {
+  public enum DraftHandling {
     /** Delete pending drafts on this revision only. */
     DELETE,
 
@@ -80,7 +82,7 @@
     PUBLISH_ALL_REVISIONS
   }
 
-  public static enum NotifyHandling {
+  public enum NotifyHandling {
     NONE, OWNER, OWNER_REVIEWERS, ALL
   }
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewerApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewerApi.java
index 11b670d..5571726 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewerApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewerApi.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.extensions.api.changes;
 
+import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 
 import java.util.Map;
@@ -22,4 +23,20 @@
 
   Map<String, Short> votes() throws RestApiException;
   void deleteVote(String label) throws RestApiException;
+
+  /**
+   * A default implementation which allows source compatibility
+   * when adding new methods to the interface.
+   **/
+  class NotImplemented implements ReviewerApi {
+    @Override
+    public Map<String, Short> votes() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void deleteVote(String label) throws RestApiException {
+      throw new NotImplementedException();
+    }
+  }
 }
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 627917c..b23c7f9 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
@@ -75,7 +75,7 @@
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
    **/
-  public class NotImplemented implements RevisionApi {
+  class NotImplemented implements RevisionApi {
     @Override
     public void delete() throws RestApiException {
       throw new NotImplementedException();
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/StarsInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/StarsInput.java
new file mode 100644
index 0000000..d3dff98
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/StarsInput.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.changes;
+
+import java.util.Set;
+
+public class StarsInput {
+  public Set<String> add;
+  public Set<String> remove;
+
+  public StarsInput() {
+  }
+
+  public StarsInput(Set<String> add) {
+    this.add = add;
+  }
+
+  public StarsInput(Set<String> add, Set<String> remove) {
+    this.add = add;
+    this.remove = remove;
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/SubmitInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/SubmitInput.java
index 053248f..6abf83df 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/SubmitInput.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/SubmitInput.java
@@ -14,10 +14,14 @@
 
 package com.google.gerrit.extensions.api.changes;
 
+import com.google.gerrit.extensions.api.changes.ReviewInput.NotifyHandling;
+
 public class SubmitInput {
   /** Not used anymore, kept for backward compatibility */
   @Deprecated
   public boolean waitForMerge;
 
   public String onBehalfOf;
+
+  public NotifyHandling notify = NotifyHandling.ALL;
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Config.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Config.java
index 348cf4b..3f4971e 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Config.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Config.java
@@ -26,7 +26,7 @@
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
    **/
-  public class NotImplemented implements Config {
+  class NotImplemented implements Config {
     @Override
     public Server server() {
       throw new NotImplementedException();
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Server.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Server.java
index c093b18..1c535dd 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Server.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Server.java
@@ -27,7 +27,7 @@
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
    **/
-  public class NotImplemented implements Server {
+  class NotImplemented implements Server {
     @Override
     public String getVersion() throws RestApiException {
       throw new NotImplementedException();
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/GroupApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/GroupApi.java
index f5a89c0..d3f4463 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/GroupApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/GroupApi.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.extensions.common.GroupAuditEventInfo;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.common.GroupOptionsInfo;
+import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 
 import java.util.List;
@@ -141,4 +142,102 @@
    * @throws RestApiException
    */
   List<? extends GroupAuditEventInfo> auditLog() throws RestApiException;
+
+  /**
+   * A default implementation which allows source compatibility
+   * when adding new methods to the interface.
+   **/
+  class NotImplemented implements GroupApi {
+    @Override
+    public GroupInfo get() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public GroupInfo detail() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public String name() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void name(String name) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public GroupInfo owner() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void owner(String owner) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public String description() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void description(String description) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public GroupOptionsInfo options() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void options(GroupOptionsInfo options) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public List<AccountInfo> members() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public List<AccountInfo> members(boolean recursive)
+        throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void addMembers(String... members) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void removeMembers(String... members) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public List<GroupInfo> includedGroups() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void addGroups(String... groups) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void removeGroups(String... groups) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public List<? extends GroupAuditEventInfo> auditLog()
+        throws RestApiException {
+      throw new NotImplementedException();
+    }
+  }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/Groups.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/Groups.java
index b909f31..b58009d 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/Groups.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/Groups.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.extensions.client.ListGroupsOption;
 import com.google.gerrit.extensions.common.GroupInfo;
+import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 
 import java.util.ArrayList;
@@ -51,7 +52,7 @@
   /** @return new request for listing groups. */
   ListRequest list();
 
-  public abstract class ListRequest {
+  abstract class ListRequest {
     private final EnumSet<ListGroupsOption> options =
         EnumSet.noneOf(ListGroupsOption.class);
     private final List<String> projects = new ArrayList<>();
@@ -114,6 +115,11 @@
       return this;
     }
 
+    public ListRequest withOwned(boolean owned) {
+      this.owned = owned;
+      return this;
+    }
+
     public ListRequest withLimit(int limit) {
       this.limit = limit;
       return this;
@@ -174,4 +180,30 @@
       return suggest;
     }
   }
+
+  /**
+   * A default implementation which allows source compatibility
+   * when adding new methods to the interface.
+   **/
+  class NotImplemented implements Groups {
+    @Override
+    public GroupApi id(String id) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public GroupApi create(String name) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public GroupApi create(GroupInput input) 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/api/projects/BranchApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchApi.java
index e7c98e9..222248d 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
@@ -34,7 +34,7 @@
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
    **/
-  public class NotImplemented implements BranchApi {
+  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/ChildProjectApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ChildProjectApi.java
index a930f0d..3bffac0 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ChildProjectApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ChildProjectApi.java
@@ -26,7 +26,7 @@
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
    **/
-  public class NotImplemented implements ChildProjectApi {
+  class NotImplemented implements ChildProjectApi {
     @Override
     public ProjectInfo get() 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 e3eb4be..75cebaa 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,7 @@
 
 package com.google.gerrit.extensions.api.projects;
 
+import com.google.gerrit.extensions.api.access.ProjectAccessInfo;
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -28,10 +29,12 @@
   String description() throws RestApiException;
   void description(PutDescriptionInput in) throws RestApiException;
 
+  ProjectAccessInfo access() throws RestApiException;
+
   ListRefsRequest<BranchInfo> branches();
   ListRefsRequest<TagInfo> tags();
 
-  public abstract class ListRefsRequest<T extends RefInfo> {
+  abstract class ListRefsRequest<T extends RefInfo> {
     protected int limit;
     protected int start;
     protected String substring;
@@ -108,7 +111,7 @@
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
    **/
-  public class NotImplemented implements ProjectApi {
+  class NotImplemented implements ProjectApi {
     @Override
     public ProjectApi create() throws RestApiException {
       throw new NotImplementedException();
@@ -130,6 +133,11 @@
     }
 
     @Override
+    public ProjectAccessInfo access() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public void description(PutDescriptionInput in)
         throws RestApiException {
       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
index 27bdf16..92ac1d6 100644
--- 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
@@ -41,4 +41,4 @@
     public String value;
     public List<String> values;
   }
-}
\ 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 0e848b9..fdbadb2 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
@@ -60,8 +60,8 @@
 
   ListRequest list();
 
-  public abstract class ListRequest {
-    public static enum FilterType {
+  abstract class ListRequest {
+    public enum FilterType {
       CODE, PARENT_CANDIDATES, PERMISSIONS, ALL
     }
 
@@ -175,7 +175,7 @@
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
    **/
-  public class NotImplemented implements Projects {
+  class NotImplemented implements Projects {
     @Override
     public ProjectApi name(String name) throws RestApiException {
       throw new NotImplementedException();
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagApi.java
index 6cc1ba4..3071fd2 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagApi.java
@@ -24,7 +24,7 @@
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
    **/
-  public class NotImplemented implements TagApi {
+  class NotImplemented implements TagApi {
     @Override
     public TagInfo get() throws RestApiException {
       throw new NotImplementedException();
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
index 901951e..99b2cfa 100644
--- 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
@@ -14,17 +14,33 @@
 
 package com.google.gerrit.extensions.auth.oauth;
 
+import java.io.Serializable;
+
 /* OAuth token */
-public class OAuthToken {
+public class OAuthToken implements Serializable {
+
+  private static final long serialVersionUID = 1L;
 
   private final String token;
   private final String secret;
   private final String raw;
 
+  /**
+   * Time of expiration of this token, or {@code Long#MAX_VALUE} if this
+   * token never expires, or time of expiration is unknown.
+   */
+  private final long expiresAt;
+
   public OAuthToken(String token, String secret, String raw) {
+    this(token, secret, raw, Long.MAX_VALUE);
+  }
+
+  public OAuthToken(String token, String secret, String raw,
+      long expiresAt) {
     this.token = token;
     this.secret = secret;
     this.raw = raw;
+    this.expiresAt = expiresAt;
   }
 
   public String getToken() {
@@ -38,4 +54,12 @@
   public String getRaw() {
     return raw;
   }
+
+  public long getExpiresAt() {
+    return expiresAt;
+  }
+
+  public boolean isExpired() {
+    return System.currentTimeMillis() > expiresAt;
+  }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthTokenEncrypter.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthTokenEncrypter.java
new file mode 100644
index 0000000..b2f4262
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthTokenEncrypter.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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;
+
+@ExtensionPoint
+public interface OAuthTokenEncrypter {
+
+  /**
+   * Encrypts the secret parts of the given OAuth access token.
+   *
+   * @param unencrypted a raw OAuth access token.
+   */
+  OAuthToken encrypt(OAuthToken unencrypted);
+
+  /**
+   * Decrypts the secret parts of the given OAuth access token.
+   *
+   * @param encrypted an encryppted OAuth access token.
+   */
+  OAuthToken decrypt(OAuthToken encrypted);
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java
index 703f7b3..1373b09 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java
@@ -32,7 +32,7 @@
   public static final short[] CONTEXT_CHOICES =
       {3, 10, 25, 50, 75, 100, WHOLE_FILE_CONTEXT};
 
-  public static enum Whitespace {
+  public enum Whitespace {
     IGNORE_NONE,
     IGNORE_TRAILING,
     IGNORE_LEADING_AND_TRAILING,
@@ -60,6 +60,7 @@
   public Whitespace ignoreWhitespace;
   public Boolean retainHeader;
   public Boolean skipDeleted;
+  public Boolean skipUnchanged;
   public Boolean skipUncommented;
 
   public static DiffPreferencesInfo defaults() {
@@ -78,6 +79,7 @@
     i.showTabs = true;
     i.showWhitespaceErrors = true;
     i.skipDeleted = false;
+    i.skipUnchanged = false;
     i.skipUncommented = false;
     i.syntaxHighlighting = true;
     i.hideTopMenu = false;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/EditPreferencesInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/EditPreferencesInfo.java
index 3e45523..1f7b84a 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/EditPreferencesInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/EditPreferencesInfo.java
@@ -18,6 +18,7 @@
 public class EditPreferencesInfo {
   public Integer tabSize;
   public Integer lineLength;
+  public Integer indentUnit;
   public Integer cursorBlinkRate;
   public Boolean hideTopMenu;
   public Boolean showTabs;
@@ -26,6 +27,7 @@
   public Boolean hideLineNumbers;
   public Boolean matchBrackets;
   public Boolean autoCloseBrackets;
+  public Boolean showBase;
   public Theme theme;
   public KeyMapType keyMapType;
 
@@ -33,6 +35,7 @@
     EditPreferencesInfo i = new EditPreferencesInfo();
     i.tabSize = 8;
     i.lineLength = 100;
+    i.indentUnit = 2;
     i.cursorBlinkRate = 0;
     i.hideTopMenu = false;
     i.showTabs = true;
@@ -41,6 +44,7 @@
     i.hideLineNumbers = false;
     i.matchBrackets = true;
     i.autoCloseBrackets = false;
+    i.showBase = false;
     i.theme = Theme.DEFAULT;
     i.keyMapType = KeyMapType.DEFAULT;
     return i;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
index cb3ab59..9754f12 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
@@ -27,11 +27,11 @@
   public static final int[] PAGESIZE_CHOICES = {10, 25, 50, 100};
 
   /** Preferred method to download a change. */
-  public static enum DownloadCommand {
+  public enum DownloadCommand {
     REPO_DOWNLOAD, PULL, CHECKOUT, CHERRY_PICK, FORMAT_PATCH
   }
 
-  public static enum DateFormat {
+  public enum DateFormat {
     /** US style dates: Apr 27, Feb 14, 2010 */
     STD("MMM d", "MMM d, yyyy"),
 
@@ -64,7 +64,7 @@
     }
   }
 
-  public static enum ReviewCategoryStrategy {
+  public enum ReviewCategoryStrategy {
     NONE,
     NAME,
     EMAIL,
@@ -72,18 +72,18 @@
     ABBREV
   }
 
-  public static enum DiffView {
+  public enum DiffView {
     SIDE_BY_SIDE,
     UNIFIED_DIFF
   }
 
-  public static enum EmailStrategy {
+  public enum EmailStrategy {
     ENABLED,
     CC_ON_OWN_COMMENTS,
     DISABLED
   }
 
-  public static enum TimeFormat {
+  public enum TimeFormat {
     /** 12-hour clock: 1:15 am, 2:13 pm */
     HHMM_12("h:mm a"),
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GerritTopMenu.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GerritTopMenu.java
index 81d6149..0a5b033 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GerritTopMenu.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GerritTopMenu.java
@@ -15,11 +15,11 @@
 package com.google.gerrit.extensions.client;
 
 public enum GerritTopMenu {
-  ALL, MY, DIFFERENCES, PROJECTS, PEOPLE, PLUGINS, DOCUMENTATION;
+  ALL, MY, PROJECTS, PEOPLE, PLUGINS, DOCUMENTATION;
 
   public final String menuName;
 
-  private GerritTopMenu() {
+  GerritTopMenu() {
     menuName = name().substring(0, 1) + name().substring(1).toLowerCase();
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/InheritableBoolean.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/InheritableBoolean.java
index 57d4849..ce6464d 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/InheritableBoolean.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/InheritableBoolean.java
@@ -18,4 +18,4 @@
   TRUE,
   FALSE,
   INHERIT
-}
\ No newline at end of file
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/KeyMapType.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/KeyMapType.java
index 261168d..9c85ac7 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/KeyMapType.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/KeyMapType.java
@@ -17,5 +17,6 @@
 public enum KeyMapType {
   DEFAULT,
   EMACS,
+  SUBLIME,
   VIM
-}
\ No newline at end of file
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListChangesOption.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListChangesOption.java
index 4c336f7..88c02b82 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListChangesOption.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListChangesOption.java
@@ -70,7 +70,7 @@
 
   private final int value;
 
-  private ListChangesOption(int v) {
+  ListChangesOption(int v) {
     this.value = v;
   }
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListGroupsOption.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListGroupsOption.java
index d87f73e..e95570f 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListGroupsOption.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListGroupsOption.java
@@ -26,7 +26,7 @@
 
   private final int value;
 
-  private ListGroupsOption(int v) {
+  ListGroupsOption(int v) {
     this.value = v;
   }
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ProjectState.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ProjectState.java
index 6f4190d..3114cb9 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ProjectState.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ProjectState.java
@@ -18,4 +18,4 @@
   ACTIVE,
   READ_ONLY,
   HIDDEN
-}
\ No newline at end of file
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ProjectWatchInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ProjectWatchInfo.java
new file mode 100644
index 0000000..beb869e
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ProjectWatchInfo.java
@@ -0,0 +1,50 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.client;
+
+import java.util.Objects;
+
+public class ProjectWatchInfo {
+  public String project;
+  public String filter;
+
+  public Boolean notifyNewChanges;
+  public Boolean notifyNewPatchSets;
+  public Boolean notifyAllComments;
+  public Boolean notifySubmittedChanges;
+  public Boolean notifyAbandonedChanges;
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj instanceof ProjectWatchInfo) {
+      ProjectWatchInfo w = (ProjectWatchInfo) obj;
+      return Objects.equals(project, w.project)
+          && Objects.equals(filter, w.filter)
+          && Objects.equals(notifyNewChanges, w.notifyNewChanges)
+          && Objects.equals(notifyNewPatchSets, w.notifyNewPatchSets)
+          && Objects.equals(notifyAllComments, w.notifyAllComments)
+          && Objects.equals(notifySubmittedChanges, w.notifySubmittedChanges)
+          && Objects.equals(notifyAbandonedChanges, w.notifyAbandonedChanges);
+    }
+    return false;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects
+        .hash(project, filter, notifyNewChanges, notifyNewPatchSets,
+            notifyAllComments, notifySubmittedChanges, notifyAbandonedChanges);
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/SubmitType.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/SubmitType.java
index 2b916f1..fcfeb01 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/SubmitType.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/SubmitType.java
@@ -20,4 +20,4 @@
   REBASE_IF_NECESSARY,
   MERGE_ALWAYS,
   CHERRY_PICK
-}
\ No newline at end of file
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Theme.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Theme.java
index a32d7d3..c03a684 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Theme.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Theme.java
@@ -17,27 +17,105 @@
 public enum Theme {
   // Light themes
   DEFAULT,
+  DAY_3024,
+  BASE16_LIGHT,
   ECLIPSE,
   ELEGANT,
+  MDN_LIKE,
   NEAT,
+  NEO,
+  PARAISO_LIGHT,
+  SOLARIZED_LIGHT,
+  TTCN,
+  XQ_LIGHT,
+  YETI,
 
   // Dark themes
+  NIGHT_3024,
+  ABCDEF,
+  AMBIANCE,
+  BASE16_DARK,
+  BESPIN,
+  BLACKBOARD,
+  COBALT,
+  COLORFORTH,
+  DRACULA,
+  ERLANG_DARK,
+  HOPSCOTCH,
+  ICECODER,
+  ISOTOPE,
+  LESSER_DARK,
+  LIQUIBYTE,
+  MATERIAL,
+  MBO,
   MIDNIGHT,
+  MONOKAI,
   NIGHT,
-  TWILIGHT;
+  PARAISO_DARK,
+  PASTEL_ON_DARK,
+  RAILSCASTS,
+  RUBYBLUE,
+  SETI,
+  SOLARIZED_DARK,
+  THE_MATRIX,
+  TOMORROW_NIGHT_BRIGHT,
+  TOMORROW_NIGHT_EIGHTIES,
+  TWILIGHT,
+  VIBRANT_INK,
+  XQ_DARK,
+  ZENBURN;
 
   public boolean isDark() {
     switch (this) {
+      case NIGHT_3024:
+      case ABCDEF:
+      case AMBIANCE:
+      case BASE16_DARK:
+      case BESPIN:
+      case BLACKBOARD:
+      case COBALT:
+      case COLORFORTH:
+      case DRACULA:
+      case ERLANG_DARK:
+      case HOPSCOTCH:
+      case ICECODER:
+      case ISOTOPE:
+      case LESSER_DARK:
+      case LIQUIBYTE:
+      case MATERIAL:
+      case MBO:
       case MIDNIGHT:
+      case MONOKAI:
       case NIGHT:
+      case PARAISO_DARK:
+      case PASTEL_ON_DARK:
+      case RAILSCASTS:
+      case RUBYBLUE:
+      case SETI:
+      case SOLARIZED_DARK:
+      case THE_MATRIX:
+      case TOMORROW_NIGHT_BRIGHT:
+      case TOMORROW_NIGHT_EIGHTIES:
       case TWILIGHT:
+      case VIBRANT_INK:
+      case XQ_DARK:
+      case ZENBURN:
         return true;
       case DEFAULT:
+      case DAY_3024:
+      case BASE16_LIGHT:
       case ECLIPSE:
       case ELEGANT:
+      case MDN_LIKE:
       case NEAT:
+      case NEO:
+      case PARAISO_LIGHT:
+      case SOLARIZED_LIGHT:
+      case TTCN:
+      case XQ_LIGHT:
+      case YETI:
       default:
         return false;
     }
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ApprovalInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ApprovalInfo.java
index ce120cd..6d28dbc 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ApprovalInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ApprovalInfo.java
@@ -17,6 +17,7 @@
 import java.sql.Timestamp;
 
 public class ApprovalInfo extends AccountInfo {
+  public String tag;
   public Integer value;
   public Timestamp date;
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/BlameInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/BlameInfo.java
new file mode 100644
index 0000000..df3f373
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/BlameInfo.java
@@ -0,0 +1,43 @@
+// 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.common;
+
+import java.util.List;
+
+public class BlameInfo {
+  public String author;
+  public String id;
+  public int time;
+  public String commitMsg;
+  public List<RangeInfo> ranges;
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    BlameInfo blameInfo = (BlameInfo) o;
+
+    return id.equals(blameInfo.id);
+  }
+
+  @Override
+  public int hashCode() {
+    return id.hashCode();
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeMessageInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeMessageInfo.java
index 7ad40b5..e79918f 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeMessageInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeMessageInfo.java
@@ -18,6 +18,7 @@
 
 public class ChangeMessageInfo {
   public String id;
+  public String tag;
   public AccountInfo author;
   public Timestamp date;
   public String message;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/CommentInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/CommentInfo.java
index b1f8183..b7535e1 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/CommentInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/CommentInfo.java
@@ -18,4 +18,5 @@
 
 public class CommentInfo extends Comment {
   public AccountInfo author;
+  public String tag;
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DiffInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DiffInfo.java
index 58b2d39..3df4b86 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DiffInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DiffInfo.java
@@ -35,7 +35,7 @@
   // Binary file
   public Boolean binary;
 
-  public static enum IntraLineStatus {
+  public enum IntraLineStatus {
     OK,
     TIMEOUT,
     FAILURE
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ProblemInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ProblemInfo.java
index d04b346..4dd910d 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ProblemInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ProblemInfo.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.extensions.common;
 
 public class ProblemInfo {
-  public static enum Status {
+  public enum Status {
     FIXED, FIX_FAILED
   }
 
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFactory.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RangeInfo.java
similarity index 68%
rename from gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFactory.java
rename to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RangeInfo.java
index f68b629..5268825 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFactory.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RangeInfo.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 The Android Open Source Project
+// Copyright (C) 2016 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,9 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.prettify.client;
+package com.google.gerrit.extensions.common;
 
-/** Creates a new PrettyFormatter instance for one formatting run. */
-public interface PrettyFactory {
-  PrettyFormatter get();
+public class RangeInfo {
+  public int start;
+  public int end;
+
+  public RangeInfo(int start, int end) {
+    this.start = start;
+    this.end = end;
+  }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java
index a21c2d4..068d9a0 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java
@@ -62,7 +62,7 @@
   private Charset characterEncoding;
   private long contentLength = -1;
   private boolean gzip = true;
-  private boolean base64 = false;
+  private boolean base64;
   private String attachmentName;
 
   /** @return the MIME type of the result, for HTTP clients. */
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/systemstatus/ServerInformation.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/systemstatus/ServerInformation.java
index c7deedb..45aec57 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/systemstatus/ServerInformation.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/systemstatus/ServerInformation.java
@@ -17,7 +17,7 @@
 /** Exports current server information to an extension. */
 public interface ServerInformation {
   /** Current state of the server. */
-  public enum State {
+  enum State {
     /**
      * The server is starting up, and network connections are not yet being
      * accepted. Plugins or extensions starting during this time are starting
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
index ad74849..b9004f2 100644
--- 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
@@ -18,7 +18,7 @@
 import com.google.gerrit.extensions.common.WebLinkInfo;
 
 @ExtensionPoint
-public interface PatchSetWebLink extends WebLink{
+public interface PatchSetWebLink extends WebLink {
 
   /**
    * {@link com.google.gerrit.extensions.common.WebLinkInfo}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/TopMenu.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/TopMenu.java
index bc41813..7ad12cd 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/TopMenu.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/TopMenu.java
@@ -22,7 +22,7 @@
 
 @ExtensionPoint
 public interface TopMenu {
-  public class MenuEntry {
+  class MenuEntry {
     public final String name;
     public final List<MenuItem> items;
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/UiAction.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/UiAction.java
index fb81e0c..1807673 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/UiAction.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/UiAction.java
@@ -32,7 +32,7 @@
   Description getDescription(R resource);
 
   /** Describes an action invokable through the web interface. */
-  public static class Description {
+  class Description {
     private String method;
     private String id;
     private String label;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/WebLink.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/WebLink.java
index e497f7d..fd677ca 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/WebLink.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/WebLink.java
@@ -24,7 +24,7 @@
   /**
    * Class that holds target defaults for WebLink anchors.
    */
-  public static class Target {
+  class Target {
     /**
      * Opens the link in a new window or tab
      */
diff --git a/gerrit-gpg/BUCK b/gerrit-gpg/BUCK
index 5d193aa..73d9f04 100644
--- a/gerrit-gpg/BUCK
+++ b/gerrit-gpg/BUCK
@@ -8,8 +8,8 @@
   '//lib/guice:guice',
   '//lib/guice:guice-assistedinject',
   '//lib/guice:guice-servlet',
+  '//lib/jgit/org.eclipse.jgit:jgit',
   '//lib/log:api',
-  '@jgit//org.eclipse.jgit:jgit',
 ]
 
 java_library(
@@ -50,7 +50,7 @@
     '//lib:truth',
     '//lib/bouncycastle:bcpg',
     '//lib/bouncycastle:bcprov',
-    '@jgit//org.eclipse.jgit.junit:junit',
+    '//lib/jgit/org.eclipse.jgit.junit:junit',
   ],
   source_under_test = [':gpg'],
   visibility = ['//tools/eclipse:classpath'],
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java
index d3ca8c1..7e55d45 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java
@@ -246,7 +246,7 @@
     }
   }
 
-  private final AccountExternalId.Key toExtIdKey(byte[] fp) {
+  private AccountExternalId.Key toExtIdKey(byte[] fp) {
     return new AccountExternalId.Key(
         AccountExternalId.SCHEME_GPGKEY,
         BaseEncoding.base16().encode(fp));
diff --git a/gerrit-gwtdebug/src/main/java/com/google/gwt/dev/codeserver/WebServer.java b/gerrit-gwtdebug/src/main/java/com/google/gwt/dev/codeserver/WebServer.java
index 8072d75..728f276 100644
--- a/gerrit-gwtdebug/src/main/java/com/google/gwt/dev/codeserver/WebServer.java
+++ b/gerrit-gwtdebug/src/main/java/com/google/gwt/dev/codeserver/WebServer.java
@@ -91,7 +91,7 @@
 
   private static final MimeTypes MIME_TYPES = new MimeTypes();
 
-  private static final String TIME_IN_THE_PAST = "Fri, 01 Jan 1990 00:00:00 GMT";
+  private static final String TIME_IN_THE_PAST = "Mon, 01 Jan 1990 00:00:00 GMT";
 
   private final SourceHandler handler;
   private final JsonExporter jsonExporter;
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyResources.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyResources.java
index f3435cc..a97b392 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyResources.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyResources.java
@@ -21,7 +21,7 @@
 import com.google.gwt.resources.client.ImageResource;
 
 public interface ClippyResources extends ClientBundle {
-  public static final ClippyResources I = GWT.create(ClippyResources.class);
+  ClippyResources I = GWT.create(ClippyResources.class);
 
   @Source("clippy.css")
   ClippyCss css();
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabelText.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabelText.java
index 4d1b837..8e4d090 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabelText.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabelText.java
@@ -18,7 +18,7 @@
 import com.google.gwt.i18n.client.Constants;
 
 interface CopyableLabelText extends Constants {
-  static final CopyableLabelText I = GWT.create(CopyableLabelText.class);
+  CopyableLabelText I = GWT.create(CopyableLabelText.class);
 
   String tooltip();
   String copied();
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 d26ca8c..2b5984d 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
@@ -18,7 +18,7 @@
 import com.google.gwt.i18n.client.Constants;
 
 public interface KeyConstants extends Constants {
-  public static final KeyConstants I = GWT.create(KeyConstants.class);
+  KeyConstants I = GWT.create(KeyConstants.class);
 
   String applicationSection();
   String showHelp();
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyResources.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyResources.java
index a52ca2a..562e12d 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyResources.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyResources.java
@@ -18,7 +18,7 @@
 import com.google.gwt.resources.client.ClientBundle;
 
 public interface KeyResources extends ClientBundle {
-  public static final KeyResources I = GWT.create(KeyResources.class);
+  KeyResources I = GWT.create(KeyResources.class);
 
   @Source("key.css")
   KeyCss css();
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpTextArea.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpTextArea.java
index 38a488e..fd0da74 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpTextArea.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpTextArea.java
@@ -14,7 +14,6 @@
 
 package com.google.gwtexpui.globalkey.client;
 
-import com.google.gwt.dom.client.Element;
 import com.google.gwt.user.client.ui.TextArea;
 
 public class NpTextArea extends TextArea {
@@ -22,11 +21,6 @@
     addKeyPressHandler(GlobalKey.STOP_PROPAGATION);
   }
 
-  public NpTextArea(final Element element) {
-    super(element);
-    addKeyPressHandler(GlobalKey.STOP_PROPAGATION);
-  }
-
   public void setSpellCheck(boolean spell) {
     getElement().setPropertyBoolean("spellcheck", spell);
   }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressResources.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressResources.java
index 0276e9a..6bcf2c4 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressResources.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressResources.java
@@ -18,7 +18,7 @@
 import com.google.gwt.resources.client.ClientBundle;
 
 public interface ProgressResources extends ClientBundle {
-  public static final ProgressResources I = GWT.create(ProgressResources.class);
+  ProgressResources I = GWT.create(ProgressResources.class);
 
   @Source("progress.css")
   ProgressCss css();
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 6eaa7fd..a84835e 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
@@ -102,7 +102,7 @@
     }
   }
 
-  private static interface Tag {
+  private interface Tag {
     void assertSafe(String name, String value);
   }
 
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/Tooltip.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/Tooltip.java
index e3ab034..7939697 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/Tooltip.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/Tooltip.java
@@ -23,7 +23,7 @@
 /** Displays custom tooltip message below an element. */
 public class Tooltip {
   interface Resources extends ClientBundle {
-    static final Resources I = GWT.create(Resources.class);
+    Resources I = GWT.create(Resources.class);
 
     @Source("tooltip.css")
     Css css();
diff --git a/gerrit-gwtui-common/BUCK b/gerrit-gwtui-common/BUCK
index b417d30..a8ea06f 100644
--- a/gerrit-gwtui-common/BUCK
+++ b/gerrit-gwtui-common/BUCK
@@ -64,7 +64,7 @@
     ':client',
     '//lib:junit',
     '//lib/gwt:user',
-    '@jgit//org.eclipse.jgit:jgit',
+    '//lib/jgit/org.eclipse.jgit:jgit',
   ],
   source_under_test = [':client'],
   vm_args = ['-Xmx512m'],
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/CommonConstants.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/CommonConstants.java
index 032c47c..5d0b93f 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/CommonConstants.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/CommonConstants.java
@@ -18,7 +18,7 @@
 import com.google.gwt.i18n.client.Constants;
 
 public interface CommonConstants extends Constants {
-  public static final CommonConstants C = GWT.create(CommonConstants.class);
+  CommonConstants C = GWT.create(CommonConstants.class);
 
   String inTheFuture();
   String month();
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/CommonMessages.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/CommonMessages.java
index aa5e3cf..5a5b4a3 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/CommonMessages.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/CommonMessages.java
@@ -18,7 +18,7 @@
 import com.google.gwt.i18n.client.Messages;
 
 public interface CommonMessages extends Messages {
-  public static final CommonMessages M = GWT.create(CommonMessages.class);
+  CommonMessages M = GWT.create(CommonMessages.class);
 
   String secondsAgo(long seconds);
   String minutesAgo(long minutes);
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/GerritUiExtensionPoint.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/GerritUiExtensionPoint.java
index 9dcb111..088b6fd 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/GerritUiExtensionPoint.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/GerritUiExtensionPoint.java
@@ -20,6 +20,7 @@
   CHANGE_SCREEN_HEADER_RIGHT_OF_BUTTONS,
   CHANGE_SCREEN_HEADER_RIGHT_OF_POP_DOWNS,
   CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK,
+  CHANGE_SCREEN_BELOW_RELATED_INFO_BLOCK,
 
   /* MyPasswordScreen */
   PASSWORD_SCREEN_BOTTOM,
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/Resources.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/Resources.java
index ae7114f..95751fa 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/Resources.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/Resources.java
@@ -42,6 +42,9 @@
   @Source("resultset_up_gray.png")
   ImageResource arrowUp();
 
+  @Source("lightbulb.png")
+  ImageResource blame();
+
   @Source("page_white_put.png")
   ImageResource downloadIcon();
 
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AccountInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AccountInfo.java
index 6ac0404..830dcb3 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AccountInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AccountInfo.java
@@ -40,9 +40,9 @@
     return ts;
   }
 
-  private final native String registeredOnRaw() /*-{ return this.registered_on; }-*/;
-  private final native Timestamp _getRegisteredOn() /*-{ return this._cts; }-*/;
-  private final native void _setRegisteredOn(Timestamp ts) /*-{ this._cts = ts; }-*/;
+  private native String registeredOnRaw() /*-{ return this.registered_on; }-*/;
+  private native Timestamp _getRegisteredOn() /*-{ return this._cts; }-*/;
+  private native void _setRegisteredOn(Timestamp ts) /*-{ this._cts = ts; }-*/;
 
   /**
    * @return true if the server supplied avatar information about this account.
@@ -64,7 +64,7 @@
     return null;
   }
 
-  private final native JsArray<AvatarInfo> avatars()
+  private native JsArray<AvatarInfo> avatars()
   /*-{ return this.avatars }-*/;
 
   public final native void name(String n) /*-{ this.name = n }-*/;
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AuthInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AuthInfo.java
index 15d1c6c..0e3c32b 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AuthInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AuthInfo.java
@@ -90,8 +90,8 @@
   public final native String editFullNameUrl() /*-{ return this.edit_full_name_url; }-*/;
   public final native String httpPasswordUrl() /*-{ return this.http_password_url; }-*/;
   public final native boolean isGitBasicAuth() /*-{ return this.is_git_basic_auth || false; }-*/;
-  private final native String authTypeRaw() /*-{ return this.auth_type; }-*/;
-  private final native JsArrayString _editableAccountFields()
+  private native String authTypeRaw() /*-{ return this.auth_type; }-*/;
+  private native JsArrayString _editableAccountFields()
   /*-{ return this.editable_account_fields; }-*/;
 
   protected AuthInfo() {
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
index 2c85556..1153270 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
@@ -71,8 +71,8 @@
     return revList.get(revList.length() - 1).isEdit();
   }
 
-  private final native Timestamp _getCts() /*-{ return this._cts; }-*/;
-  private final native void _setCts(Timestamp ts) /*-{ this._cts = ts; }-*/;
+  private native Timestamp _getCts() /*-{ return this._cts; }-*/;
+  private native void _setCts(Timestamp ts) /*-{ this._cts = ts; }-*/;
 
   public final Timestamp updated() {
     return JavaSqlTimestamp_JsonSerializer.parseTimestamp(updatedRaw());
@@ -112,12 +112,12 @@
   public final native boolean mergeable() /*-{ return this.mergeable ? true : false; }-*/;
   public final native int insertions() /*-{ return this.insertions; }-*/;
   public final native int deletions() /*-{ return this.deletions; }-*/;
-  private final native String statusRaw() /*-{ return this.status; }-*/;
+  private native String statusRaw() /*-{ return this.status; }-*/;
   public final native String subject() /*-{ return this.subject; }-*/;
   public final native AccountInfo owner() /*-{ return this.owner; }-*/;
-  private final native String createdRaw() /*-{ return this.created; }-*/;
-  private final native String updatedRaw() /*-{ return this.updated; }-*/;
-  private final native String submittedRaw() /*-{ return this.submitted; }-*/;
+  private native String createdRaw() /*-{ return this.created; }-*/;
+  private native String updatedRaw() /*-{ return this.updated; }-*/;
+  private native String submittedRaw() /*-{ return this.submitted; }-*/;
   public final native boolean starred() /*-{ return this.starred ? true : false; }-*/;
   public final native boolean reviewed() /*-{ return this.reviewed ? true : false; }-*/;
   public final native NativeMap<LabelInfo> allLabels() /*-{ return this.labels; }-*/;
@@ -142,7 +142,7 @@
   public final native JsArray<AccountInfo> removableReviewers()
   /*-{ return this.removable_reviewers; }-*/;
 
-  private final native NativeMap<JsArray<AccountInfo>> _reviewers()
+  private native NativeMap<JsArray<AccountInfo>> _reviewers()
   /*-{ return this.reviewers; }-*/;
   public final Map<ReviewerState, List<AccountInfo>> reviewers() {
     NativeMap<JsArray<AccountInfo>> reviewers = _reviewers();
@@ -173,14 +173,14 @@
     }
     return SubmitType.valueOf(submitType);
   }
-  private final native String _submitType() /*-{ return this.submit_type; }-*/;
+  private native String _submitType() /*-{ return this.submit_type; }-*/;
 
   public final boolean submittable() {
     init();
     return _submittable();
   }
 
-  private final native boolean _submittable()
+  private native boolean _submittable()
   /*-{ return this.submittable ? true : false; }-*/;
 
   /**
@@ -259,7 +259,7 @@
       return null;
     }
 
-    private final native NativeMap<NativeString> _values() /*-{ return this.values; }-*/;
+    private native NativeMap<NativeString> _values() /*-{ return this.values; }-*/;
     public final Set<String> values() {
       return Natives.keys(_values());
     }
@@ -334,7 +334,7 @@
       revisionInfo.takeFromEdit(edit);
       return revisionInfo;
     }
-    private final native void takeFromEdit(EditInfo edit) /*-{
+    private native void takeFromEdit(EditInfo edit) /*-{
       this._number = 0;
       this.name = edit.name;
       this.commit = edit.commit;
@@ -435,7 +435,7 @@
   public static class GitPerson extends JavaScriptObject {
     public final native String name() /*-{ return this.name; }-*/;
     public final native String email() /*-{ return this.email; }-*/;
-    private final native String dateRaw() /*-{ return this.date; }-*/;
+    private native String dateRaw() /*-{ return this.date; }-*/;
 
     public final Timestamp date() {
       return JavaSqlTimestamp_JsonSerializer.parseTimestamp(dateRaw());
@@ -449,7 +449,7 @@
     public final native AccountInfo author() /*-{ return this.author; }-*/;
     public final native String message() /*-{ return this.message; }-*/;
     public final native int _revisionNumber() /*-{ return this._revision_number || 0; }-*/;
-    private final native String dateRaw() /*-{ return this.date; }-*/;
+    private native String dateRaw() /*-{ return this.date; }-*/;
 
     public final Timestamp date() {
       return JavaSqlTimestamp_JsonSerializer.parseTimestamp(dateRaw());
@@ -475,7 +475,7 @@
     public final native JsArrayString branches() /*-{ return this.branches; }-*/;
     public final native JsArrayString tags() /*-{ return this.tags; }-*/;
     public final native JsArrayString external(String n) /*-{ return this.external[n]; }-*/;
-    private final native NativeMap<JsArrayString> external() /*-{ return this.external; }-*/;
+    private native NativeMap<JsArrayString> external() /*-{ return this.external; }-*/;
 
     protected IncludedInInfo() {
     }
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/DownloadInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/DownloadInfo.java
index eee2847..183e6af 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/DownloadInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/DownloadInfo.java
@@ -39,8 +39,8 @@
   }
 
   public final native DownloadSchemeInfo scheme(String n) /*-{ return this.schemes[n]; }-*/;
-  private final native NativeMap<DownloadSchemeInfo> _schemes() /*-{ return this.schemes; }-*/;
-  private final native JsArrayString _archives() /*-{ return this.archives; }-*/;
+  private native NativeMap<DownloadSchemeInfo> _schemes() /*-{ return this.schemes; }-*/;
+  private native JsArrayString _archives() /*-{ return this.archives; }-*/;
 
   protected DownloadInfo() {
   }
@@ -96,8 +96,8 @@
     public final native boolean isAuthSupported() /*-{ return this.is_auth_supported || false; }-*/;
     public final native String command(String n) /*-{ return this.commands[n]; }-*/;
     public final native String cloneCommand(String n) /*-{ return this.clone_commands[n]; }-*/;
-    private final native NativeMap<NativeString> _commands() /*-{ return this.commands; }-*/;
-    private final native NativeMap<NativeString> _cloneCommands() /*-{ return this.clone_commands; }-*/;
+    private native NativeMap<NativeString> _commands() /*-{ return this.commands; }-*/;
+    private native NativeMap<NativeString> _cloneCommands() /*-{ return this.clone_commands; }-*/;
 
     protected DownloadSchemeInfo() {
     }
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/FileInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/FileInfo.java
index 555e06e..9b290a5 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/FileInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/FileInfo.java
@@ -37,12 +37,12 @@
   public final long size() {
     return (long)_size();
   }
-  private final native double _size() /*-{ return this.size || 0; }-*/;
+  private native double _size() /*-{ return this.size || 0; }-*/;
 
   public final long sizeDelta() {
     return (long)_sizeDelta();
   }
-  private final native double _sizeDelta() /*-{ return this.size_delta || 0; }-*/;
+  private native double _sizeDelta() /*-{ return this.size_delta || 0; }-*/;
 
   public final native int _row() /*-{ return this._row }-*/;
   public final native void _row(int r) /*-{ this._row = r }-*/;
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GeneralPreferences.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GeneralPreferences.java
index 40e6278..45953cb 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GeneralPreferences.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GeneralPreferences.java
@@ -65,7 +65,7 @@
         ? changesPerPage
         : GeneralPreferencesInfo.DEFAULT_PAGESIZE;
   }
-  private final native short get(String n, int d)
+  private native short get(String n, int d)
   /*-{ return this.hasOwnProperty(n) ? this[n] : d }-*/;
 
   public final native boolean showSiteHeader()
@@ -81,21 +81,21 @@
     String s = downloadCommandRaw();
     return s != null ? DownloadCommand.valueOf(s) : null;
   }
-  private final native String downloadCommandRaw()
+  private native String downloadCommandRaw()
   /*-{ return this.download_command }-*/;
 
   public final DateFormat dateFormat() {
     String s = dateFormatRaw();
     return s != null ? DateFormat.valueOf(s) : null;
   }
-  private final native String dateFormatRaw()
+  private 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()
+  private native String timeFormatRaw()
   /*-{ return this.time_format }-*/;
 
   public final native boolean relativeDateInChangeTable()
@@ -117,14 +117,14 @@
     String s = reviewCategeoryStrategyRaw();
     return s != null ? ReviewCategoryStrategy.valueOf(s) : ReviewCategoryStrategy.NONE;
   }
-  private final native String reviewCategeoryStrategyRaw()
+  private native String reviewCategeoryStrategyRaw()
   /*-{ return this.review_category_strategy }-*/;
 
   public final DiffView diffView() {
     String s = diffViewRaw();
     return s != null ? DiffView.valueOf(s) : null;
   }
-  private final native String diffViewRaw()
+  private native String diffViewRaw()
   /*-{ return this.diff_view }-*/;
 
   public final EmailStrategy emailStrategy() {
@@ -132,7 +132,7 @@
     return s != null ? EmailStrategy.valueOf(s) : null;
   }
 
-  private final native String emailStrategyRaw()
+  private native String emailStrategyRaw()
   /*-{ return this.email_strategy }-*/;
 
   public final native JsArray<TopMenuItem> my()
@@ -159,13 +159,13 @@
   public final void dateFormat(DateFormat f) {
     dateFormatRaw(f != null ? f.toString() : null);
   }
-  private final native void dateFormatRaw(String f)
+  private 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)
+  private native void timeFormatRaw(String f)
   /*-{ this.time_format = f }-*/;
 
   public final native void relativeDateInChangeTable(boolean d)
@@ -186,19 +186,19 @@
   public final void reviewCategoryStrategy(ReviewCategoryStrategy s) {
     reviewCategoryStrategyRaw(s != null ? s.toString() : null);
   }
-  private final native void reviewCategoryStrategyRaw(String s)
+  private native void reviewCategoryStrategyRaw(String s)
   /*-{ this.review_category_strategy = s }-*/;
 
   public final void diffView(DiffView d) {
     diffViewRaw(d != null ? d.toString() : null);
   }
-  private final native void diffViewRaw(String d)
+  private native void diffViewRaw(String d)
   /*-{ this.diff_view = d }-*/;
 
   public final void emailStrategy(EmailStrategy s) {
     emailStrategyRaw(s != null ? s.toString() : null);
   }
-  private final native void emailStrategyRaw(String s)
+  private native void emailStrategyRaw(String s)
   /*-{ this.email_strategy = s }-*/;
 
   public final void setMyMenus(List<TopMenuItem> myMenus) {
@@ -218,8 +218,8 @@
     return urlAliases;
   }
 
-  private final native String urlAliasToken(String m) /*-{ return this.url_aliases[m]; }-*/;
-  private final native NativeMap<NativeString> _urlAliases() /*-{ return this.url_aliases; }-*/;
+  private native String urlAliasToken(String m) /*-{ return this.url_aliases[m]; }-*/;
+  private native NativeMap<NativeString> _urlAliases() /*-{ return this.url_aliases; }-*/;
 
   public final void setUrlAliases(Map<String, String> urlAliases) {
     initUrlAliases();
@@ -227,8 +227,8 @@
       putUrlAlias(e.getKey(), e.getValue());
     }
   }
-  private final native void putUrlAlias(String m, String t) /*-{ this.url_aliases[m] = t; }-*/;
-  private final native void initUrlAliases() /*-{ this.url_aliases = {}; }-*/;
+  private native void putUrlAlias(String m, String t) /*-{ this.url_aliases[m] = t; }-*/;
+  private native void initUrlAliases() /*-{ this.url_aliases = {}; }-*/;
 
   protected GeneralPreferences() {
   }
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GitwebInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GitwebInfo.java
index eb5a697..93bd6c6 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GitwebInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GitwebInfo.java
@@ -156,7 +156,7 @@
     return url() + pattern.replace(p);
   }
 
-  private final String encode(String segment) {
+  private String encode(String segment) {
     if (type().urlEncode()) {
       return URL.encodeQueryString(type().replacePathSeparator(segment));
     } else {
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GpgKeyInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GpgKeyInfo.java
index f7477a1..7955347 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GpgKeyInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GpgKeyInfo.java
@@ -27,7 +27,7 @@
   public final native JsArrayString userIds() /*-{ return this.user_ids; }-*/;
   public final native String key() /*-{ return this.key; }-*/;
 
-  private final native String statusRaw() /*-{ return this.status; }-*/;
+  private native String statusRaw() /*-{ return this.status; }-*/;
   public final Status status() {
     String s = statusRaw();
     if (s == null) {
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ServerInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ServerInfo.java
index b0e52fa..c91d08d 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ServerInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ServerInfo.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.client.rpc.NativeString;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArrayString;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -43,7 +44,7 @@
   }
 
   public final native String urlAliasToken(String n) /*-{ return this.url_aliases[n]; }-*/;
-  private final native NativeMap<NativeString> _urlAliases() /*-{ return this.url_aliases; }-*/;
+  private native NativeMap<NativeString> _urlAliases() /*-{ return this.url_aliases; }-*/;
 
 
   public final boolean hasSshd() {
@@ -55,6 +56,7 @@
 
   public static class ChangeConfigInfo extends JavaScriptObject {
     public final native boolean allowDrafts() /*-{ return this.allow_drafts || false; }-*/;
+    public final native boolean allowBlame() /*-{ return this.allow_blame || false; }-*/;
     public final native int largeChange() /*-{ return this.large_change || 0; }-*/;
     public final native String replyLabel() /*-{ return this.reply_label; }-*/;
     public final native String replyTooltip() /*-{ return this.reply_tooltip; }-*/;
@@ -68,6 +70,8 @@
 
   public static class PluginConfigInfo extends JavaScriptObject {
     public final native boolean hasAvatars() /*-{ return this.has_avatars || false; }-*/;
+    public final native JsArrayString jsResourcePaths() /*-{
+        return this.js_resource_paths || []; }-*/;
 
     protected PluginConfigInfo() {
     }
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/NativeString.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/NativeString.java
index 0e16dc0..572d454 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/NativeString.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/NativeString.java
@@ -22,7 +22,7 @@
   public static final JavaScriptObject TYPE = init();
 
   // Used from core and plugins
-  private static final native JavaScriptObject init() /*-{
+  private static native JavaScriptObject init() /*-{
     if ($wnd.Gerrit === undefined || $wnd.Gerrit.JsonString === undefined) {
       return function(s){this.s=s};
     } else {
@@ -30,16 +30,16 @@
     }
   }-*/;
 
-  static final NativeString wrap(String s) {
+  static NativeString wrap(String s) {
     return wrap0(TYPE, s);
   }
 
-  private static final native NativeString wrap0(JavaScriptObject T, String s)
+  private static native NativeString wrap0(JavaScriptObject T, String s)
   /*-{ return new T(s) }-*/;
 
-  public final native String asString() /*-{ return this.s; }-*/;
+  public native String asString() /*-{ return this.s; }-*/;
 
-  public static final AsyncCallback<NativeString>
+  public static AsyncCallback<NativeString>
   unwrap(final AsyncCallback<String> cb) {
     return new AsyncCallback<NativeString>() {
       @Override
@@ -54,11 +54,11 @@
     };
   }
 
-  public static final boolean is(JavaScriptObject o) {
+  public static boolean is(JavaScriptObject o) {
     return is(TYPE, o);
   }
 
-  private static final native boolean is(JavaScriptObject T, JavaScriptObject o)
+  private static native boolean is(JavaScriptObject T, JavaScriptObject o)
   /*-{ return o instanceof T }-*/;
 
   protected NativeString() {
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/TransformCallback.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/TransformCallback.java
index 93c86a2..00e6bb1 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/TransformCallback.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/TransformCallback.java
@@ -17,7 +17,7 @@
 import com.google.gwt.user.client.rpc.AsyncCallback;
 
 /** Transforms a value and passes it on to another callback. */
-public abstract class TransformCallback<I, O> implements AsyncCallback<I>{
+public abstract class TransformCallback<I, O> implements AsyncCallback<I> {
   private final AsyncCallback<O> callback;
 
   protected TransformCallback(AsyncCallback<O> callback) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java
index bec89cc..4ca1721 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java
@@ -46,18 +46,6 @@
    * @param size A requested size. Note that the size can be ignored depending
    *        on the avatar provider. A size <= 0 indicates to let the provider
    *        decide a default size.
-   */
-  public AvatarImage(AccountInfo account, int size) {
-    this(account, size, true);
-  }
-
-  /**
-   * An avatar image for the given account using the requested size.
-   *
-   * @param account The account in which we are interested
-   * @param size A requested size. Note that the size can be ignored depending
-   *        on the avatar provider. A size <= 0 indicates to let the provider
-   *        decide a default size.
    * @param addPopup show avatar popup with user info on hovering over the
    *        avatar image
    */
@@ -138,7 +126,7 @@
     private Timer showTimer;
     private Timer hideTimer;
 
-    public PopupHandler(AccountInfo account, UIObject target) {
+    PopupHandler(AccountInfo account, UIObject target) {
       this.account = account;
       this.target = target;
     }
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 79b4135..7875ae8 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
@@ -88,7 +88,6 @@
 import com.google.gerrit.client.editor.EditScreen;
 import com.google.gerrit.client.groups.GroupApi;
 import com.google.gerrit.client.groups.GroupInfo;
-import com.google.gerrit.client.patches.UnifiedPatchScreen;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.RestApi;
 import com.google.gerrit.client.ui.Screen;
@@ -425,7 +424,7 @@
       int line = 0;
       int at = rest.lastIndexOf('@');
       if (at > 0) {
-        String l = rest.substring(at+1);
+        String l = rest.substring(at + 1);
         if (l.startsWith("a")) {
           side = DisplaySide.A;
           l = l.substring(1);
@@ -486,8 +485,6 @@
       codemirror(token, baseId, id, side, line, false);
     } else if ("unified".equals(panel)) {
       unified(token, baseId, id, side, line);
-    } else if ("unified1".equals(panel)) {
-      unified1(token, baseId, id);
     } else if ("edit".equals(panel)) {
       codemirror(token, null, id, side, line, true);
     } else {
@@ -511,18 +508,6 @@
     });
   }
 
-  private static void unified1(final String token,
-      final PatchSet.Id baseId,
-      final Patch.Key id) {
-    GWT.runAsync(new AsyncSplit(token) {
-      @Override
-      public void onSuccess() {
-        UnifiedPatchScreen.TopView top = Gerrit.getPatchScreenTopView();
-        Gerrit.display(token, new UnifiedPatchScreen(id, top, baseId));
-      }
-    });
-  }
-
   private static void codemirror(final String token, final PatchSet.Id baseId,
       final Patch.Key id, final DisplaySide side, final int line,
       final boolean edit) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java
index a5c5659..87cb4b3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java
@@ -27,7 +27,6 @@
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Label;
 import com.google.gwt.user.client.ui.PopupPanel;
-import com.google.gwt.user.client.ui.Widget;
 import com.google.gwtexpui.safehtml.client.SafeHtml;
 import com.google.gwtjsonrpc.client.RemoteJsonException;
 
@@ -96,11 +95,6 @@
     body.add(message.toBlockWidget());
   }
 
-  public ErrorDialog(final Widget w) {
-    this();
-    body.add(w);
-  }
-
   /** Create a dialog box to nicely format an exception. */
   public ErrorDialog(final Throwable what) {
     this();
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 efa4612..d32b745 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
@@ -26,7 +26,6 @@
 import com.google.gerrit.client.api.ApiGlue;
 import com.google.gerrit.client.api.PluginLoader;
 import com.google.gerrit.client.change.LocalComments;
-import com.google.gerrit.client.changes.ChangeConstants;
 import com.google.gerrit.client.changes.ChangeListScreen;
 import com.google.gerrit.client.config.ConfigServerApi;
 import com.google.gerrit.client.documentation.DocInfo;
@@ -37,7 +36,6 @@
 import com.google.gerrit.client.info.TopMenu;
 import com.google.gerrit.client.info.TopMenuItem;
 import com.google.gerrit.client.info.TopMenuList;
-import com.google.gerrit.client.patches.UnifiedPatchScreen;
 import com.google.gerrit.client.rpc.CallbackGroup;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.Natives;
@@ -56,7 +54,6 @@
 import com.google.gwt.aria.client.Roles;
 import com.google.gwt.core.client.EntryPoint;
 import com.google.gwt.core.client.GWT;
-import com.google.gwt.dom.client.AnchorElement;
 import com.google.gwt.dom.client.Document;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
@@ -102,7 +99,6 @@
 
 public class Gerrit implements EntryPoint {
   public static final GerritConstants C = GWT.create(GerritConstants.class);
-  public static final ChangeConstants CC = GWT.create(ChangeConstants.class);
   public static final GerritMessages M = GWT.create(GerritMessages.class);
   public static final GerritResources RESOURCES =
       GWT.create(GerritResources.class);
@@ -138,7 +134,6 @@
   private static SearchPanel searchPanel;
   private static final Dispatcher dispatcher = new Dispatcher();
   private static ViewSite<Screen> body;
-  private static UnifiedPatchScreen patchScreen;
   private static String lastChangeListToken;
   private static String lastViewToken;
 
@@ -152,13 +147,6 @@
     Window.Location.reload();
   }
 
-  public static UnifiedPatchScreen.TopView getPatchScreenTopView() {
-    if (patchScreen == null) {
-      return null;
-    }
-    return patchScreen.getTopView();
-  }
-
   public static void displayLastChangeList() {
     if (lastChangeListToken != null) {
       display(lastChangeListToken);
@@ -214,26 +202,6 @@
     }
   }
 
-  /**
-   * Update any top level menus which can vary based on the view which was
-   * loaded.
-   * @param view the loaded view.
-   */
-  public static void updateMenus(Screen view) {
-    LinkMenuBar diffBar = menuBars.get(GerritTopMenu.DIFFERENCES.menuName);
-    if (view instanceof UnifiedPatchScreen) {
-      patchScreen = (UnifiedPatchScreen) view;
-      menuLeft.setVisible(diffBar, true);
-      menuLeft.selectTab(menuLeft.getWidgetIndex(diffBar));
-    } else {
-      if (patchScreen != null && menuLeft.getSelectedWidget() == diffBar) {
-        menuLeft.selectTab(isSignedIn() ? 1 : 0);
-      }
-      patchScreen = null;
-      menuLeft.setVisible(diffBar, false);
-    }
-  }
-
   public static void selectMenu(LinkMenuBar bar) {
     menuLeft.selectTab(menuLeft.getWidgetIndex(bar));
   }
@@ -718,15 +686,6 @@
       menuLeft.selectTab(0);
     }
 
-    patchScreen = null;
-    LinkMenuBar diffBar = new LinkMenuBar();
-    menuBars.put(GerritTopMenu.DIFFERENCES.menuName, diffBar);
-    menuLeft.addInvisible(diffBar, C.menuDiff());
-    addDiffLink(diffBar, C.menuDiffCommit(), UnifiedPatchScreen.TopView.COMMIT);
-    addDiffLink(diffBar, C.menuDiffPreferences(), UnifiedPatchScreen.TopView.PREFERENCES);
-    addDiffLink(diffBar, C.menuDiffPatchSets(), UnifiedPatchScreen.TopView.PATCH_SETS);
-    addDiffLink(diffBar, C.menuDiffFiles(), UnifiedPatchScreen.TopView.FILES);
-
     final LinkMenuBar projectsBar = new LinkMenuBar();
     menuBars.put(GerritTopMenu.PROJECTS.menuName, projectsBar);
     addLink(projectsBar, C.menuProjectsList(), PageLinks.ADMIN_PROJECTS);
@@ -975,7 +934,7 @@
 
       @Override
       public void onKeyDown(KeyDownEvent event) {
-        if(event.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
+        if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
           showHidePopup();
           event.preventDefault();
         }
@@ -1018,19 +977,6 @@
     m.insertItem(new LinkMenuItem(text, historyToken), beforeIndex);
   }
 
-  private static void addDiffLink(final LinkMenuBar m, final String text,
-      final UnifiedPatchScreen.TopView tv) {
-    m.addItem(new LinkMenuItem(text, "") {
-        @Override
-        public void go() {
-          if (patchScreen != null) {
-            patchScreen.setTopView(tv);
-          }
-          AnchorElement.as(getElement()).blur();
-        }
-      });
-  }
-
   private static LinkMenuItem addProjectLink(LinkMenuBar m, TopMenuItem item) {
     LinkMenuItem i = new ProjectLinkMenuItem(item.getName(), item.getUrl()) {
         @Override
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 4d0165d..980f27d 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
@@ -37,27 +37,9 @@
   String cSIZE();
   String cSUBJECT();
   String cSTATUS();
-  String cellsNextToFileComment();
-  String changeScreenDescription();
-  String changeScreenStarIcon();
   String changeSize();
   String changeTable();
   String changeTablePrevNextLinks();
-  String changeTypeCell();
-  String commentCell();
-  String commentEditorPanel();
-  String commentHolder();
-  String commentHolderLeftmost();
-  String commentPanel();
-  String commentPanelAuthorCell();
-  String commentPanelButtons();
-  String commentPanelContent();
-  String commentPanelDateCell();
-  String commentPanelHeader();
-  String commentPanelLast();
-  String commentPanelMessage();
-  String commentPanelSummary();
-  String commentPanelSummaryCell();
   String commentedActionDialog();
   String commentedActionMessage();
   String contributorAgreementAlreadySubmitted();
@@ -69,14 +51,6 @@
   String dataCellHidden();
   String dataHeader();
   String dataHeaderHidden();
-  String diffLinkCell();
-  String diffText();
-  String diffTextCONTEXT();
-  String diffTextDELETE();
-  String diffTextFileHeader();
-  String diffTextHunkHeader();
-  String diffTextINSERT();
-  String diffTextNoLF();
   String downloadBox();
   String downloadBoxTable();
   String downloadBoxTableCommandColumn();
@@ -89,7 +63,6 @@
   String downloadLinkHeaderGap();
   String downloadLinkList();
   String downloadLink_Active();
-  String drafts();
   String editHeadButton();
   String emptySection();
   String errorDialog();
@@ -99,12 +72,6 @@
   String errorDialogTitle();
   String extensionPanel();
   String loadingPluginsDialog();
-  String fileColumnHeader();
-  String fileCommentBorder();
-  String fileLine();
-  String fileLineDELETE();
-  String fileLineINSERT();
-  String filePathCell();
   String gerritBody();
   String gerritTopMenu();
   String greenCheckClass();
@@ -120,18 +87,15 @@
   String groupUUIDPanel();
   String header();
   String iconCell();
-  String iconCellOfFileCommentRow();
   String iconHeader();
   String identityUntrustedExternalId();
   String infoBlock();
   String inputFieldTypeHint();
   String labelNotApplicable();
   String leftMostCell();
-  String lineNumber();
   String link();
   String linkMenuBar();
   String linkMenuItemNotLast();
-  String linkPanel();
   String maxObjectSizeLimitEffectiveLabel();
   String menuBarUserName();
   String menuBarUserNameAvatar();
@@ -141,18 +105,8 @@
   String menuScreenMenuBar();
   String needsReview();
   String negscore();
-  String noborder();
-  String nowrap();
   String pagingLink();
-  String patchBrowserPopup();
-  String patchBrowserPopupBody();
-  String patchCellReverseDiff();
-  String patchContentTable();
-  String patchHistoryTable();
-  String patchHistoryTablePatchSetHeader();
-  String patchNoDifference();
   String patchSetActions();
-  String patchSizeCell();
   String pluginProjectConfigInheritedValue();
   String pluginsTable();
   String posscore();
@@ -166,18 +120,14 @@
   String registerScreenExplain();
   String registerScreenNextLinks();
   String registerScreenSection();
-  String reviewedPanelBottom();
-  String rightBorder();
   String rpcStatus();
   String screen();
   String screenHeader();
   String searchPanel();
   String suggestBoxPopup();
   String sectionHeader();
-  String sideBySideScreenLinkTable();
   String singleLine();
   String smallHeading();
-  String sourceFilePath();
   String specialBranchDataCell();
   String specialBranchIconCell();
   String sshHostKeyPanel();
@@ -188,15 +138,12 @@
   String sshKeyPanelInvalid();
   String sshKeyTable();
   String stringListPanelButtons();
-  String topMostCell();
   String topmenu();
   String topmenuMenuLeft();
   String topmenuMenuRight();
   String topmenuTDglue();
   String topmenuTDmenu();
   String topmost();
-  String unifiedTable();
-  String unifiedTableHeader();
   String userInfoPopup();
   String usernameField();
   String watchedProjectFilter();
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SecureStoreException.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/RangeInfo.java
similarity index 61%
rename from gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SecureStoreException.java
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/RangeInfo.java
index 9693399..f86d8f4 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SecureStoreException.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/RangeInfo.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2016 The Android Open 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,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.pgm.util;
+package com.google.gerrit.client;
 
-public class SecureStoreException extends RuntimeException {
-  private static final long serialVersionUID = 5581700510568485065L;
+import com.google.gwt.core.client.JavaScriptObject;
 
-  SecureStoreException(String msg) {
-    super(msg);
-  }
+public class RangeInfo extends JavaScriptObject {
+  public final native int start() /*-{ return this.start; }-*/;
 
-  SecureStoreException(String msg, Exception e) {
-    super(msg, e);
+  public final native int end() /*-{ return this.end; }-*/;
+
+  protected RangeInfo() {
   }
 }
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 afbaf85..54c5b92 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
@@ -29,7 +29,9 @@
   private static final List<ParamSuggester> paramSuggester = Arrays.asList(
       new ParamSuggester(Arrays.asList("project:", "parentproject:"),
           new ProjectNameSuggestOracle()),
-      new ParamSuggester(Arrays.asList("owner:", "reviewer:"),
+      new ParamSuggester(Arrays.asList(
+          "owner:", "reviewer:", "commentby:", "reviewedby:", "author:",
+          "committer:", "from:"),
           new AccountSuggestOracle() {
             @Override
             public void onRequestSuggestions(final Request request, final Callback done) {
@@ -102,6 +104,8 @@
     suggestions.add("has:draft");
     suggestions.add("has:edit");
     suggestions.add("has:star");
+    suggestions.add("has:stars");
+    suggestions.add("star:");
 
     suggestions.add("is:");
     suggestions.add("is:starred");
@@ -124,6 +128,7 @@
     suggestions.add("status:closed");
     suggestions.add("status:merged");
     suggestions.add("status:abandoned");
+    suggestions.add("status:draft");
 
     suggestions.add("added:");
     suggestions.add("deleted:");
@@ -205,7 +210,7 @@
   private static class SearchSuggestion implements SuggestOracle.Suggestion {
     private final String suggestion;
     private final String fullQuery;
-    public SearchSuggestion(String suggestion, String fullQuery) {
+    SearchSuggestion(String suggestion, String fullQuery) {
       this.suggestion = suggestion;
       // Add a space to the query if it is a complete operation (e.g.
       // "status:open") so the user can keep on typing.
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 7d543d8..a3fc6dd 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
@@ -44,6 +44,7 @@
     p.theme(in.theme);
     p.hideEmptyPane(in.hideEmptyPane);
     p.retainHeader(in.retainHeader);
+    p.skipUnchanged(in.skipUnchanged);
     p.skipUncommented(in.skipUncommented);
     p.skipDeleted(in.skipDeleted);
     p.matchBrackets(in.matchBrackets);
@@ -63,6 +64,7 @@
     p.showTabs = showTabs();
     p.showWhitespaceErrors = showWhitespaceErrors();
     p.skipDeleted = skipDeleted();
+    p.skipUnchanged = skipUnchanged();
     p.skipUncommented = skipUncommented();
     p.syntaxHighlighting = syntaxHighlighting();
     p.hideTopMenu = hideTopMenu();
@@ -138,6 +140,7 @@
   public final native void renderEntireFile(boolean r) /*-{ this.render_entire_file = r }-*/;
   public final native void retainHeader(boolean r) /*-{ this.retain_header = r }-*/;
   public final native void hideEmptyPane(boolean s) /*-{ this.hide_empty_pane = s }-*/;
+  public final native void skipUnchanged(boolean s) /*-{ this.skip_unchanged = s }-*/;
   public final native void skipUncommented(boolean s) /*-{ this.skip_uncommented = s }-*/;
   public final native void skipDeleted(boolean s) /*-{ this.skip_deleted = s }-*/;
   public final native void matchBrackets(boolean m) /*-{ this.match_brackets = m }-*/;
@@ -154,15 +157,16 @@
   public final native boolean renderEntireFile() /*-{ return this.render_entire_file || false }-*/;
   public final native boolean hideEmptyPane() /*-{ return this.hide_empty_pane || false }-*/;
   public final native boolean retainHeader() /*-{ return this.retain_header || false }-*/;
+  public final native boolean skipUnchanged() /*-{ return this.skip_unchanged || false }-*/;
   public final native boolean skipUncommented() /*-{ return this.skip_uncommented || false }-*/;
   public final native boolean skipDeleted() /*-{ return this.skip_deleted || false }-*/;
   public final native boolean matchBrackets() /*-{ return this.match_brackets || false }-*/;
 
-  private final native void setThemeRaw(String i) /*-{ this.theme = i }-*/;
-  private final native void setIgnoreWhitespaceRaw(String i) /*-{ this.ignore_whitespace = i }-*/;
-  private final native String ignoreWhitespaceRaw() /*-{ return this.ignore_whitespace }-*/;
-  private final native String themeRaw() /*-{ return this.theme }-*/;
-  private final native int get(String n, int d) /*-{ return this.hasOwnProperty(n) ? this[n] : d }-*/;
+  private native void setThemeRaw(String i) /*-{ this.theme = i }-*/;
+  private native void setIgnoreWhitespaceRaw(String i) /*-{ this.ignore_whitespace = i }-*/;
+  private native String ignoreWhitespaceRaw() /*-{ return this.ignore_whitespace }-*/;
+  private native String themeRaw() /*-{ return this.theme }-*/;
+  private native int get(String n, int d) /*-{ return this.hasOwnProperty(n) ? this[n] : d }-*/;
 
   protected DiffPreferences() {
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/EditPreferences.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/EditPreferences.java
index d37ddd5..c710fe1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/EditPreferences.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/EditPreferences.java
@@ -24,6 +24,7 @@
     EditPreferences p = createObject().cast();
     p.tabSize(in.tabSize);
     p.lineLength(in.lineLength);
+    p.indentUnit(in.indentUnit);
     p.cursorBlinkRate(in.cursorBlinkRate);
     p.hideTopMenu(in.hideTopMenu);
     p.showTabs(in.showTabs);
@@ -32,6 +33,7 @@
     p.hideLineNumbers(in.hideLineNumbers);
     p.matchBrackets(in.matchBrackets);
     p.autoCloseBrackets(in.autoCloseBrackets);
+    p.showBase(in.showBase);
     p.theme(in.theme);
     p.keyMapType(in.keyMapType);
     return p;
@@ -40,6 +42,7 @@
   public final EditPreferencesInfo copyTo(EditPreferencesInfo p) {
     p.tabSize = tabSize();
     p.lineLength = lineLength();
+    p.indentUnit = indentUnit();
     p.cursorBlinkRate = cursorBlinkRate();
     p.hideTopMenu = hideTopMenu();
     p.showTabs = showTabs();
@@ -48,6 +51,7 @@
     p.hideLineNumbers = hideLineNumbers();
     p.matchBrackets = matchBrackets();
     p.autoCloseBrackets = autoCloseBrackets();
+    p.showBase = showBase();
     p.theme = theme();
     p.keyMapType = keyMapType();
     return p;
@@ -56,15 +60,16 @@
   public final void theme(Theme i) {
     setThemeRaw(i != null ? i.toString() : Theme.DEFAULT.toString());
   }
-  private final native void setThemeRaw(String i) /*-{ this.theme = i }-*/;
+  private native void setThemeRaw(String i) /*-{ this.theme = i }-*/;
 
   public final void keyMapType(KeyMapType i) {
     setkeyMapTypeRaw(i != null ? i.toString() : KeyMapType.DEFAULT.toString());
   }
-  private final native void setkeyMapTypeRaw(String i) /*-{ this.key_map_type = i }-*/;
+  private native void setkeyMapTypeRaw(String i) /*-{ this.key_map_type = i }-*/;
 
   public final native void tabSize(int t) /*-{ this.tab_size = t }-*/;
   public final native void lineLength(int c) /*-{ this.line_length = c }-*/;
+  public final native void indentUnit(int c) /*-{ this.indent_unit = c }-*/;
   public final native void cursorBlinkRate(int r) /*-{ this.cursor_blink_rate = r }-*/;
   public final native void hideTopMenu(boolean s) /*-{ this.hide_top_menu = s }-*/;
   public final native void showTabs(boolean s) /*-{ this.show_tabs = s }-*/;
@@ -73,18 +78,19 @@
   public final native void hideLineNumbers(boolean s) /*-{ this.hide_line_numbers = s }-*/;
   public final native void matchBrackets(boolean m) /*-{ this.match_brackets = m }-*/;
   public final native void autoCloseBrackets(boolean c) /*-{ this.auto_close_brackets = c }-*/;
+  public final native void showBase(boolean s) /*-{ this.show_base = s }-*/;
 
   public final Theme theme() {
     String s = themeRaw();
     return s != null ? Theme.valueOf(s) : Theme.DEFAULT;
   }
-  private final native String themeRaw() /*-{ return this.theme }-*/;
+  private native String themeRaw() /*-{ return this.theme }-*/;
 
   public final KeyMapType keyMapType() {
     String s = keyMapTypeRaw();
     return s != null ? KeyMapType.valueOf(s) : KeyMapType.DEFAULT;
   }
-  private final native String keyMapTypeRaw() /*-{ return this.key_map_type }-*/;
+  private native String keyMapTypeRaw() /*-{ return this.key_map_type }-*/;
 
   public final int tabSize() {
     return get("tab_size", 8);
@@ -94,6 +100,10 @@
     return get("line_length", 100);
   }
 
+  public final int indentUnit() {
+    return get("indent_unit", 2);
+  }
+
   public final int cursorBlinkRate() {
     return get("cursor_blink_rate", 0);
   }
@@ -105,7 +115,8 @@
   public final native boolean hideLineNumbers() /*-{ return this.hide_line_numbers || false }-*/;
   public final native boolean matchBrackets() /*-{ return this.match_brackets || false }-*/;
   public final native boolean autoCloseBrackets() /*-{ return this.auto_close_brackets || false }-*/;
-  private final native int get(String n, int d) /*-{ return this.hasOwnProperty(n) ? this[n] : d }-*/;
+  public final native boolean showBase() /*-{ return this.show_base || false }-*/;
+  private native int get(String n, int d) /*-{ return this.hasOwnProperty(n) ? this[n] : d }-*/;
 
   protected EditPreferences() {
   }
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 e8c58ef..405ef68 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
@@ -81,7 +81,7 @@
 
   private void linkByPlugin(String pluginName, String text, String target) {
     if (ambiguousMenuNames.contains(text)) {
-      text += " ("+ pluginName + ")";
+      text += " (" + pluginName + ")";
     }
     link(text, target);
   }
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 8f5d7a8..39849c4 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
@@ -57,11 +57,6 @@
     this(null, null, change, null, revision, action);
   }
 
-  public ActionButton(ChangeInfo change, EditInfo edit,
-      ActionInfo action) {
-    this(null, null, change, edit, null, action);
-  }
-
   private ActionButton(Project.NameKey project, BranchInfo branch,
       ChangeInfo change, EditInfo edit, RevisionInfo revision,
       ActionInfo action) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java
index cd72686..a71dffe 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java
@@ -211,7 +211,9 @@
   protected void display(final GroupInfo group, final boolean canModify) {
     groupUUIDLabel.setText(group.getGroupUUID().get());
     groupNameTxt.setText(group.name());
-    ownerTxt.setText(group.owner() != null?group.owner():Util.M.deletedReference(group.getOwnerUUID().get()));
+    ownerTxt.setText(group.owner() != null
+        ? group.owner()
+        : Util.M.deletedReference(group.getOwnerUUID().get()));
     descTxt.setText(group.description());
     visibleToAllCheckBox.setValue(group.options().isVisibleToAll());
     setMembersTabVisible(AccountGroup.isInternalGroup(group.getGroupUUID()));
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminResources.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminResources.java
index 90ae162..688a59f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminResources.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminResources.java
@@ -19,7 +19,7 @@
 import com.google.gwt.resources.client.ImageResource;
 
 public interface AdminResources extends ClientBundle {
-  public static final AdminResources I = GWT.create(AdminResources.class);
+  AdminResources I = GWT.create(AdminResources.class);
 
   @Source("admin.css")
   AdminCss css();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
index bfe7787..86222ce 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
@@ -126,7 +126,7 @@
       @Override
       public void onKeyUp(KeyUpEvent event) {
         Query q = new Query(filterTxt.getValue())
-          .open(event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER);
+          .open(event.getNativeKeyCode() == KeyCodes.KEY_ENTER);
         if (match.equals(q.qMatch)) {
           q.start(start);
         }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupReferenceBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupReferenceBox.java
index c1082bc..d6d0fe3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupReferenceBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupReferenceBox.java
@@ -47,7 +47,7 @@
             toValue(event.getSelectedItem()));
       }
     });
-    suggestBox.addCloseHandler(new CloseHandler<RemoteSuggestBox>(){
+    suggestBox.addCloseHandler(new CloseHandler<RemoteSuggestBox>() {
       @Override
       public void onClose(CloseEvent<RemoteSuggestBox> event) {
         suggestBox.setText("");
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java
index aed1dc2..64fc0e5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java
@@ -113,7 +113,7 @@
         return a.name().compareTo(b.name());
       }
     });
-    for(GroupInfo group : list.subList(fromIndex, toIndex)) {
+    for (GroupInfo group : list.subList(fromIndex, toIndex)) {
       final int row = table.getRowCount();
       table.insertRow(row);
       applyDataRowStyle(row);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java
index 0bf1b4b..6345207 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java
@@ -183,7 +183,7 @@
     driver.edit(mock);
   }
 
-  @UiHandler(value={"cancel1", "cancel2"})
+  @UiHandler(value = {"cancel1", "cancel2"})
   void onCancel(@SuppressWarnings("unused") ClickEvent event) {
     Gerrit.display(PageLinks.toProjectAcceess(getProjectKey()));
   }
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 c6bd1d1..c948a8e 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
@@ -727,7 +727,7 @@
   private static class LabeledWidgetsGrid extends FlexTable {
     private String labelSuffix;
 
-    public LabeledWidgetsGrid() {
+    LabeledWidgetsGrid() {
       super();
       labelSuffix = ":";
     }
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 3c6f0b2..f31b116 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
@@ -183,7 +183,7 @@
       @Override
       public void onKeyUp(KeyUpEvent event) {
         Query q = new Query(filterTxt.getValue())
-          .open(event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER);
+          .open(event.getNativeKeyCode() == KeyCodes.KEY_ENTER);
         if (match.equals(q.qMatch)) {
           q.start(start);
         }
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 7a48533..626252a 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
@@ -236,14 +236,14 @@
     }
   }
 
-  private static final String getPluginName() {
+  private static String getPluginName() {
     if (pluginName != null) {
       return pluginName;
     }
     return PluginName.fromUrl(PluginName.getCallerUrl());
   }
 
-  private static final void go(String urlOrToken) {
+  private static void go(String urlOrToken) {
     if (urlOrToken.startsWith("http:")
         || urlOrToken.startsWith("https:")
         || urlOrToken.startsWith("//")) {
@@ -253,35 +253,35 @@
     }
   }
 
-  private static final void refresh() {
+  private static void refresh() {
     Gerrit.display(History.getToken());
   }
 
-  private static final ServerInfo getServerInfo() {
+  private static ServerInfo getServerInfo() {
     return Gerrit.info();
   }
 
-  private static final AccountInfo getCurrentUser() {
+  private static AccountInfo getCurrentUser() {
     return Gerrit.getUserAccount();
   }
 
-  private static final GeneralPreferences getUserPreferences() {
+  private static GeneralPreferences getUserPreferences() {
     return Gerrit.getUserPreferences();
   }
 
-  private static final void refreshUserPreferences() {
+  private static void refreshUserPreferences() {
     Gerrit.refreshUserPreferences();
   }
 
-  private static final void refreshMenuBar() {
+  private static void refreshMenuBar() {
     Gerrit.refreshMenuBar();
   }
 
-  private static final boolean isSignedIn() {
+  private static boolean isSignedIn() {
     return Gerrit.isSignedIn();
   }
 
-  private static final void showError(String message) {
+  private static void showError(String message) {
     new ErrorDialog(message).center();
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ChangeGlue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ChangeGlue.java
index 82b6810..1e77773 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ChangeGlue.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ChangeGlue.java
@@ -55,11 +55,11 @@
     }
   }
 
-  private static final native JavaScriptObject get(String id) /*-{
+  private static native JavaScriptObject get(String id) /*-{
     return $wnd.Gerrit.change_actions[id];
   }-*/;
 
-  private static final native boolean invoke(JavaScriptObject h,
+  private static native boolean invoke(JavaScriptObject h,
       ChangeInfo a, RevisionInfo r)
   /*-{ return h(a,r) }-*/;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/EditGlue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/EditGlue.java
index 49b150a..b7e3df3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/EditGlue.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/EditGlue.java
@@ -45,7 +45,7 @@
     }
   }
 
-  private static final native JavaScriptObject get(String id) /*-{
+  private static native JavaScriptObject get(String id) /*-{
     return $wnd.Gerrit.edit_actions[id];
   }-*/;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionPanel.java
index 0702cba..e20c577 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionPanel.java
@@ -130,14 +130,14 @@
     final native void putBoolean(String k, boolean v) /*-{ this.p[k] = v; }-*/;
     final native void putObject(String k, JavaScriptObject v) /*-{ this.p[k] = v; }-*/;
 
-    private static final native Context create(
+    private static native Context create(
         JavaScriptObject T,
         Definition d,
         Element e)
     /*-{ return new T(d,e) }-*/;
 
     private static final JavaScriptObject TYPE = init();
-    private static final native JavaScriptObject init() /*-{
+    private static native JavaScriptObject init() /*-{
       var T = function(d,e) {
         this._d = d;
         this._u = [];
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionScreen.java
index 98b06bb..19dbe06 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionScreen.java
@@ -109,7 +109,7 @@
     final native void onLoad() /*-{ this._d.onLoad(this) }-*/;
     final native JsArray<JavaScriptObject> unload() /*-{ return this._u }-*/;
 
-    private static final native Context create(
+    private static native Context create(
         JavaScriptObject T,
         Definition d,
         ExtensionScreen s,
@@ -118,7 +118,7 @@
     /*-{ return new T(d,s,e,m) }-*/;
 
     private static final JavaScriptObject TYPE = init();
-    private static final native JavaScriptObject init() /*-{
+    private static native JavaScriptObject init() /*-{
       var T = function(d,s,e,m) {
         this._d = d;
         this._s = s;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionSettingsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionSettingsScreen.java
index c351bbf..1d7259b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionSettingsScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionSettingsScreen.java
@@ -91,7 +91,7 @@
       return Natives.keys(settingsScreens());
     }
 
-    private static final native NativeMap<NativeString> settingsScreens()
+    private static native NativeMap<NativeString> settingsScreens()
     /*-{ return $wnd.Gerrit.settingsScreens; }-*/;
 
     public final native String getPath() /*-{ return this.path; }-*/;
@@ -114,7 +114,7 @@
     final native void onLoad() /*-{ this._d.onLoad(this) }-*/;
     final native JsArray<JavaScriptObject> unload() /*-{ return this._u }-*/;
 
-    private static final native Context create(
+    private static native Context create(
         JavaScriptObject T,
         Definition d,
         ExtensionSettingsScreen s,
@@ -122,7 +122,7 @@
     /*-{ return new T(d,s,e) }-*/;
 
     private static final JavaScriptObject TYPE = init();
-    private static final native JavaScriptObject init() /*-{
+    private static native JavaScriptObject init() /*-{
       var T = function(d,s,e) {
         this._d = d;
         this._s = s;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/HtmlTemplate.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/HtmlTemplate.java
index 95757ab..6af244a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/HtmlTemplate.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/HtmlTemplate.java
@@ -62,19 +62,19 @@
     };
   }-*/;
 
-  private static final String css(String css) {
+  private static String css(String css) {
     String name = DOM.createUniqueId();
     StyleInjector.inject("." + name + "{" + css + "}");
     return name;
   }
 
-  private static final String id(IdMap idMap, String key) {
+  private static String id(IdMap idMap, String key) {
     String id = DOM.createUniqueId();
     idMap.put(id, key);
     return " id='" + id + "'";
   }
 
-  private static final String html(ReplacementMap opts, String id) {
+  private static String html(ReplacementMap opts, String id) {
     int d = id.indexOf('.');
     if (0 < d) {
       String name = id.substring(0, d);
@@ -84,7 +84,7 @@
     return new SafeHtmlBuilder().append(opts.str(id)).asString();
   }
 
-  private static final Node parseHtml(
+  private static Node parseHtml(
       String html,
       IdMap ids,
       ReplacementMap opts,
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/Plugin.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/Plugin.java
index 04d4ce5..fb549ee 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/Plugin.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/Plugin.java
@@ -26,14 +26,14 @@
     return create(TYPE, url, name);
   }
 
-  final native String url() /*-{ return this._scriptUrl }-*/;
-  final native String name() /*-{ return this.name }-*/;
+  native String url() /*-{ return this._scriptUrl }-*/;
+  native String name() /*-{ return this.name }-*/;
 
-  final native boolean loaded() /*-{ return this._success || this._failure != null }-*/;
-  final native Exception failure() /*-{ return this._failure }-*/;
-  final native void failure(Exception e) /*-{ this._failure = e }-*/;
-  final native boolean success() /*-{ return this._success || false }-*/;
-  final native void _initialized() /*-{ this._success = true }-*/;
+  native boolean loaded() /*-{ return this._success || this._failure != null }-*/;
+  native Exception failure() /*-{ return this._failure }-*/;
+  native void failure(Exception e) /*-{ this._failure = e }-*/;
+  native boolean success() /*-{ return this._success || false }-*/;
+  native void _initialized() /*-{ this._success = true }-*/;
 
   private static native Plugin create(JavaScriptObject T, String u, String n)
   /*-{ return new T(u,n) }-*/;
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 a560711..330ec15 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
@@ -76,7 +76,7 @@
   protected static final native JavaScriptException makeException()
   /*-{ try { null.a() } catch (e) { return e } }-*/;
 
-  private static final native boolean hasStack(JavaScriptException e)
+  private static native boolean hasStack(JavaScriptException e)
   /*-{ return !!e.stack }-*/;
 
   /** Extracts URL from the stack frame. */
@@ -103,7 +103,7 @@
       return UNKNOWN;
     }
 
-    private static final native JsArrayString getStack(JavaScriptException e)
+    private static native JsArrayString getStack(JavaScriptException e)
     /*-{ return e.stack ? e.stack.split('\n') : [] }-*/;
   }
 }
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 69887e7..f9084d9 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
@@ -61,11 +61,11 @@
     }
   }
 
-  private static final native JavaScriptObject projectAction(String id) /*-{
+  private static native JavaScriptObject projectAction(String id) /*-{
     return $wnd.Gerrit.project_actions[id];
   }-*/;
 
-  private static final native JavaScriptObject branchAction(String id) /*-{
+  private static native JavaScriptObject branchAction(String id) /*-{
     return $wnd.Gerrit.branch_actions[id];
   }-*/;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/RevisionGlue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/RevisionGlue.java
index 914ef85..50ebce7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/RevisionGlue.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/RevisionGlue.java
@@ -46,7 +46,7 @@
     }
   }
 
-  private static final native JavaScriptObject get(String id) /*-{
+  private static native JavaScriptObject get(String id) /*-{
     return $wnd.Gerrit.revision_actions[id];
   }-*/;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/blame/BlameInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/blame/BlameInfo.java
new file mode 100644
index 0000000..bbd939a
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/blame/BlameInfo.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.client.blame;
+
+import com.google.gerrit.client.RangeInfo;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+
+public class BlameInfo extends JavaScriptObject {
+  public final native String author() /*-{ return this.author; }-*/;
+  public final native String id() /*-{ return this.id; }-*/;
+  public final native String commitMsg() /*-{ return this.commit_msg; }-*/;
+  public final native int time() /*-{ return this.time; }-*/;
+  public final native JsArray<RangeInfo> ranges() /*-{ return this.ranges; }-*/;
+
+  protected BlameInfo() {
+  }
+
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ActionMessageBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ActionMessageBox.java
index 28943ab..396bc8a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ActionMessageBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ActionMessageBox.java
@@ -35,7 +35,7 @@
   interface Binder extends UiBinder<HTMLPanel, ActionMessageBox> {}
   private static final Binder uiBinder = GWT.create(Binder.class);
 
-  static interface Style extends CssResource {
+  interface Style extends CssResource {
     String popup();
   }
 
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 033a3a8..36107ee 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
@@ -37,7 +37,7 @@
 class Actions extends Composite {
   private static final String[] CORE = {
     "abandon", "cherrypick", "followup", "hashtags", "publish",
-    "rebase", "restore", "revert", "submit", "topic", "/"};
+    "rebase", "restore", "revert", "submit", "topic", "/",};
 
   interface Binder extends UiBinder<FlowPanel, Actions> {}
   private static final Binder uiBinder = GWT.create(Binder.class);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java
index e63a182..319329d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java
@@ -189,6 +189,7 @@
   @UiField Element actionText;
   @UiField Element actionDate;
   @UiField SimplePanel changeExtension;
+  @UiField SimplePanel relatedExtension;
 
   @UiField Actions actions;
   @UiField Labels labels;
@@ -333,6 +334,9 @@
     addExtensionPoint(
         GerritUiExtensionPoint.CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK,
         changeExtension, change, rev);
+    addExtensionPoint(
+        GerritUiExtensionPoint.CHANGE_SCREEN_BELOW_RELATED_INFO_BLOCK,
+        relatedExtension, change, rev);
   }
 
   private void addExtensionPoint(GerritUiExtensionPoint extensionPoint,
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml
index 2eb4ae9..0916c00 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml
@@ -347,6 +347,10 @@
       padding-top: 5px;
     }
 
+    .relatedExtension {
+      padding-top: 5px;
+    }
+
     .pushCertStatus {
       padding-left: 5px;
     }
@@ -520,6 +524,7 @@
         </td>
         <td class='{style.relatedColumn}'>
           <c:RelatedChanges ui:field='related'/>
+          <g:SimplePanel ui:field='relatedExtension' styleName='{style.relatedExtension}'/>
         </td>
       </tr>
     </table>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java
index 71cc7fb..60d66e0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java
@@ -68,7 +68,7 @@
 import java.sql.Timestamp;
 
 public class FileTable extends FlowPanel {
-  static final FileTableResources R = GWT
+  private static final FileTableResources R = GWT
       .create(FileTableResources.class);
 
   interface FileTableResources extends ClientBundle {
@@ -95,7 +95,7 @@
     String restoreDelete();
   }
 
-  public static enum Mode {
+  public enum Mode {
     REVIEW,
     EDIT
   }
@@ -115,7 +115,7 @@
     init(DELETE, RESTORE, REVIEWED, OPEN);
   }
 
-  private static final native void init(String d, String t, String r, String o) /*-{
+  private static native void init(String d, String t, String r, String o) /*-{
     $wnd[d] = $entry(function(e,i) {
       @com.google.gerrit.client.change.FileTable::onDelete(Lcom/google/gwt/dom/client/NativeEvent;I)(e,i)
     });
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.java
index ed117aa..44aae25 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.java
@@ -62,7 +62,7 @@
     init(REMOVE);
   }
 
-  private static final native void init(String r) /*-{
+  private static native void init(String r) /*-{
     $wnd[r] = $entry(function(e) {
       @com.google.gerrit.client.change.Hashtags::onRemove(Lcom/google/gwt/dom/client/NativeEvent;)(e)
     });
@@ -114,9 +114,9 @@
     hashtagTextBox.addKeyDownHandler(new KeyDownHandler() {
       @Override
       public void onKeyDown(KeyDownEvent e) {
-        if (e.getNativeEvent().getKeyCode() == KeyCodes.KEY_ESCAPE) {
+        if (e.getNativeKeyCode() == KeyCodes.KEY_ESCAPE) {
           onCancel(null);
-        } else if (e.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
+        } else if (e.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
           onAdd(null);
         }
       }
@@ -132,7 +132,7 @@
         ClickEvent.getType());
   }
 
-  void init(ChangeScreen.Style style){
+  void init(ChangeScreen.Style style) {
     this.style = style;
   }
 
@@ -246,12 +246,12 @@
       input.init(toJsArrayString(add), toJsArrayString(remove));
       return input;
     }
-    private static JsArrayString toJsArrayString(String commaSeparated){
+    private static JsArrayString toJsArrayString(String commaSeparated) {
       if (commaSeparated == null || commaSeparated.equals("")) {
         return null;
       }
       JsArrayString array = JsArrayString.createArray().cast();
-      for (String hashtag : commaSeparated.split(",")){
+      for (String hashtag : commaSeparated.split(",")) {
         array.push(hashtag.trim());
       }
       return array;
@@ -265,12 +265,4 @@
     protected PostInput() {
     }
   }
-
-  public static class Result extends JavaScriptObject {
-    public final native JsArrayString hashtags() /*-{ return this.hashtags; }-*/;
-    public final native String error() /*-{ return this.error; }-*/;
-
-    protected Result() {
-    }
-  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java
index 827362e..89550c6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java
@@ -59,7 +59,7 @@
     init(REMOVE_REVIEWER, REMOVE_VOTE);
   }
 
-  private static final native void init(String r, String v) /*-{
+  private static native void init(String r, String v) /*-{
     $wnd[r] = $entry(function(e) {
       @com.google.gerrit.client.change.Labels::onRemoveReviewer(Lcom/google/gwt/dom/client/NativeEvent;)(e)
     });
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LineComment.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LineComment.java
index 9873bed..a0f8858 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LineComment.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LineComment.java
@@ -57,14 +57,14 @@
       ps = defaultPs;
       psLoc.removeFromParent();
       psLoc = null;
-      psNum= null;
+      psNum = null;
     } else {
       ps = defaultPs;
       sideLoc.removeFromParent();
       sideLoc = null;
       psLoc.removeFromParent();
       psLoc = null;
-      psNum= null;
+      psNum = null;
     }
 
     if (info.hasLine()) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.java
index f53c1a3..6c27ed9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.java
@@ -50,7 +50,7 @@
   interface Binder extends UiBinder<HTMLPanel, Message> {}
   private static final Binder uiBinder = GWT.create(Binder.class);
 
-  static interface Style extends CssResource {
+  interface Style extends CssResource {
     String closed();
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsBox.java
index 1d76612..1753c7b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsBox.java
@@ -63,7 +63,7 @@
     init(OPEN);
   }
 
-  private static final native void init(String o) /*-{
+  private static native void init(String o) /*-{
     $wnd[o] = $entry(function(e,i) {
       return @com.google.gerrit.client.change.PatchSetsBox::onOpen(Lcom/google/gwt/dom/client/NativeEvent;I)(e,i);
     });
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java
index 9612f71..cc5c9b7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java
@@ -137,7 +137,7 @@
     abstract String getTitle(int count);
     abstract String getTitle(String count);
 
-    private Tab(String defaultTitle, String tooltip) {
+    Tab(String defaultTitle, String tooltip) {
       this.defaultTitle = defaultTitle;
       this.tooltip = tooltip;
     }
@@ -215,6 +215,11 @@
         EnumSet.of(ListChangesOption.CURRENT_REVISION, ListChangesOption.CURRENT_COMMIT),
         new TabChangeListCallback(Tab.CHERRY_PICKS, info.project(), revision));
 
+    // TODO(sbeller): show only on latest revision
+    ChangeApi.change(info.legacyId().get()).view("submitted_together")
+        .get(new TabChangeListCallback(Tab.SUBMITTED_TOGETHER,
+            info.project(), revision));
+
     if (!Gerrit.info().change().isSubmitWholeTopicEnabled()
         && info.topic() != null && !"".equals(info.topic())) {
       StringBuilder topicQuery = new StringBuilder();
@@ -226,11 +231,6 @@
                      ListChangesOption.DETAILED_LABELS,
                      ListChangesOption.LABELS),
           new TabChangeListCallback(Tab.SAME_TOPIC, info.project(), revision));
-    } else {
-      // TODO(sbeller): show only on latest revision
-      ChangeApi.change(info.legacyId().get()).view("submitted_together")
-          .get(new TabChangeListCallback(Tab.SUBMITTED_TOGETHER,
-              info.project(), revision));
     }
   }
 
@@ -385,7 +385,7 @@
       String s = statusRaw();
       return s != null ? Change.Status.valueOf(s) : null;
     }
-    private final native String statusRaw() /*-{ return this.status; }-*/;
+    private native String statusRaw() /*-{ return this.status; }-*/;
 
     final native void setId(String i)
     /*-{ if(i)this.change_id=i; }-*/;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChangesTab.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChangesTab.java
index cc34df2..788b4b0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChangesTab.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChangesTab.java
@@ -67,7 +67,7 @@
   private static final SafeHtml POINTER_HTML =
       AbstractImagePrototype.create(Gerrit.RESOURCES.arrowRight()).getSafeHtml();
 
-  private static final native String init(String o) /*-{
+  private static native String init(String o) /*-{
     $wnd[o] = $entry(@com.google.gerrit.client.change.RelatedChangesTab::onOpen(
       Lcom/google/gwt/dom/client/NativeEvent;Lcom/google/gwt/dom/client/Element;));
     return o + '(event,this)';
@@ -599,7 +599,7 @@
     }
   }
 
-  private static final native Node createDocumentFragment() /*-{
+  private static native Node createDocumentFragment() /*-{
     return $doc.createDocumentFragment();
   }-*/;
 }
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 85afe9f..e29048a 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
@@ -15,6 +15,7 @@
 package com.google.gerrit.client.change;
 
 import static com.google.gwt.event.dom.client.KeyCodes.KEY_ENTER;
+import static com.google.gwt.event.dom.client.KeyCodes.KEY_MAC_ENTER;
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.changes.ChangeApi;
@@ -43,6 +44,8 @@
 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.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
 import com.google.gwt.event.dom.client.KeyPressEvent;
 import com.google.gwt.event.dom.client.KeyPressHandler;
 import com.google.gwt.event.dom.client.MouseOutEvent;
@@ -123,12 +126,13 @@
     }
 
     addDomHandler(
-      new KeyPressHandler() {
+      new KeyDownHandler() {
         @Override
-        public void onKeyPress(KeyPressEvent e) {
+        public void onKeyDown(KeyDownEvent e) {
           e.stopPropagation();
-          if ((e.getCharCode() == '\n' || e.getCharCode() == KEY_ENTER)
-              && e.isControlKeyDown()) {
+          if ((e.getNativeKeyCode() == KEY_ENTER
+              || e.getNativeKeyCode() == KEY_MAC_ENTER)
+              && (e.isControlKeyDown() || e.isMetaKeyDown())) {
             e.preventDefault();
             if (post.isEnabled()) {
               onPost(null);
@@ -136,6 +140,14 @@
           }
         }
       },
+      KeyDownEvent.getType());
+    addDomHandler(
+      new KeyPressHandler() {
+        @Override
+        public void onKeyPress(KeyPressEvent e) {
+          e.stopPropagation();
+        }
+      },
       KeyPressEvent.getType());
   }
 
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 4fc76c1..fdfaf61 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
@@ -19,9 +19,9 @@
 import com.google.gwt.resources.client.CssResource;
 
 public interface Resources extends ClientBundle {
-  public static final Resources I = GWT.create(Resources.class);
-  static final ChangeConstants C = GWT.create(ChangeConstants.class);
-  static final ChangeMessages M = GWT.create(ChangeMessages.class);
+  Resources I = GWT.create(Resources.class);
+  ChangeConstants C = GWT.create(ChangeConstants.class);
+  ChangeMessages M = GWT.create(ChangeMessages.class);
 
   @Source("common.css") Style style();
 
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 71942ce..b69d1c0 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
@@ -293,7 +293,7 @@
       return Natives.keys(_approvals());
     }
     final native String approval(String l) /*-{ return this.approvals[l]; }-*/;
-    private final native NativeMap<NativeString> _approvals() /*-{ return this.approvals; }-*/;
+    private native NativeMap<NativeString> _approvals() /*-{ return this.approvals; }-*/;
 
     protected ReviewerInfo() {
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
index bff3f47..b181341 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
@@ -97,6 +97,14 @@
     return call(id, "detail");
   }
 
+  public static RestApi blame(PatchSet.Id id, String path, boolean base) {
+    return revision(id)
+        .view("files")
+        .id(path)
+        .view("blame")
+        .addParameter("base", base);
+  }
+
   public static RestApi actions(int id, String revision) {
     if (revision == null || revision.equals("")) {
       revision = "current";
@@ -170,7 +178,7 @@
   public static RestApi hashtags(int changeId) {
     return change(changeId).view("hashtags");
   }
-  public static RestApi hashtag(int changeId, String hashtag){
+  public static RestApi hashtag(int changeId, String hashtag) {
     return change(changeId).view("hashtags").id(hashtag);
   }
 
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 0a97729..b2334d1d 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
@@ -62,25 +62,17 @@
   String patchTableColumnName();
   String patchTableColumnComments();
   String patchTableColumnSize();
-  String patchTableColumnDiff();
-  String patchTableDiffSideBySide();
-  String patchTableDiffUnified();
   String commitMessage();
 
   String patchTablePrev();
   String patchTableNext();
   String patchTableOpenDiff();
-  String patchTableOpenUnifiedDiff();
-  String upToChangeIconLink();
-  String prevPatchLinkIcon();
-  String nextPatchLinkIcon();
 
   String approvalTableAddReviewerHint();
   String approvalTableAddManyReviewersConfirmationDialogTitle();
 
   String changeInfoBlockUploaded();
   String changeInfoBlockUpdated();
-  String changePermalink();
 
   String messageNoAuthor();
 
@@ -108,12 +100,8 @@
   String pagedChangeListPrev();
   String pagedChangeListNext();
 
-  String reviewed();
   String submitFailed();
 
-  String diffAllSideBySide();
-  String diffAllUnified();
-
   String votable();
 
   String pushCertMissing();
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 5a3ce66..b7e2677 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
@@ -44,22 +44,17 @@
 patchTableColumnName = File Path
 patchTableColumnComments = Comments
 patchTableColumnSize = Size
-patchTableColumnDiff = Diff
-patchTableDiffSideBySide = Side-by-Side
-patchTableDiffUnified = Unified
 commitMessage = Commit Message
 
 patchTablePrev = Previous file
 patchTableNext = Next file
 patchTableOpenDiff = Open diff
-patchTableOpenUnifiedDiff = Open unified diff
 
 approvalTableAddReviewerHint = Name or Email or Group
 approvalTableAddManyReviewersConfirmationDialogTitle = Adding Group Members as Reviewers
 
 changeInfoBlockUploaded = Uploaded
 changeInfoBlockUpdated = Updated
-changePermalink = Permalink
 
 messageNoAuthor = Gerrit Code Review
 
@@ -87,16 +82,8 @@
 pagedChangeListPrev = &#x21e6;Prev
 pagedChangeListNext = Next&#x21e8;
 
-upToChangeIconLink = &#x21e7;Up to change
-prevPatchLinkIcon = &#x21e6;
-nextPatchLinkIcon = &#x21e8;
-
-reviewed = Reviewed
 submitFailed = Submit Failed
 
-diffAllSideBySide = All Side-by-Side
-diffAllUnified = All Unified
-
 votable = Votable:
 
 pushCertMissing = This patch set was created without a push certificate
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeEditApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeEditApi.java
index 0928cd8..f86ddf7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeEditApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeEditApi.java
@@ -28,7 +28,7 @@
 /** REST API helpers to remotely edit a change. */
 public class ChangeEditApi {
   /** Get file (or commit message) contents. */
-  public static void get(PatchSet.Id id, String path,
+  public static void get(PatchSet.Id id, String path, boolean base,
       HttpCallback<NativeString> cb) {
     RestApi api;
     if (id.get() != 0) {
@@ -36,13 +36,19 @@
       // exist for the caller, or is not currently active.
       api = ChangeApi.revision(id).view("files").id(path).view("content");
     } else if (Patch.COMMIT_MSG.equals(path)) {
-      api = editMessage(id.getParentKey().get());
+      api = editMessage(id.getParentKey().get()).addParameter("base", base);
     } else {
-      api = editFile(id.getParentKey().get(), path);
+      api = editFile(id.getParentKey().get(), path).addParameter("base", base);
     }
     api.get(cb);
   }
 
+  /** Get file (or commit message) contents of the edit. */
+  public static void get(PatchSet.Id id, String path,
+      HttpCallback<NativeString> cb) {
+    get(id, path, false, cb);
+  }
+
   /** Get meta info for change edit. */
   public static void getMeta(PatchSet.Id id, String path,
       AsyncCallback<EditFileInfo> cb) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
index e1dc0a3..c5397ee 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
@@ -18,53 +18,28 @@
 
 public interface ChangeMessages extends Messages {
   String accountDashboardTitle(String fullName);
-  String changesOpenInProject(String string);
-  String changesMergedInProject(String string);
-  String changesAbandonedInProject(String string);
 
   String revertChangeDefaultMessage(String commitMsg, String commitId);
 
   String cherryPickedChangeDefaultMessage(String commitMsg, String commitId);
   String changeScreenTitleId(String changeId);
-  String outdatedHeader(int outdated);
-  String patchSetHeader(int id);
   String loadingPatchSet(int id);
-  String submitPatchSet(int id);
 
-  String patchTableComments(@PluralCount int count);
-  String patchTableDrafts(@PluralCount int count);
   String patchTableSize_Modify(int insertions, int deletions);
   String patchTableSize_ModifyBinaryFiles(String bytesInserted,
       String bytesDeleted);
   String patchTableSize_ModifyBinaryFilesWithPercentages(String bytesInserted,
       String percentageInserted, String bytesDeleted, String percentageDeleted);
   String patchTableSize_LongModify(int insertions, int deletions);
-  String patchTableSize_Lines(@PluralCount int insertions);
 
-  String removeHashtag(String name);
   String removeReviewer(String fullName);
   String removeVote(String label);
-  String messageWrittenOn(String date);
-
-  String renamedFrom(String sourcePath);
-  String copiedFrom(String sourcePath);
-  String otherFrom(String sourcePath);
 
   String blockedOn(String labelName);
   String needs(String labelName);
-  String publishComments(String changeId, int ps);
-  String lineHeader(int line);
 
   String changeQueryWindowTitle(String query);
   String changeQueryPageTitle(String query);
 
-  String reviewerNotFound(String who);
-  String accountInactive(String who);
-  String changeNotVisibleTo(String who);
-  String groupIsEmpty(String group);
-  String groupIsNotAllowed(String group);
-  String groupHasTooManyMembers(String group);
-  String groupManyMembersConfirmation(String group, int memberCount);
-
   String insertionsAndDeletions(int insertions, int deletions);
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
index 8b96dac..f0d7e59 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
@@ -1,50 +1,25 @@
 # Changes to this file should also be made in
 # gerrit-server/src/main/resources/com/google/gerrit/server/change/ChangeMessages.properties
 accountDashboardTitle = Code Review Dashboard for {0}
-changesOpenInProject = Open Changes In {0}
-changesMergedInProject = Merged Changes In {0}
-changesAbandonedInProject = Abandoned Changes In {0}
 
 revertChangeDefaultMessage = Revert \"{0}\"\n\nThis reverts commit {1}.
 cherryPickedChangeDefaultMessage = {0}\n(cherry picked from commit {1})
 
 changeScreenTitleId = Change {0}
-outdatedHeader = Change depends on {0} outdated change(s) and should be rebased on the latest patch sets.
-patchSetHeader = Patch Set {0}
 loadingPatchSet = Loading Patch Set {0} ...
-submitPatchSet = Submit Patch Set {0}
 
-patchTableComments = {0} comments
-patchTableDrafts = {0} drafts
 patchTableSize_Modify = +{0}, -{1}
 patchTableSize_ModifyBinaryFiles = +{0}, -{1}
 patchTableSize_ModifyBinaryFilesWithPercentages = +{0} (+{1}), -{2} (-{3})
 patchTableSize_LongModify = {0} inserted, {1} deleted
-patchTableSize_Lines = {0} lines
 
-removeHashtag = Remove hashtag {0}
 removeReviewer = Remove reviewer {0}
 removeVote = Remove vote {0}
-messageWrittenOn = on {0}
 
-renamedFrom = renamed from {0}
-copiedFrom = copied from {0}
-otherFrom = from {0}
-
-blockedOn = Blocked on {0}
-needs = Needs {0}
-publishComments = Change {0} - Patch Set {1}: Publish Comments
-lineHeader = Line {0}:
+blockedOn = Blocked on {0} Label
+needs = Needs {0} Label
 
 changeQueryWindowTitle = {0}
 changeQueryPageTitle = Search for {0}
 
-reviewerNotFound = {0} is neither a registered user nor a group.
-accountInactive = {0} is not an active user.
-changeNotVisibleTo = {0} cannot access the change.
-groupIsEmpty = The group {0} does not have any members to add as reviewers.
-groupIsNotAllowed =  The group {0} cannot be added as reviewer.
-groupHasTooManyMembers = The group {0} has too many members to add them all as reviewers.
-groupManyMembersConfirmation = The group {0} has {1} members. Do you want to add them all as reviewers?
-
 insertionsAndDeletions = +{0}, -{1}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages_en.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages_en.properties
deleted file mode 100644
index d9c7059..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages_en.properties
+++ /dev/null
@@ -1,8 +0,0 @@
-patchTableComments[one] = 1 comment
-patchTableComments = {0} comments
-
-patchTableDrafts[one] = 1 draft
-patchTableDrafts = {0} drafts
-
-patchTableSize_Lines[one] = 1 line
-patchTableSize_Lines = {0} lines
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 c0ab584..9c78955 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
@@ -493,7 +493,7 @@
       if (titleText != null) {
         setTitleText(titleText);
         return true;
-      } else if(titleWidget != null) {
+      } else if (titleWidget != null) {
         setTitleWidget(titleWidget);
         return true;
       }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java
index 9088c1c..8e73f73 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java
@@ -77,7 +77,7 @@
   public final void side(Side side) {
     sideRaw(side.toString());
   }
-  private final native void sideRaw(String s) /*-{ this.side = s }-*/;
+  private native void sideRaw(String s) /*-{ this.side = s }-*/;
 
   public final native String path() /*-{ return this.path }-*/;
   public final native String id() /*-{ return this.id }-*/;
@@ -90,7 +90,7 @@
         ? Side.valueOf(s)
         : Side.REVISION;
   }
-  private final native String sideRaw() /*-{ return this.side }-*/;
+  private native String sideRaw() /*-{ return this.side }-*/;
 
   public final Timestamp updated() {
     Timestamp r = updatedTimestamp();
@@ -103,9 +103,9 @@
     }
     return r;
   }
-  private final native String updatedRaw() /*-{ return this.updated }-*/;
-  private final native Timestamp updatedTimestamp() /*-{ return this._ts }-*/;
-  private final native void updatedTimestamp(Timestamp t) /*-{ this._ts = t }-*/;
+  private native String updatedRaw() /*-{ return this.updated }-*/;
+  private native Timestamp updatedTimestamp() /*-{ return this._ts }-*/;
+  private native void updatedTimestamp(Timestamp t) /*-{ this._ts = t }-*/;
 
   public final native AccountInfo author() /*-{ return this.author }-*/;
   public final native int line() /*-{ return this.line || 0 }-*/;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java
index 2b3c6ae..e7fba34 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java
@@ -51,7 +51,7 @@
   protected AsyncCallback<ChangeList> loadCallback() {
     return new GerritCallback<ChangeList>() {
       @Override
-      public final void onSuccess(ChangeList result) {
+      public void onSuccess(ChangeList result) {
         if (isAttached()) {
           if (result.length() == 1 && isSingleQuery(query)) {
             ChangeInfo c = result.get(0);
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 096dbd0..a127e7f 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
@@ -19,11 +19,11 @@
 import com.google.gwt.core.client.JsArray;
 
 public class ReviewInput extends JavaScriptObject {
-  public static enum NotifyHandling {
+  public enum NotifyHandling {
     NONE, OWNER, OWNER_REVIEWERS, ALL
   }
 
-  public static enum DraftHandling {
+  public enum DraftHandling {
     DELETE, PUBLISH, KEEP, PUBLISH_ALL_REVISIONS
   }
 
@@ -42,14 +42,14 @@
   public final void notify(NotifyHandling e) {
     _notify(e.name());
   }
-  private final native void _notify(String n) /*-{ this.notify=n; }-*/;
+  private native void _notify(String n) /*-{ this.notify=n; }-*/;
 
   public final void drafts(DraftHandling e) {
     _drafts(e.name());
   }
-  private final native void _drafts(String n) /*-{ this.drafts=n; }-*/;
+  private native void _drafts(String n) /*-{ this.drafts=n; }-*/;
 
-  private final native void init() /*-{
+  private native void init() /*-{
     this.labels = {};
     this.strict_labels = true;
   }-*/;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java
index 8750389..43b3b80 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java
@@ -37,7 +37,7 @@
   private static final Event.Type<ChangeStarHandler> TYPE = new Event.Type<>();
 
   /** Handler that can receive notifications of a change's starred status. */
-  public static interface ChangeStarHandler {
+  public interface ChangeStarHandler {
     void onChangeStar(ChangeStarEvent event);
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/SubmitInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/SubmitInfo.java
index 6d8bc30..7a24774 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/SubmitInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/SubmitInfo.java
@@ -22,7 +22,7 @@
     return Change.Status.valueOf(statusRaw());
   }
 
-  private final native String statusRaw() /*-{ return this.status; }-*/;
+  private native String statusRaw() /*-{ return this.status; }-*/;
 
   protected SubmitInfo() {
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardsTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardsTable.java
index 3b168e1..47c0359 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardsTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardsTable.java
@@ -84,7 +84,7 @@
     });
 
     String ref = null;
-    for(DashboardInfo d : list) {
+    for (DashboardInfo d : list) {
       if (!d.ref().equals(ref)) {
         ref = d.ref();
         insertTitleRow(table.getRowCount(), ref);
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 91533ab..5cc7799 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
@@ -25,7 +25,6 @@
 import net.codemirror.lib.TextMarker;
 
 import java.util.ArrayList;
-import java.util.Comparator;
 import java.util.List;
 
 /** Colors modified regions for {@link SideBySide} and {@link Unified}. */
@@ -33,23 +32,15 @@
   static final native void onClick(Element e, JavaScriptObject f)
   /*-{ e.onclick = f }-*/;
 
-  private final Scrollbar scrollbar;
-  private final LineMapper mapper;
+  final Scrollbar scrollbar;
+  final LineMapper lineMapper;
 
   private List<TextMarker> markers;
   private List<Runnable> undo;
 
   ChunkManager(Scrollbar scrollbar) {
     this.scrollbar = scrollbar;
-    this.mapper = new LineMapper();
-  }
-
-  LineMapper getLineMapper() {
-    return mapper;
-  }
-
-  Scrollbar getScrollbar() {
-    return scrollbar;
+    this.lineMapper = new LineMapper();
   }
 
   abstract DiffChunkInfo getFirst();
@@ -59,7 +50,7 @@
   }
 
   void reset() {
-    mapper.reset();
+    lineMapper.reset();
     for (TextMarker m : markers) {
       m.clear();
     }
@@ -110,7 +101,7 @@
 
     DiffChunkInfo lookUp = chunks.get(res);
     // If edit, skip the deletion chunk and set focus on the insertion one.
-    if (lookUp.isEdit() && lookUp.getSide() == A) {
+    if (lookUp.edit && lookUp.side == A) {
       res = res + (dir == Direction.PREV ? -1 : 1);
       if (res < 0 || chunks.size() <= res) {
         return;
@@ -118,8 +109,8 @@
     }
 
     DiffChunkInfo target = chunks.get(res);
-    CodeMirror targetCm = host.getCmFromSide(target.getSide());
-    int cmLine = getCmLine(target.getStart(), target.getSide());
+    CodeMirror targetCm = host.getCmFromSide(target.side);
+    int cmLine = getCmLine(target.start, target.side);
     targetCm.setCursor(Pos.create(cmLine));
     targetCm.focus();
     targetCm.scrollToY(
@@ -127,28 +118,5 @@
         - 0.5 * targetCm.scrollbarV().getClientHeight());
   }
 
-  Comparator<DiffChunkInfo> getDiffChunkComparator() {
-    // Chunks are ordered by their starting line. If it's a deletion,
-    // use its corresponding line on the revision side for comparison.
-    // In the edit case, put the deletion chunk right before the
-    // insertion chunk. This placement guarantees well-ordering.
-    return new Comparator<DiffChunkInfo>() {
-      @Override
-      public int compare(DiffChunkInfo a, DiffChunkInfo b) {
-        if (a.getSide() == b.getSide()) {
-          return a.getStart() - b.getStart();
-        } else if (a.getSide() == A) {
-          int comp = mapper.lineOnOther(a.getSide(), a.getStart())
-              .getLine() - b.getStart();
-          return comp == 0 ? -1 : comp;
-        } else {
-          int comp = a.getStart() -
-              mapper.lineOnOther(b.getSide(), b.getStart()).getLine();
-          return comp == 0 ? 1 : comp;
-        }
-      }
-    };
-  }
-
   abstract int getCmLine(int line, DisplaySide side);
-}
\ No newline at end of file
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.java
index 4654a3d..70ef947 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.java
@@ -24,6 +24,7 @@
 
 import net.codemirror.lib.CodeMirror;
 import net.codemirror.lib.Configuration;
+import net.codemirror.lib.Pos;
 import net.codemirror.lib.TextMarker;
 import net.codemirror.lib.TextMarker.FromTo;
 
@@ -56,7 +57,13 @@
   CommentBox(CommentGroup group, CommentRange range) {
     this.group = group;
     if (range != null) {
-      fromTo = FromTo.create(range);
+      DiffScreen screen = group.getManager().host;
+      int startCmLine =
+          screen.getCmLine(range.startLine() - 1, group.getSide());
+      int endCmLine = screen.getCmLine(range.endLine() - 1, group.getSide());
+      fromTo = FromTo.create(
+          Pos.create(startCmLine, range.startCharacter()),
+          Pos.create(endCmLine, range.endCharacter()));
       rangeMarker = group.getCm().markText(
           fromTo.from(),
           fromTo.to(),
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 d48ada7..20dd883 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
@@ -63,6 +63,10 @@
     return line;
   }
 
+  DisplaySide getSide() {
+    return side;
+  }
+
   void add(PublishedBox box) {
     comments.add(box);
     comments.setVisible(true);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java
index 4386436..a26b1ce 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.client.diff;
 
+import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.changes.CommentInfo;
 import com.google.gerrit.client.patches.SkippedLine;
 import com.google.gerrit.client.rpc.CallbackGroup;
@@ -24,12 +25,18 @@
 import com.google.gwt.core.client.JsArray;
 
 import net.codemirror.lib.CodeMirror;
+import net.codemirror.lib.Pos;
+import net.codemirror.lib.TextMarker.FromTo;
 
+import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.SortedMap;
+import java.util.TreeMap;
 
 /** Tracks comment widgets for {@link DiffScreen}. */
 abstract class CommentManager {
@@ -37,19 +44,23 @@
   private final PatchSet.Id revision;
   private final String path;
   private final CommentLinkProcessor commentLinkProcessor;
-
+  final SortedMap<Integer, CommentGroup> sideA;
+  final SortedMap<Integer, CommentGroup> sideB;
   private final Map<String, PublishedBox> published;
   private final Set<DraftBox> unsavedDrafts;
+  final DiffScreen host;
   private boolean attached;
   private boolean expandAll;
   private boolean open;
 
   CommentManager(
+      DiffScreen host,
       PatchSet.Id base,
       PatchSet.Id revision,
       String path,
       CommentLinkProcessor clp,
       boolean open) {
+    this.host = host;
     this.base = base;
     this.revision = revision;
     this.path = path;
@@ -58,6 +69,8 @@
 
     published = new HashMap<>();
     unsavedDrafts = new HashSet<>();
+    sideA = new TreeMap<>();
+    sideB = new TreeMap<>();
   }
 
   void setAttached(boolean attached) {
@@ -130,27 +143,300 @@
     return forSide;
   }
 
-  abstract void insertNewDraft(DisplaySide side, int line);
+  static FromTo adjustSelection(CodeMirror cm) {
+    FromTo fromTo = cm.getSelectedRange();
+    Pos to = fromTo.to();
+    if (to.ch() == 0) {
+      to.line(to.line() - 1);
+      to.ch(cm.getLine(to.line()).length());
+    }
+    return fromTo;
+  }
 
-  abstract Runnable newDraftCallback(final CodeMirror cm);
+  abstract CommentGroup group(DisplaySide side, int cmLinePlusOne);
 
-  abstract DraftBox addDraftBox(DisplaySide side, CommentInfo info);
+  /**
+   * Create a new {@link DraftBox} at the specified line and focus it.
+   *
+   * @param side which side the draft will appear on.
+   * @param line the line the draft will be at. Lines are 1-based. Line 0 is a
+   *        special case creating a file level comment.
+   */
+  void insertNewDraft(DisplaySide side, int line) {
+    if (line == 0) {
+      host.skipManager.ensureFirstLineIsVisible();
+    }
 
-  abstract void setExpandAllComments(boolean b);
+    CommentGroup group = group(side, line);
+    if (0 < group.getBoxCount()) {
+      CommentBox last = group.getCommentBox(group.getBoxCount() - 1);
+      if (last instanceof DraftBox) {
+        ((DraftBox)last).setEdit(true);
+      } else {
+        ((PublishedBox)last).doReply();
+      }
+    } else {
+      addDraftBox(side, CommentInfo.create(
+          getPath(),
+          getStoredSideFromDisplaySide(side),
+          line,
+          null)).setEdit(true);
+    }
+  }
 
-  abstract Runnable commentNav(CodeMirror src, Direction dir);
+  abstract String getTokenSuffixForActiveLine(CodeMirror cm);
 
-  abstract void clearLine(DisplaySide side, int line, CommentGroup group);
+  Runnable signInCallback(final CodeMirror cm) {
+    return new Runnable() {
+      @Override
+      public void run() {
+        String token = host.getToken();
+        if (cm.extras().hasActiveLine()) {
+          token += "@" + getTokenSuffixForActiveLine(cm);
+        }
+        Gerrit.doSignIn(token);
+      }
+    };
+  }
 
-  abstract void renderPublished(DisplaySide forSide, JsArray<CommentInfo> in);
+  abstract void newDraft(CodeMirror cm);
 
-  abstract List<SkippedLine> splitSkips(int context, List<SkippedLine> skips);
+  Runnable newDraftCallback(final CodeMirror cm) {
+    if (!Gerrit.isSignedIn()) {
+      return signInCallback(cm);
+    }
 
-  abstract void newDraftOnGutterClick(CodeMirror cm, String gutterClass, int line);
+    return new Runnable() {
+      @Override
+      public void run() {
+        if (cm.extras().hasActiveLine()) {
+          newDraft(cm);
+        }
+      }
+    };
+  }
 
-  abstract Runnable toggleOpenBox(final CodeMirror cm);
+  DraftBox addDraftBox(DisplaySide side, CommentInfo info) {
+    int cmLinePlusOne = host.getCmLine(info.line() - 1, side) + 1;
+    CommentGroup group = group(side, cmLinePlusOne);
+    DraftBox box = new DraftBox(
+        group,
+        getCommentLinkProcessor(),
+        getPatchSetIdFromSide(side),
+        info,
+        isExpandAll());
 
-  abstract Runnable openCloseAll(final CodeMirror cm);
+    if (info.inReplyTo() != null) {
+      PublishedBox r = getPublished().get(info.inReplyTo());
+      if (r != null) {
+        r.setReplyBox(box);
+      }
+    }
 
-  abstract DiffScreen getDiffScreen();
+    group.add(box);
+    box.setAnnotation(host.getDiffTable().scrollbar.draft(
+        host.getCmFromSide(side),
+        Math.max(0, cmLinePlusOne - 1)));
+    return box;
+  }
+
+  void setExpandAllComments(boolean b) {
+    setExpandAll(b);
+    for (CommentGroup g : sideA.values()) {
+      g.setOpenAll(b);
+    }
+    for (CommentGroup g : sideB.values()) {
+      g.setOpenAll(b);
+    }
+  }
+
+  abstract SortedMap<Integer, CommentGroup> getMapForNav(DisplaySide side);
+
+  Runnable commentNav(final CodeMirror src, final Direction dir) {
+    return new Runnable() {
+      @Override
+      public void run() {
+        // Every comment appears in both side maps as a linked pair.
+        // It is only necessary to search one side to find a comment
+        // on either side of the editor pair.
+        SortedMap<Integer, CommentGroup> map = getMapForNav(src.side());
+        int line = src.extras().hasActiveLine()
+            ? src.getLineNumber(src.extras().activeLine()) + 1
+            : 0;
+
+        CommentGroup g;
+        if (dir == Direction.NEXT) {
+          map = map.tailMap(line + 1);
+          if (map.isEmpty()) {
+            return;
+          }
+          g = map.get(map.firstKey());
+          while (g.getBoxCount() == 0) {
+            map = map.tailMap(map.firstKey() + 1);
+            if (map.isEmpty()) {
+              return;
+            }
+            g = map.get(map.firstKey());
+          }
+        } else {
+          map = map.headMap(line);
+          if (map.isEmpty()) {
+            return;
+          }
+          g = map.get(map.lastKey());
+          while (g.getBoxCount() == 0) {
+            map = map.headMap(map.lastKey());
+            if (map.isEmpty()) {
+              return;
+            }
+            g = map.get(map.lastKey());
+          }
+        }
+
+        CodeMirror cm = g.getCm();
+        double y = cm.heightAtLine(g.getLine() - 1, "local");
+        cm.setCursor(Pos.create(g.getLine() - 1));
+        cm.scrollToY(y - 0.5 * cm.scrollbarV().getClientHeight());
+        cm.focus();
+      }
+    };
+  }
+
+  void clearLine(DisplaySide side, int line, CommentGroup group) {
+    SortedMap<Integer, CommentGroup> map = map(side);
+    if (map.get(line) == group) {
+      map.remove(line);
+    }
+  }
+
+  void render(CommentsCollections in, boolean expandAll) {
+    if (in.publishedBase != null) {
+      renderPublished(DisplaySide.A, in.publishedBase);
+    }
+    if (in.publishedRevision != null) {
+      renderPublished(DisplaySide.B, in.publishedRevision);
+    }
+    if (in.draftsBase != null) {
+      renderDrafts(DisplaySide.A, in.draftsBase);
+    }
+    if (in.draftsRevision != null) {
+      renderDrafts(DisplaySide.B, in.draftsRevision);
+    }
+    if (expandAll) {
+      setExpandAllComments(true);
+    }
+    for (CommentGroup g : sideA.values()) {
+      g.init(host.getDiffTable());
+    }
+    for (CommentGroup g : sideB.values()) {
+      g.init(host.getDiffTable());
+      g.handleRedraw();
+    }
+    setAttached(true);
+  }
+
+  void renderPublished(DisplaySide forSide, JsArray<CommentInfo> in) {
+    for (CommentInfo info : Natives.asList(in)) {
+      DisplaySide side = displaySide(info, forSide);
+      if (side != null) {
+        int cmLinePlusOne = host.getCmLine(info.line() - 1, side) + 1;
+        CommentGroup group = group(side, cmLinePlusOne);
+        PublishedBox box = new PublishedBox(
+            group,
+            getCommentLinkProcessor(),
+            getPatchSetIdFromSide(side),
+            info,
+            side,
+            isOpen());
+        group.add(box);
+        box.setAnnotation(host.getDiffTable().scrollbar.comment(
+            host.getCmFromSide(side),
+            cmLinePlusOne - 1));
+        getPublished().put(info.id(), box);
+      }
+    }
+  }
+
+  abstract Collection<Integer> getLinesWithCommentGroups();
+
+  private static void checkAndAddSkip(List<SkippedLine> out, SkippedLine s) {
+    if (s.getSize() > 1) {
+      out.add(s);
+    }
+  }
+
+  List<SkippedLine> splitSkips(int context, List<SkippedLine> skips) {
+    if (sideA.containsKey(0) || sideB.containsKey(0)) {
+      // Special case of file comment; cannot skip first line.
+      for (SkippedLine skip : skips) {
+        if (skip.getStartA() == 0) {
+          skip.incrementStart(1);
+          break;
+        }
+      }
+    }
+
+    for (int boxLine : getLinesWithCommentGroups()) {
+      List<SkippedLine> temp = new ArrayList<>(skips.size() + 2);
+      for (SkippedLine skip : skips) {
+        int startLine = host.getCmLine(skip.getStartB(), DisplaySide.B);
+        int deltaBefore = boxLine - startLine;
+        int deltaAfter = startLine + skip.getSize() - boxLine;
+        if (deltaBefore < -context || deltaAfter < -context) {
+          temp.add(skip); // Size guaranteed to be greater than 1
+        } else if (deltaBefore > context && deltaAfter > context) {
+          SkippedLine before = new SkippedLine(
+              skip.getStartA(), skip.getStartB(),
+              skip.getSize() - deltaAfter - context);
+          skip.incrementStart(deltaBefore + context);
+          checkAndAddSkip(temp, before);
+          checkAndAddSkip(temp, skip);
+        } else if (deltaAfter > context) {
+          skip.incrementStart(deltaBefore + context);
+          checkAndAddSkip(temp, skip);
+        } else if (deltaBefore > context) {
+          skip.reduceSize(deltaAfter + context);
+          checkAndAddSkip(temp, skip);
+        }
+      }
+      if (temp.isEmpty()) {
+        return temp;
+      }
+      skips = temp;
+    }
+    return skips;
+  }
+
+  abstract void newDraftOnGutterClick(CodeMirror cm, String gutterClass,
+      int line);
+
+  abstract CommentGroup getCommentGroupOnActiveLine(CodeMirror cm);
+
+  Runnable toggleOpenBox(final CodeMirror cm) {
+    return new Runnable() {
+      @Override
+      public void run() {
+        CommentGroup group = getCommentGroupOnActiveLine(cm);
+        if (group != null) {
+          group.openCloseLast();
+        }
+      }
+    };
+  }
+
+  Runnable openCloseAll(final CodeMirror cm) {
+    return new Runnable() {
+      @Override
+      public void run() {
+        CommentGroup group = getCommentGroupOnActiveLine(cm);
+        if (group != null) {
+          group.openCloseAll();
+        }
+      }
+    };
+  }
+
+  SortedMap<Integer, CommentGroup> map(DisplaySide side) {
+    return side == DisplaySide.A ? sideA : sideB;
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentRange.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentRange.java
index 9f46e9b..d3c150d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentRange.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentRange.java
@@ -43,7 +43,7 @@
   public final native int endLine() /*-{ return this.end_line; }-*/;
   public final native int endCharacter() /*-{ return this.end_character; }-*/;
 
-  private final native void set(int sl, int sc, int el, int ec) /*-{
+  private native void set(int sl, int sc, int el, int ec) /*-{
     this.start_line = sl;
     this.start_character = sc;
     this.end_line = el;
@@ -52,4 +52,4 @@
 
   protected CommentRange() {
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentsCollections.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentsCollections.java
index b23a8cf..83f74a3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentsCollections.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentsCollections.java
@@ -29,17 +29,23 @@
 
 /** Collection of published and draft comments loaded from the server. */
 class CommentsCollections {
-  private String path;
-
+  private final String path;
+  private final PatchSet.Id base;
+  private final PatchSet.Id revision;
+  private NativeMap<JsArray<CommentInfo>> publishedBaseAll;
+  private NativeMap<JsArray<CommentInfo>> publishedRevisionAll;
   JsArray<CommentInfo> publishedBase;
   JsArray<CommentInfo> publishedRevision;
   JsArray<CommentInfo> draftsBase;
   JsArray<CommentInfo> draftsRevision;
 
-  void load(PatchSet.Id base, PatchSet.Id revision, String path,
-      CallbackGroup group) {
+  CommentsCollections(PatchSet.Id base, PatchSet.Id revision, String path) {
     this.path = path;
+    this.base = base;
+    this.revision = revision;
+  }
 
+  void load(CallbackGroup group) {
     if (base != null) {
       CommentApi.comments(base, group.add(publishedBase()));
     }
@@ -53,10 +59,25 @@
     }
   }
 
+  boolean hasCommentForPath(String filePath) {
+    if (base != null) {
+      JsArray<CommentInfo> forBase = publishedBaseAll.get(filePath);
+      if (forBase != null && forBase.length() > 0) {
+        return true;
+      }
+    }
+    JsArray<CommentInfo> forRevision = publishedRevisionAll.get(filePath);
+    if (forRevision != null && forRevision.length() > 0) {
+      return true;
+    }
+    return false;
+  }
+
   private AsyncCallback<NativeMap<JsArray<CommentInfo>>> publishedBase() {
     return new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
       @Override
       public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
+        publishedBaseAll = result;
         publishedBase = sort(result.get(path));
       }
 
@@ -70,6 +91,7 @@
     return new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
       @Override
       public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
+        publishedRevisionAll = result;
         publishedRevision = sort(result.get(path));
       }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffApi.java
index df51c75..bc5a305 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffApi.java
@@ -14,20 +14,17 @@
 
 package com.google.gerrit.client.diff;
 
+import static com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace.IGNORE_ALL;
+
 import com.google.gerrit.client.changes.ChangeApi;
 import com.google.gerrit.client.info.FileInfo;
 import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.client.rpc.RestApi;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
-import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 
 public class DiffApi {
-  public enum IgnoreWhitespace {
-    NONE, TRAILING, CHANGED, ALL
-  }
-
   public static void list(int id, String base, String revision,
       AsyncCallback<NativeMap<FileInfo>> cb) {
     RestApi api = ChangeApi.revision(id, revision).view("files");
@@ -71,7 +68,7 @@
   }
 
   public DiffApi ignoreWhitespace(DiffPreferencesInfo.Whitespace w) {
-    if (w != null && w != Whitespace.IGNORE_ALL) {
+    if (w != null && w != IGNORE_ALL) {
       call.addParameter("whitespace", w);
     }
     return this;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffChunkInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffChunkInfo.java
index 1e5c5e5..51a7c8b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffChunkInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffChunkInfo.java
@@ -15,32 +15,39 @@
 package com.google.gerrit.client.diff;
 
 /** Object recording the position of a diff chunk and whether it's an edit */
-class DiffChunkInfo {
-  private DisplaySide side;
-  private int start;
-  private int end;
-  private boolean edit;
+class DiffChunkInfo implements Comparable<DiffChunkInfo> {
+  final DisplaySide side;
+  final int start;
+  final int end;
+  final boolean edit;
 
-  DiffChunkInfo(DisplaySide side, int start, int end, boolean edit) {
+  private final int startOnOther;
+
+  DiffChunkInfo(DisplaySide side, int start, int startOnOther, int end,
+      boolean edit) {
     this.side = side;
     this.start = start;
+    this.startOnOther = startOnOther;
     this.end = end;
     this.edit = edit;
   }
 
-  DisplaySide getSide() {
-    return side;
+  /**
+   * Chunks are ordered by their starting line. If it's a deletion, use its
+   * corresponding line on the revision side for comparison. In the edit case,
+   * put the deletion chunk right before the insertion chunk. This placement
+   * guarantees well-ordering.
+   */
+  @Override
+  public int compareTo(DiffChunkInfo o) {
+    if (side == o.side) {
+      return start - o.start;
+    } else if (side == DisplaySide.A) {
+      int comp = startOnOther - o.start;
+      return comp == 0 ? -1 : comp;
+    } else {
+      int comp = start - o.startOnOther;
+      return comp == 0 ? 1 : comp;
+    }
   }
-
-  int getStart() {
-    return start;
-  }
-
-  int getEnd() {
-    return end;
-  }
-
-  boolean isEdit() {
-    return edit;
-  }
-}
\ No newline at end of file
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java
index 82dad3f..b7910f5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java
@@ -27,9 +27,6 @@
 import java.util.List;
 
 public class DiffInfo extends JavaScriptObject {
-  public static final String GITLINK = "x-git/gitlink";
-  public static final String SYMLINK = "x-git/symlink";
-
   public final native FileMeta metaA() /*-{ return this.meta_a; }-*/;
   public final native FileMeta metaB() /*-{ return this.meta_b; }-*/;
   public final native JsArrayString diffHeader() /*-{ return this.diff_header; }-*/;
@@ -45,7 +42,7 @@
     return filterWebLinks(DiffView.UNIFIED_DIFF);
   }
 
-  private final List<WebLinkInfo> filterWebLinks(DiffView diffView) {
+  private List<WebLinkInfo> filterWebLinks(DiffView diffView) {
     List<WebLinkInfo> filteredDiffWebLinks = new LinkedList<>();
     List<DiffWebLinkInfo> allDiffWebLinks = Natives.asList(webLinks());
     if (allDiffWebLinks != null) {
@@ -66,7 +63,7 @@
   public final ChangeType changeType() {
     return ChangeType.valueOf(changeTypeRaw());
   }
-  private final native String changeTypeRaw()
+  private native String changeTypeRaw()
   /*-{ return this.change_type }-*/;
 
   public final IntraLineStatus intralineStatus() {
@@ -75,7 +72,7 @@
         ? IntraLineStatus.valueOf(s)
         : IntraLineStatus.OFF;
   }
-  private final native String intralineStatusRaw()
+  private native String intralineStatusRaw()
   /*-{ return this.intraline_status }-*/;
 
   public final boolean hasSkip() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffScreen.java
index cf0f9aa..98f4eb5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffScreen.java
@@ -79,8 +79,8 @@
 
 /** Base class for SideBySide and Unified */
 abstract class DiffScreen extends Screen {
-  static final KeyMap RENDER_ENTIRE_FILE_KEYMAP = KeyMap.create()
-      .propagate("Ctrl-F");
+  private static final KeyMap RENDER_ENTIRE_FILE_KEYMAP = KeyMap.create()
+      .propagate("Ctrl-F").propagate("Ctrl-G").propagate("Shift-Ctrl-G");
 
   enum FileSize {
     SMALL(0),
@@ -99,7 +99,7 @@
   final PatchSet.Id revision;
   final String path;
   final DiffPreferences prefs;
-  final DiffView diffScreenType;
+  final SkipManager skipManager;
 
   private DisplaySide startSide;
   private int startLine;
@@ -133,12 +133,13 @@
     this.path = path;
     this.startSide = startSide;
     this.startLine = startLine;
-    this.diffScreenType = diffScreenType;
 
     prefs = DiffPreferences.create(Gerrit.getDiffPreferences());
     handlers = new ArrayList<>(6);
     keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation());
-    header = new Header(keysNavigation, base, revision, path, diffScreenType);
+    header = new Header(
+        keysNavigation, base, revision, path, diffScreenType, prefs);
+    skipManager = new SkipManager(this);
   }
 
   @Override
@@ -208,8 +209,9 @@
           }));
     }
 
-    final CommentsCollections comments = new CommentsCollections();
-    comments.load(base, revision, path, group2);
+    final CommentsCollections comments =
+        new CommentsCollections(base, revision, path);
+    comments.load(group2);
 
     RestApi call = ChangeApi.detail(changeId.get());
     ChangeList.addOptions(call, EnumSet.of(
@@ -267,10 +269,6 @@
     return keysAction;
   }
 
-  KeyCommandSet getKeysComment() {
-    return keysComment;
-  }
-
   @Override
   protected void onUnload() {
     super.onUnload();
@@ -315,7 +313,6 @@
         .on("']'", header.navigate(Direction.NEXT))
         .on("R", header.toggleReviewed())
         .on("O", getCommentManager().toggleOpenBox(cm))
-        .on("Enter", getCommentManager().toggleOpenBox(cm))
         .on("N", maybeNextVimSearch(cm))
         .on("Ctrl-Alt-E", openEditScreen(cm))
         .on("P", getChunkManager().diffChunkNav(cm, Direction.PREV))
@@ -365,7 +362,34 @@
         .on("Ctrl-F", new Runnable() {
           @Override
           public void run() {
-            cm.vim().handleKey("/");
+            cm.execCommand("find");
+          }
+        })
+        .on("Ctrl-G", new Runnable() {
+          @Override
+          public void run() {
+            cm.execCommand("findNext");
+          }
+        })
+        .on("Enter", maybeNextCmSearch(cm))
+        .on("Shift-Ctrl-G", new Runnable() {
+          @Override
+          public void run() {
+            cm.execCommand("findPrev");
+          }
+        })
+        .on("Shift-Enter", new Runnable() {
+          @Override
+          public void run() {
+            cm.execCommand("findPrev");
+          }
+        })
+        .on("Esc", new Runnable() {
+          @Override
+          public void run() {
+            cm.setCursor(cm.getCursor());
+            cm.execCommand("clearSearch");
+            cm.vim().handleEx("nohlsearch");
           }
         })
         .on("Ctrl-A", new Runnable() {
@@ -477,8 +501,8 @@
         new NoOpKeyCommand(0, 'j', PatchUtil.C.lineNext()),
         new NoOpKeyCommand(0, 'k', PatchUtil.C.linePrev()));
     keysNavigation.add(
-        new NoOpKeyCommand(0, 'n', PatchUtil.C.chunkNext2()),
-        new NoOpKeyCommand(0, 'p', PatchUtil.C.chunkPrev2()));
+        new NoOpKeyCommand(0, 'n', PatchUtil.C.chunkNext()),
+        new NoOpKeyCommand(0, 'p', PatchUtil.C.chunkPrev()));
     keysNavigation.add(
         new NoOpKeyCommand(KeyCommand.M_SHIFT, 'n', PatchUtil.C.commentNext()),
         new NoOpKeyCommand(KeyCommand.M_SHIFT, 'p', PatchUtil.C.commentPrev()));
@@ -574,7 +598,15 @@
     getChunkManager().render(diff);
   }
 
-  abstract void setShowLineNumbers(boolean b);
+  void setShowLineNumbers(boolean b) {
+    if (b) {
+      getDiffTable().addStyleName(
+          Resources.I.diffTableStyle().showLineNumbers());
+    } else {
+      getDiffTable().removeStyleName(
+          Resources.I.diffTableStyle().showLineNumbers());
+    }
+  }
 
   void setShowIntraline(boolean b) {
     if (b && getIntraLineStatus() == DiffInfo.IntraLineStatus.OFF) {
@@ -586,7 +618,7 @@
     }
   }
 
-  void toggleShowIntraline() {
+  private void toggleShowIntraline() {
     prefs.intralineDifference(!prefs.intralineDifference());
     setShowIntraline(prefs.intralineDifference());
     prefsAction.update();
@@ -598,8 +630,8 @@
     operation(new Runnable() {
       @Override
       public void run() {
-        getSkipManager().removeAll();
-        getSkipManager().render(context, diff);
+        skipManager.removeAll();
+        skipManager.render(context, diff);
         updateRenderEntireFile();
       }
     });
@@ -690,8 +722,6 @@
 
   abstract CommentManager getCommentManager();
 
-  abstract SkipManager getSkipManager();
-
   Change.Status getChangeStatus() {
     return changeStatus;
   }
@@ -730,7 +760,7 @@
 
   abstract void operation(final Runnable apply);
 
-  Runnable upToChange(final boolean openReplyBox) {
+  private Runnable upToChange(final boolean openReplyBox) {
     return new Runnable() {
       @Override
       public void run() {
@@ -752,12 +782,12 @@
     };
   }
 
-  Runnable maybePrevVimSearch(final CodeMirror cm) {
+  private Runnable maybePrevVimSearch(final CodeMirror cm) {
     return new Runnable() {
       @Override
       public void run() {
         if (cm.vim().hasSearchHighlight()) {
-          cm.vim().handleKey("n");
+          cm.vim().handleKey("N");
         } else {
           getCommentManager().commentNav(cm, Direction.NEXT).run();
         }
@@ -765,7 +795,7 @@
     };
   }
 
-  Runnable maybeNextVimSearch(final CodeMirror cm) {
+  private Runnable maybeNextVimSearch(final CodeMirror cm) {
     return new Runnable() {
       @Override
       public void run() {
@@ -778,6 +808,20 @@
     };
   }
 
+  Runnable maybeNextCmSearch(final CodeMirror cm) {
+    return new Runnable() {
+      @Override
+      public void run() {
+        if (cm.hasSearchHighlight()) {
+          cm.execCommand("findNext");
+        } else {
+          cm.execCommand("clearSearch");
+          getCommentManager().toggleOpenBox(cm).run();
+        }
+      }
+    };
+  }
+
   boolean renderEntireFile() {
     return prefs.renderEntireFile() && canRenderEntireFile(prefs);
   }
@@ -873,12 +917,12 @@
             operation(new Runnable() {
               @Override
               public void run() {
-                getSkipManager().removeAll();
+                skipManager.removeAll();
                 getChunkManager().reset();
                 getDiffTable().scrollbar.removeDiffAnnotations();
                 setShowIntraline(prefs.intralineDifference());
                 render(diff);
-                getSkipManager().render(prefs.context(), diff);
+                skipManager.render(prefs.context(), diff);
               }
             });
           }
@@ -886,7 +930,7 @@
       });
   }
 
-  static FileSize bucketFileSize(DiffInfo diff) {
+  private static FileSize bucketFileSize(DiffInfo diff) {
     FileMeta a = diff.metaA();
     FileMeta b = diff.metaB();
     FileSize[] sizes = FileSize.values();
@@ -905,9 +949,11 @@
   private GutterClickHandler onGutterClick(final CodeMirror cm) {
     return new GutterClickHandler() {
       @Override
-      public void handle(CodeMirror instance, final int line, final String gutterClass,
-          NativeEvent clickEvent) {
-        if (clickEvent.getButton() == NativeEvent.BUTTON_LEFT
+      public void handle(CodeMirror instance, final int line,
+          final String gutterClass, NativeEvent clickEvent) {
+        if (Element.as(clickEvent.getEventTarget())
+                .hasClassName(getLineNumberClassName())
+            && clickEvent.getButton() == NativeEvent.BUTTON_LEFT
             && !clickEvent.getMetaKey()
             && !clickEvent.getAltKey()
             && !clickEvent.getCtrlKey()
@@ -916,7 +962,8 @@
           Scheduler.get().scheduleDeferred(new ScheduledCommand() {
             @Override
             public void execute() {
-              getCommentManager().newDraftOnGutterClick(cm, gutterClass, line + 1);
+              getCommentManager().newDraftOnGutterClick(
+                  cm, gutterClass, line + 1);
             }
           });
         }
@@ -932,10 +979,16 @@
 
   abstract DiffTable getDiffTable();
 
+  abstract int getCmLine(int line, DisplaySide side);
+
+  abstract String getLineNumberClassName();
+
   LineOnOtherInfo lineOnOther(DisplaySide side, int line) {
-    return getChunkManager().getLineMapper().lineOnOther(side, line);
+    return getChunkManager().lineMapper.lineOnOther(side, line);
   }
 
   abstract ScreenLoadCallback<ConfigInfoCache.Entry> getScreenLoadCallback(
       CommentsCollections comments);
+
+  abstract boolean isSideBySide();
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.css
index 2378f18..7569cf5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.css
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.css
@@ -35,6 +35,7 @@
   margin: 0 0 3px 0;
 }
 
-@external .dark, .noIntraline;
+@external .dark, .noIntraline, .showLineNumbers;
 .dark {}
-.noIntraline {}
\ No newline at end of file
+.noIntraline {}
+.showLineNumbers {}
\ No newline at end of file
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 58cc2c0..4374986 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
@@ -46,6 +46,7 @@
     String range();
     String rangeHighlight();
     String diffHeader();
+    String showLineNumbers();
   }
 
   @UiField Element patchSetNavRow;
@@ -97,6 +98,16 @@
     return changeType;
   }
 
+  void setUpBlameIconA(CodeMirror cm, boolean isBase, PatchSet.Id rev,
+      String path) {
+    patchSetSelectBoxA.setUpBlame(cm, isBase, rev, path);
+  }
+
+  void setUpBlameIconB(CodeMirror cm, PatchSet.Id rev,
+      String path) {
+    patchSetSelectBoxB.setUpBlame(cm, false, rev, path);
+  }
+
   void set(DiffPreferences prefs, JsArray<RevisionInfo> list, DiffInfo info,
       boolean editExists, boolean current, boolean open, boolean binary) {
     this.changeType = info.changeType();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DisplaySide.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DisplaySide.java
index 4dfcd8c..c7ee678 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DisplaySide.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DisplaySide.java
@@ -16,5 +16,9 @@
 
 /** Enum representing the side on a side-by-side view */
 public enum DisplaySide {
-  A, B
+  A, B;
+
+  DisplaySide otherSide() {
+    return this == A ? B : A;
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java
index a554df6..e7dabbb 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.account.DiffPreferences;
 import com.google.gerrit.client.changes.ChangeApi;
 import com.google.gerrit.client.changes.ReviewInfo;
 import com.google.gerrit.client.changes.Util;
@@ -35,6 +36,7 @@
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DiffView;
 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.gwt.core.client.GWT;
 import com.google.gwt.core.client.JsArray;
@@ -68,7 +70,7 @@
     Resources.I.style().ensureInjected();
   }
 
-  private static enum ReviewedState {
+  private enum ReviewedState {
     AUTO_REVIEW, LOADED
   }
 
@@ -91,20 +93,23 @@
   private final PatchSet.Id patchSetId;
   private final String path;
   private final DiffView diffScreenType;
+  private final DiffPreferences prefs;
   private boolean hasPrev;
   private boolean hasNext;
   private String nextPath;
+  private JsArray<FileInfo> files;
   private PreferencesAction prefsAction;
   private ReviewedState reviewedState;
 
   Header(KeyCommandSet keys, PatchSet.Id base, PatchSet.Id patchSetId,
-      String path, DiffView diffSreenType) {
+      String path, DiffView diffSreenType, DiffPreferences prefs) {
     initWidget(uiBinder.createAndBindUi(this));
     this.keys = keys;
     this.base = base;
     this.patchSetId = patchSetId;
     this.path = path;
     this.diffScreenType = diffSreenType;
+    this.prefs = prefs;
 
     if (!Gerrit.isSignedIn()) {
       reviewed.getElement().getStyle().setVisibility(Visibility.HIDDEN);
@@ -141,34 +146,27 @@
     return b;
   }
 
+  private int findCurrentFileIndex(JsArray<FileInfo> files) {
+    int currIndex = 0;
+    for (int i = 0; i < files.length(); i++) {
+      if (path.equals(files.get(i).path())) {
+        currIndex = i;
+        break;
+      }
+    }
+    return currIndex;
+  }
+
   @Override
   protected void onLoad() {
     DiffApi.list(patchSetId, base, new GerritCallback<NativeMap<FileInfo>>() {
       @Override
       public void onSuccess(NativeMap<FileInfo> result) {
-        JsArray<FileInfo> files = result.values();
+        files = result.values();
         FileInfo.sortFileInfoByPath(files);
         fileNumber.setInnerText(
             Integer.toString(Natives.asList(files).indexOf(result.get(path)) + 1));
         fileCount.setInnerText(Integer.toString(files.length()));
-        int index = 0; // TODO: Maybe use patchIndex.
-        for (int i = 0; i < files.length(); i++) {
-          if (path.equals(files.get(i).path())) {
-            index = i;
-            break;
-          }
-        }
-        FileInfo nextInfo = index == files.length() - 1
-            ? null
-            : files.get(index + 1);
-        KeyCommand p = setupNav(prev, '[', PatchUtil.C.previousFileHelp(),
-            index == 0 ? null : files.get(index - 1));
-        KeyCommand n = setupNav(next, ']', PatchUtil.C.nextFileHelp(),
-            nextInfo);
-        if (p != null && n != null) {
-          keys.pair(p, n);
-        }
-        nextPath = nextInfo != null ? nextInfo.path() : null;
       }
     });
 
@@ -302,6 +300,44 @@
     }
   }
 
+  private boolean shouldSkipFile(FileInfo curr, CommentsCollections comments) {
+    return prefs.skipDeleted() && ChangeType.DELETED.matches(curr.status())
+        || prefs.skipUnchanged() && ChangeType.RENAMED.matches(curr.status())
+        || prefs.skipUncommented() && !comments.hasCommentForPath(curr.path());
+  }
+
+  void setupPrevNextFiles(CommentsCollections comments) {
+    FileInfo prevInfo = null;
+    FileInfo nextInfo = null;
+    int currIndex = findCurrentFileIndex(files);
+    for (int i = currIndex - 1; i >= 0; i--) {
+      FileInfo curr = files.get(i);
+      if (shouldSkipFile(curr, comments)) {
+        continue;
+      } else {
+        prevInfo = curr;
+        break;
+      }
+    }
+    for (int i = currIndex + 1; i < files.length(); i++) {
+      FileInfo curr = files.get(i);
+      if (shouldSkipFile(curr, comments)) {
+        continue;
+      } else {
+        nextInfo = curr;
+        break;
+      }
+    }
+    KeyCommand p = setupNav(prev, '[', PatchUtil.C.previousFileHelp(),
+        prevInfo);
+    KeyCommand n = setupNav(next, ']', PatchUtil.C.nextFileHelp(),
+        nextInfo);
+    if (p != null && n != null) {
+      keys.pair(p, n);
+    }
+    nextPath = nextInfo != null ? nextInfo.path() : null;
+  }
+
   Runnable toggleReviewed() {
     return new Runnable() {
       @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/NoOpKeyCommand.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/NoOpKeyCommand.java
index c22769e..969b861 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/NoOpKeyCommand.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/NoOpKeyCommand.java
@@ -26,4 +26,4 @@
   @Override
   public void onKeyPress(KeyPressEvent event) {
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java
index 9f106ce..39b85cf 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java
@@ -16,18 +16,21 @@
 
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.blame.BlameInfo;
+import com.google.gerrit.client.changes.ChangeApi;
 import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
 import com.google.gerrit.client.info.WebLinkInfo;
 import com.google.gerrit.client.patches.PatchUtil;
+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.extensions.client.GeneralPreferencesInfo.DiffView;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Patch;
 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.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.resources.client.CssResource;
 import com.google.gwt.uibinder.client.UiBinder;
 import com.google.gwt.uibinder.client.UiField;
@@ -40,6 +43,8 @@
 import com.google.gwt.user.client.ui.Widget;
 import com.google.gwtorm.client.KeyUtil;
 
+import net.codemirror.lib.CodeMirror;
+
 import java.util.List;
 
 /** HTMLPanel to select among patch sets */
@@ -125,6 +130,32 @@
     }
   }
 
+  void setUpBlame(final CodeMirror cm, final boolean isBase,
+      final PatchSet.Id rev, final String path) {
+    if (!Patch.COMMIT_MSG.equals(path) && Gerrit.isSignedIn()
+        && Gerrit.info().change().allowBlame()) {
+      Anchor blameIcon = createBlameIcon();
+      blameIcon.addClickHandler(new ClickHandler() {
+        @Override
+        public void onClick(ClickEvent clickEvent) {
+          if (cm.extras().getBlameInfo() != null) {
+            cm.extras().toggleAnnotation();
+          } else {
+            ChangeApi.blame(rev, path, isBase)
+              .get(new GerritCallback<JsArray<BlameInfo>>() {
+
+                @Override
+                public void onSuccess(JsArray<BlameInfo> lines) {
+                  cm.extras().toggleAnnotation(lines);
+                }
+              });
+          }
+        }
+      });
+      linkPanel.add(blameIcon);
+    }
+  }
+
   private Widget createEditIcon() {
     PatchSet.Id id = (idActive == null) ? other.idActive : idActive;
     Anchor anchor = new Anchor(
@@ -134,6 +165,13 @@
     return anchor;
   }
 
+  private Anchor createBlameIcon() {
+    Anchor anchor = new Anchor(
+        new ImageResourceRenderer().render(Gerrit.RESOURCES.blame()));
+    anchor.setTitle(PatchUtil.C.blame());
+    return anchor;
+  }
+
   static void link(PatchSetSelectBox a, PatchSetSelectBox b) {
     a.other = b;
     b.other = a;
@@ -148,7 +186,7 @@
     PatchSet.Id revision = sideA ? other.idActive : id;
 
     return new InlineHyperlink(label,
-        parent.diffScreenType == DiffView.SIDE_BY_SIDE
+        parent.isSideBySide()
             ? Dispatcher.toSideBySide(diffBase, revision, path)
             : Dispatcher.toUnified(diffBase, revision, path));
   }
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 0d856eb..cc3c004 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
@@ -102,6 +102,9 @@
   @UiField ToggleButton expandAllComments;
   @UiField ToggleButton renderEntireFile;
   @UiField ToggleButton matchBrackets;
+  @UiField ToggleButton skipDeleted;
+  @UiField ToggleButton skipUnchanged;
+  @UiField ToggleButton skipUncommented;
   @UiField ListBox theme;
   @UiField Element modeLabel;
   @UiField ListBox mode;
@@ -194,6 +197,9 @@
     manualReview.setValue(prefs.manualReview());
     expandAllComments.setValue(prefs.expandAllComments());
     matchBrackets.setValue(prefs.matchBrackets());
+    skipDeleted.setValue(!prefs.skipDeleted());
+    skipUnchanged.setValue(!prefs.skipUnchanged());
+    skipUncommented.setValue(!prefs.skipUncommented());
     setTheme(prefs.theme());
 
     if (view == null || view.canRenderEntireFile(prefs)) {
@@ -497,6 +503,24 @@
         prefs.matchBrackets());
   }
 
+  @UiHandler("skipDeleted")
+  void onSkipDeleted(ValueChangeEvent<Boolean> e) {
+    prefs.skipDeleted(!e.getValue());
+    // TODO: Update the navigation links on the current DiffScreen
+  }
+
+  @UiHandler("skipUnchanged")
+  void onSkipUnchanged(ValueChangeEvent<Boolean> e) {
+    prefs.skipUnchanged(!e.getValue());
+    // TODO: Update the navigation links on the current DiffScreen
+  }
+
+  @UiHandler("skipUncommented")
+  void onSkipUncommented(ValueChangeEvent<Boolean> e) {
+    prefs.skipUncommented(!e.getValue());
+    // TODO: Update the navigation links on the current DiffScreen
+  }
+
   @UiHandler("theme")
   void onTheme(@SuppressWarnings("unused") ChangeEvent e) {
     final Theme newTheme = getSelectedTheme();
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 a2202ab..0a5e6a2 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
@@ -297,6 +297,27 @@
         </g:ToggleButton></td>
       </tr>
       <tr>
+        <th><ui:msg>Skip Deleted Files</ui:msg></th>
+        <td><g:ToggleButton ui:field='skipDeleted'>
+          <g:upFace><ui:msg>Yes</ui:msg></g:upFace>
+          <g:downFace><ui:msg>No</ui:msg></g:downFace>
+        </g:ToggleButton></td>
+      </tr>
+      <tr>
+        <th><ui:msg>Skip Unchanged Files</ui:msg></th>
+        <td><g:ToggleButton ui:field='skipUnchanged'>
+          <g:upFace><ui:msg>Yes</ui:msg></g:upFace>
+          <g:downFace><ui:msg>No</ui:msg></g:downFace>
+        </g:ToggleButton></td>
+      </tr>
+      <tr>
+        <th><ui:msg>Skip Uncommented Files</ui:msg></th>
+        <td><g:ToggleButton ui:field='skipUncommented'>
+          <g:upFace><ui:msg>Yes</ui:msg></g:upFace>
+          <g:downFace><ui:msg>No</ui:msg></g:downFace>
+        </g:ToggleButton></td>
+      </tr>
+      <tr>
         <td></td>
         <td>
           <g:Button ui:field='apply' styleName='{style.apply}'>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.java
index cfa6e00..dc2e3a2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.java
@@ -46,7 +46,7 @@
   interface Binder extends UiBinder<HTMLPanel, PublishedBox> {}
   private static final Binder uiBinder = GWT.create(Binder.class);
 
-  static interface Style extends CssResource {
+  interface Style extends CssResource {
     String closed();
   }
 
@@ -161,7 +161,7 @@
 
   void doReply() {
     if (!Gerrit.isSignedIn()) {
-      Gerrit.doSignIn(getCommentManager().getDiffScreen().getToken());
+      Gerrit.doSignIn(getCommentManager().host.getToken());
     } else if (replyBox == null) {
       addReplyBox(false);
     } else {
@@ -179,7 +179,7 @@
   void onQuote(ClickEvent e) {
     e.stopPropagation();
     if (!Gerrit.isSignedIn()) {
-      Gerrit.doSignIn(getCommentManager().getDiffScreen().getToken());
+      Gerrit.doSignIn(getCommentManager().host.getToken());
     }
     addReplyBox(true);
   }
@@ -188,7 +188,7 @@
   void onReplyDone(ClickEvent e) {
     e.stopPropagation();
     if (!Gerrit.isSignedIn()) {
-      Gerrit.doSignIn(getCommentManager().getDiffScreen().getToken());
+      Gerrit.doSignIn(getCommentManager().host.getToken());
     } else if (replyBox == null) {
       done.setEnabled(false);
       CommentInfo input = CommentInfo.createReply(comment);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Resources.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Resources.java
index 0359df3..2723374 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Resources.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Resources.java
@@ -20,7 +20,7 @@
 
 /** Resources used by diff. */
 interface Resources extends ClientBundle {
-  static final Resources I = GWT.create(Resources.class);
+  Resources I = GWT.create(Resources.class);
 
   @Source("CommentBox.css") CommentBox.Style style();
   @Source("Scrollbar.css") Scrollbar.Style scrollbarStyle();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java
index 931636f..fff556b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java
@@ -55,6 +55,7 @@
 public class SideBySide extends DiffScreen {
   interface Binder extends UiBinder<FlowPanel, SideBySide> {}
   private static final Binder uiBinder = GWT.create(Binder.class);
+  private static final String LINE_NUMBER_CLASSNAME = "CodeMirror-linenumber";
 
   @UiField(provided = true)
   SideBySideTable diffTable;
@@ -66,7 +67,6 @@
 
   private SideBySideChunkManager chunkManager;
   private SideBySideCommentManager commentManager;
-  private SideBySideSkipManager skipManager;
 
   public SideBySide(
       PatchSet.Id base,
@@ -94,6 +94,7 @@
             getChangeStatus().isOpen());
         setTheme(result.getTheme());
         display(comments);
+        header.setupPrevNextFiles(comments);
       }
     };
   }
@@ -111,18 +112,18 @@
         cmB.refresh();
       }
     });
-    setLineLength(Patch.COMMIT_MSG.equals(prefs) ? 72 : prefs.lineLength());
+    setLineLength(Patch.COMMIT_MSG.equals(path) ? 72 : prefs.lineLength());
     diffTable.refresh();
 
     if (getStartLine() == 0) {
       DiffChunkInfo d = chunkManager.getFirst();
       if (d != null) {
-        if (d.isEdit() && d.getSide() == DisplaySide.A) {
+        if (d.edit && d.side == DisplaySide.A) {
           setStartSide(DisplaySide.B);
-          setStartLine(lineOnOther(d.getSide(), d.getStart()).getLine() + 1);
+          setStartLine(lineOnOther(d.side, d.start).getLine() + 1);
         } else {
-          setStartSide(d.getSide());
-          setStartLine(d.getStart() + 1);
+          setStartSide(d.side);
+          setStartLine(d.start + 1);
         }
       }
     }
@@ -185,18 +186,22 @@
     setThemeStyles(prefs.theme().isDark());
     setShowIntraline(prefs.intralineDifference());
     if (prefs.showLineNumbers()) {
-      diffTable.addStyleName(SideBySideTable.style.showLineNumbers());
+      diffTable.addStyleName(Resources.I.diffTableStyle().showLineNumbers());
     }
 
     cmA = newCm(diff.metaA(), diff.textA(), diffTable.cmA);
     cmB = newCm(diff.metaB(), diff.textB(), diffTable.cmB);
 
+    boolean reviewingBase = base == null;
+    getDiffTable().setUpBlameIconA(cmA, reviewingBase,
+        reviewingBase ? revision : base, path);
+    getDiffTable().setUpBlameIconB(cmB, revision, path);
+
     cmA.extras().side(DisplaySide.A);
     cmB.extras().side(DisplaySide.B);
     setShowTabs(prefs.showTabs());
 
     chunkManager = new SideBySideChunkManager(this, cmA, cmB, diffTable.scrollbar);
-    skipManager = new SideBySideSkipManager(this, commentManager);
 
     operation(new Runnable() {
       @Override
@@ -216,7 +221,7 @@
     registerCmEvents(cmA);
     registerCmEvents(cmB);
     scrollSynchronizer = new ScrollSynchronizer(diffTable, cmA, cmB,
-            chunkManager.getLineMapper());
+            chunkManager.lineMapper);
 
     setPrefsAction(new PreferencesAction(this, prefs));
     header.init(getPrefsAction(), getUnifiedDiffLink(), diff.sideBySideWebLinks());
@@ -261,13 +266,10 @@
 
   @Override
   void setShowLineNumbers(boolean b) {
+    super.setShowLineNumbers(b);
+
     cmA.setOption("lineNumbers", b);
     cmB.setOption("lineNumbers", b);
-    if (b) {
-      diffTable.addStyleName(SideBySideTable.style.showLineNumbers());
-    } else {
-      diffTable.removeStyleName(SideBySideTable.style.showLineNumbers());
-    }
   }
 
   @Override
@@ -309,6 +311,11 @@
   }
 
   @Override
+  int getCmLine(int line, DisplaySide side) {
+    return line;
+  }
+
+  @Override
   Runnable updateActiveLine(final CodeMirror cm) {
     final CodeMirror other = otherCm(cm);
     return new Runnable() {
@@ -412,7 +419,12 @@
   }
 
   @Override
-  SideBySideSkipManager getSkipManager() {
-    return skipManager;
+  boolean isSideBySide() {
+    return true;
+  }
+
+  @Override
+  String getLineNumberClassName() {
+    return LINE_NUMBER_CLASSNAME;
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideChunkManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideChunkManager.java
index dcbcb673..117fece 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideChunkManager.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideChunkManager.java
@@ -45,7 +45,7 @@
   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) /*-{
+  private static native JavaScriptObject initOnClick(DisplaySide s) /*-{
     return $entry(function(e){
       @com.google.gerrit.client.diff.SideBySideChunkManager::focus(
         Lcom/google/gwt/dom/client/NativeEvent;
@@ -105,8 +105,6 @@
   void render(DiffInfo diff) {
     super.render();
 
-    LineMapper mapper = getLineMapper();
-
     chunks = new ArrayList<>();
     padding = new ArrayList<>();
     paddingDivs = new ArrayList<>();
@@ -117,11 +115,11 @@
 
     for (Region current : Natives.asList(diff.content())) {
       if (current.ab() != null) {
-        mapper.appendCommon(current.ab().length());
+        lineMapper.appendCommon(current.ab().length());
       } else if (current.skip() > 0) {
-        mapper.appendCommon(current.skip());
+        lineMapper.appendCommon(current.skip());
       } else if (current.common()) {
-        mapper.appendCommon(current.b().length());
+        lineMapper.appendCommon(current.b().length());
       } else {
         render(current, diffColor);
       }
@@ -148,10 +146,8 @@
   }
 
   private void render(Region region, String diffColor) {
-    LineMapper mapper = getLineMapper();
-
-    int startA = mapper.getLineA();
-    int startB = mapper.getLineB();
+    int startA = lineMapper.getLineA();
+    int startB = lineMapper.getLineB();
 
     JsArrayString a = region.a();
     JsArrayString b = region.b();
@@ -169,20 +165,19 @@
     addPadding(cmA, startA + aLen - 1, bLen - aLen);
     addPadding(cmB, startB + bLen - 1, aLen - bLen);
     addGutterTag(region, startA, startB);
-    mapper.appendReplace(aLen, bLen);
+    lineMapper.appendReplace(aLen, bLen);
 
-    int endA = mapper.getLineA() - 1;
-    int endB = mapper.getLineB() - 1;
+    int endA = lineMapper.getLineA() - 1;
+    int endB = lineMapper.getLineB() - 1;
     if (aLen > 0) {
-      addDiffChunk(cmB, endA, aLen, bLen > 0);
+      addDiffChunk(cmB, endB, endA, aLen, bLen > 0);
     }
     if (bLen > 0) {
-      addDiffChunk(cmA, endB, bLen, aLen > 0);
+      addDiffChunk(cmA, endA, endB, bLen, aLen > 0);
     }
   }
 
   private void addGutterTag(Region region, int startA, int startB) {
-    Scrollbar scrollbar = getScrollbar();
     if (region.a() == null) {
       scrollbar.insert(cmB, startB, region.b().length());
     } else if (region.b() == null) {
@@ -250,10 +245,10 @@
     }
   }
 
-  private void addDiffChunk(CodeMirror cmToPad, int lineOnOther,
+  private void addDiffChunk(CodeMirror cmToPad, int line, int lineOnOther,
       int chunkSize, boolean edit) {
     chunks.add(new DiffChunkInfo(host.otherCm(cmToPad).side(),
-        lineOnOther - chunkSize + 1, lineOnOther, edit));
+        lineOnOther - chunkSize + 1, line - chunkSize + 1, lineOnOther, edit));
   }
 
   @Override
@@ -266,8 +261,7 @@
             : 0;
         int res = Collections.binarySearch(
                 chunks,
-                new DiffChunkInfo(cm.side(), line, 0, false),
-                getDiffChunkComparator());
+                new DiffChunkInfo(cm.side(), line, 0, 0, false));
         diffChunkNavHelper(chunks, host, res, dir);
       }
     };
@@ -277,4 +271,4 @@
   int getCmLine(int line, DisplaySide side) {
     return line;
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentManager.java
index 88e431f..a2af3a1c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentManager.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentManager.java
@@ -16,348 +16,91 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.changes.CommentInfo;
-import com.google.gerrit.client.patches.SkippedLine;
-import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.CommentLinkProcessor;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gwt.core.client.JsArray;
 
 import net.codemirror.lib.CodeMirror;
-import net.codemirror.lib.CodeMirror.LineHandle;
-import net.codemirror.lib.Pos;
 import net.codemirror.lib.TextMarker.FromTo;
 
-import java.util.ArrayList;
-import java.util.List;
+import java.util.Collection;
 import java.util.SortedMap;
-import java.util.TreeMap;
 
 /** Tracks comment widgets for {@link SideBySide}. */
 class SideBySideCommentManager extends CommentManager {
-  private final SideBySide host;
-  private final SortedMap<Integer, SideBySideCommentGroup> sideA;
-  private final SortedMap<Integer, SideBySideCommentGroup> sideB;
-
   SideBySideCommentManager(SideBySide host,
       PatchSet.Id base, PatchSet.Id revision,
       String path,
       CommentLinkProcessor clp,
       boolean open) {
-    super(base, revision, path, clp, open);
-
-    this.host = host;
-    sideA = new TreeMap<>();
-    sideB = new TreeMap<>();
+    super(host, base, revision, path, clp, open);
   }
 
   @Override
-  SideBySide getDiffScreen() {
-    return host;
-  }
-
-  @Override
-  void setExpandAllComments(boolean b) {
-    setExpandAll(b);
-    for (SideBySideCommentGroup g : sideA.values()) {
-      g.setOpenAll(b);
-    }
-    for (SideBySideCommentGroup g : sideB.values()) {
-      g.setOpenAll(b);
-    }
-  }
-
-  @Override
-  Runnable commentNav(final CodeMirror src, final Direction dir) {
-    return new Runnable() {
-      @Override
-      public void run() {
-        // Every comment appears in both side maps as a linked pair.
-        // It is only necessary to search one side to find a comment
-        // on either side of the editor pair.
-        SortedMap<Integer, SideBySideCommentGroup> map = map(src.side());
-        int line = src.extras().hasActiveLine()
-            ? src.getLineNumber(src.extras().activeLine()) + 1
-            : 0;
-        if (dir == Direction.NEXT) {
-          map = map.tailMap(line + 1);
-          if (map.isEmpty()) {
-            return;
-          }
-          line = map.firstKey();
-        } else {
-          map = map.headMap(line);
-          if (map.isEmpty()) {
-            return;
-          }
-          line = map.lastKey();
-        }
-
-        SideBySideCommentGroup g = map.get(line);
-        if (g.getBoxCount() == 0) {
-          g = g.getPeer();
-        }
-
-        CodeMirror cm = g.getCm();
-        double y = cm.heightAtLine(g.getLine() - 1, "local");
-        cm.setCursor(Pos.create(g.getLine() - 1));
-        cm.scrollToY(y - 0.5 * cm.scrollbarV().getClientHeight());
-        cm.focus();
-      }
-    };
-  }
-
-  void render(CommentsCollections in, boolean expandAll) {
-    if (in.publishedBase != null) {
-      renderPublished(DisplaySide.A, in.publishedBase);
-    }
-    if (in.publishedRevision != null) {
-      renderPublished(DisplaySide.B, in.publishedRevision);
-    }
-    if (in.draftsBase != null) {
-      renderDrafts(DisplaySide.A, in.draftsBase);
-    }
-    if (in.draftsRevision != null) {
-      renderDrafts(DisplaySide.B, in.draftsRevision);
-    }
-    if (expandAll) {
-      setExpandAllComments(true);
-    }
-    for (SideBySideCommentGroup g : sideA.values()) {
-      g.init(host.getDiffTable());
-    }
-    for (SideBySideCommentGroup g : sideB.values()) {
-      g.init(host.getDiffTable());
-      g.handleRedraw();
-    }
-    setAttached(true);
-  }
-
-  @Override
-  void renderPublished(DisplaySide forSide, JsArray<CommentInfo> in) {
-    for (CommentInfo info : Natives.asList(in)) {
-      DisplaySide side = displaySide(info, forSide);
-      if (side != null) {
-        SideBySideCommentGroup group = group(side, info.line());
-        PublishedBox box = new PublishedBox(
-            group,
-            getCommentLinkProcessor(),
-            getPatchSetIdFromSide(side),
-            info,
-            side,
-            isOpen());
-        group.add(box);
-        box.setAnnotation(host.getDiffTable().scrollbar.comment(
-            host.getCmFromSide(side),
-            Math.max(0, info.line() - 1)));
-        getPublished().put(info.id(), box);
-      }
-    }
-  }
-
-  @Override
-  void newDraftOnGutterClick(CodeMirror cm, String gutterClass, int line) {
-    insertNewDraft(cm.side(), line);
-  }
-
-  /**
-   * Create a new {@link DraftBox} at the specified line and focus it.
-   *
-   * @param side which side the draft will appear on.
-   * @param line the line the draft will be at. Lines are 1-based. Line 0 is a
-   *        special case creating a file level comment.
-   */
-  @Override
-  void insertNewDraft(DisplaySide side, int line) {
-    if (line == 0) {
-      host.getSkipManager().ensureFirstLineIsVisible();
-    }
-
-    SideBySideCommentGroup group = group(side, line);
-    if (0 < group.getBoxCount()) {
-      CommentBox last = group.getCommentBox(group.getBoxCount() - 1);
-      if (last instanceof DraftBox) {
-        ((DraftBox)last).setEdit(true);
-      } else {
-        ((PublishedBox)last).doReply();
-      }
-    } else {
-      addDraftBox(side, CommentInfo.create(
-          getPath(),
-          getStoredSideFromDisplaySide(side),
-          line,
-          null)).setEdit(true);
-    }
-  }
-
-  @Override
-  DraftBox addDraftBox(DisplaySide side, CommentInfo info) {
-    SideBySideCommentGroup group = group(side, info.line());
-    DraftBox box = new DraftBox(
-        group,
-        getCommentLinkProcessor(),
-        getPatchSetIdFromSide(side),
-        info,
-        isExpandAll());
-
-    if (info.inReplyTo() != null) {
-      PublishedBox r = getPublished().get(info.inReplyTo());
-      if (r != null) {
-        r.setReplyBox(box);
-      }
-    }
-
-    group.add(box);
-    box.setAnnotation(host.getDiffTable().scrollbar.draft(
-        host.getCmFromSide(side),
-        Math.max(0, info.line() - 1)));
-    return box;
-  }
-
-  @Override
-  List<SkippedLine> splitSkips(int context, List<SkippedLine> skips) {
-    if (sideB.containsKey(0)) {
-      // Special case of file comment; cannot skip first line.
-      for (SkippedLine skip : skips) {
-        if (skip.getStartB() == 0) {
-          skip.incrementStart(1);
-        }
-      }
-    }
-
-    // TODO: This is not optimal, but shouldn't be too costly in most cases.
-    // Maybe rewrite after done keeping track of diff chunk positions.
-    for (int boxLine : sideB.tailMap(1).keySet()) {
-      List<SkippedLine> temp = new ArrayList<>(skips.size() + 2);
-      for (SkippedLine skip : skips) {
-        int startLine = skip.getStartB();
-        int deltaBefore = boxLine - startLine;
-        int deltaAfter = startLine + skip.getSize() - boxLine;
-        if (deltaBefore < -context || deltaAfter < -context) {
-          temp.add(skip); // Size guaranteed to be greater than 1
-        } else if (deltaBefore > context && deltaAfter > context) {
-          SkippedLine before = new SkippedLine(
-              skip.getStartA(), skip.getStartB(),
-              skip.getSize() - deltaAfter - context);
-          skip.incrementStart(deltaBefore + context);
-          checkAndAddSkip(temp, before);
-          checkAndAddSkip(temp, skip);
-        } else if (deltaAfter > context) {
-          skip.incrementStart(deltaBefore + context);
-          checkAndAddSkip(temp, skip);
-        } else if (deltaBefore > context) {
-          skip.reduceSize(deltaAfter + context);
-          checkAndAddSkip(temp, skip);
-        }
-      }
-      if (temp.isEmpty()) {
-        return temp;
-      }
-      skips = temp;
-    }
-    return skips;
-  }
-
-  private static void checkAndAddSkip(List<SkippedLine> out, SkippedLine s) {
-    if (s.getSize() > 1) {
-      out.add(s);
-    }
+  SortedMap<Integer, CommentGroup> getMapForNav(DisplaySide side) {
+    return map(side);
   }
 
   @Override
   void clearLine(DisplaySide side, int line, CommentGroup group) {
-    SortedMap<Integer, SideBySideCommentGroup> map = map(side);
-    if (map.get(line) == group) {
-      map.remove(line);
-    }
+    super.clearLine(side, line, group);
   }
 
   @Override
-  Runnable toggleOpenBox(final CodeMirror cm) {
-    return new Runnable() {
-      @Override
-      public void run() {
-        if (cm.extras().hasActiveLine()) {
-          SideBySideCommentGroup w = map(cm.side()).get(
-              cm.getLineNumber(cm.extras().activeLine()) + 1);
-          if (w != null) {
-            w.openCloseLast();
-          }
-        }
-      }
-    };
-  }
-
-  @Override
-  Runnable openCloseAll(final CodeMirror cm) {
-    return new Runnable() {
-      @Override
-      public void run() {
-        if (cm.extras().hasActiveLine()) {
-          SideBySideCommentGroup w = map(cm.side()).get(
-              cm.getLineNumber(cm.extras().activeLine()) + 1);
-          if (w != null) {
-            w.openCloseAll();
-          }
-        }
-      }
-    };
-  }
-
-  @Override
-  Runnable newDraftCallback(final CodeMirror cm) {
+  void newDraftOnGutterClick(CodeMirror cm, String gutterClass, int line) {
     if (!Gerrit.isSignedIn()) {
-      return new Runnable() {
-        @Override
-        public void run() {
-          String token = host.getToken();
-          if (cm.extras().hasActiveLine()) {
-            LineHandle handle = cm.extras().activeLine();
-            int line = cm.getLineNumber(handle) + 1;
-            token += "@" + (cm.side() == DisplaySide.A ? "a" : "") + line;
-          }
-          Gerrit.doSignIn(token);
-        }
-     };
+      signInCallback(cm).run();
+    } else {
+      insertNewDraft(cm.side(), line);
     }
-
-    return new Runnable() {
-      @Override
-      public void run() {
-        if (cm.extras().hasActiveLine()) {
-          newDraft(cm);
-        }
-      }
-    };
   }
 
-  private void newDraft(CodeMirror cm) {
+  @Override
+  CommentGroup getCommentGroupOnActiveLine(CodeMirror cm) {
+    CommentGroup group = null;
+    if (cm.extras().hasActiveLine()) {
+      group = map(cm.side())
+          .get(cm.getLineNumber(cm.extras().activeLine()) + 1);
+    }
+    return group;
+  }
+
+  @Override
+  Collection<Integer> getLinesWithCommentGroups() {
+    return sideB.tailMap(1).keySet();
+  }
+
+  @Override
+  String getTokenSuffixForActiveLine(CodeMirror cm) {
+    return (cm.side() == DisplaySide.A ? "a" : "")
+        + (cm.getLineNumber(cm.extras().activeLine()) + 1);
+  }
+
+  @Override
+  void newDraft(CodeMirror cm) {
     int line = cm.getLineNumber(cm.extras().activeLine()) + 1;
     if (cm.somethingSelected()) {
-      FromTo fromTo = cm.getSelectedRange();
-      Pos end = fromTo.to();
-      if (end.ch() == 0) {
-        end.line(end.line() - 1);
-        end.ch(cm.getLine(end.line()).length());
-      }
-
+      FromTo fromTo = adjustSelection(cm);
       addDraftBox(cm.side(), CommentInfo.create(
               getPath(),
               getStoredSideFromDisplaySide(cm.side()),
               line,
               CommentRange.create(fromTo))).setEdit(true);
+      cm.setCursor(fromTo.to());
       cm.setSelection(cm.getCursor());
     } else {
       insertNewDraft(cm.side(), line);
     }
   }
 
-  private SideBySideCommentGroup group(DisplaySide side, int line) {
-    SideBySideCommentGroup w = map(side).get(line);
+  @Override
+  CommentGroup group(DisplaySide side, int line) {
+    SideBySideCommentGroup w = (SideBySideCommentGroup) map(side).get(line);
     if (w != null) {
       return w;
     }
 
-    int lineA, lineB;
+    int lineA;
+    int lineB;
     if (line == 0) {
       lineA = lineB = 0;
     } else if (side == DisplaySide.A) {
@@ -386,8 +129,4 @@
   private SideBySideCommentGroup newGroup(DisplaySide side, int line) {
     return new SideBySideCommentGroup(this, host.getCmFromSide(side), side, line);
   }
-
-  private SortedMap<Integer, SideBySideCommentGroup> map(DisplaySide side) {
-    return side == DisplaySide.A ? sideA : sideB;
-  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideSkipBar.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideSkipBar.java
deleted file mode 100644
index 5020fed..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideSkipBar.java
+++ /dev/null
@@ -1,217 +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.client.diff;
-
-import com.google.gerrit.client.patches.PatchUtil;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.dom.client.Style.Unit;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-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.ui.Anchor;
-import com.google.gwt.user.client.ui.HTMLPanel;
-
-import net.codemirror.lib.CodeMirror;
-import net.codemirror.lib.Configuration;
-import net.codemirror.lib.LineWidget;
-import net.codemirror.lib.Pos;
-import net.codemirror.lib.TextMarker;
-import net.codemirror.lib.TextMarker.FromTo;
-
-/** The Widget that handles expanding of skipped lines */
-class SideBySideSkipBar extends SkipBar {
-  interface Binder extends UiBinder<HTMLPanel, SideBySideSkipBar> {}
-  private static final Binder uiBinder = GWT.create(Binder.class);
-  private static final int NUM_ROWS_TO_EXPAND = 10;
-  private static final int UP_DOWN_THRESHOLD = 30;
-
-  interface SkipBarStyle extends CssResource {
-    String noExpand();
-  }
-
-  @UiField(provided=true) Anchor skipNum;
-  @UiField(provided=true) Anchor upArrow;
-  @UiField(provided=true) Anchor downArrow;
-  @UiField SkipBarStyle style;
-
-  private final SideBySideSkipManager manager;
-  private final CodeMirror cm;
-
-  private LineWidget lineWidget;
-  private TextMarker textMarker;
-  private SideBySideSkipBar otherBar;
-
-  SideBySideSkipBar(SideBySideSkipManager manager, final CodeMirror cm) {
-    this.manager = manager;
-    this.cm = cm;
-
-    skipNum = new Anchor(true);
-    upArrow = new Anchor(true);
-    downArrow = new Anchor(true);
-    initWidget(uiBinder.createAndBindUi(this));
-    addDomHandler(new ClickHandler() {
-      @Override
-      public void onClick(ClickEvent event) {
-        cm.focus();
-      }
-    }, ClickEvent.getType());
-  }
-
-  void collapse(int start, int end, boolean attach) {
-    if (attach) {
-      boolean isNew = lineWidget == null;
-      Configuration cfg = Configuration.create()
-          .set("coverGutter", true)
-          .set("noHScroll", true);
-      if (start == 0) { // First line workaround
-        lineWidget = cm.addLineWidget(end + 1, getElement(), cfg.set("above", true));
-      } else {
-        lineWidget = cm.addLineWidget(start - 1, getElement(), cfg);
-      }
-      if (isNew) {
-        lineWidget.onFirstRedraw(new Runnable() {
-          @Override
-          public void run() {
-            int w = cm.getGutterElement().getOffsetWidth();
-            getElement().getStyle().setPaddingLeft(w, Unit.PX);
-          }
-        });
-      }
-    }
-
-    textMarker = cm.markText(
-        Pos.create(start, 0),
-        Pos.create(end),
-        Configuration.create()
-          .set("collapsed", true)
-          .set("inclusiveLeft", true)
-          .set("inclusiveRight", true));
-
-    textMarker.on("beforeCursorEnter", new Runnable() {
-      @Override
-      public void run() {
-        expandAll();
-      }
-    });
-
-    int skipped = end - start + 1;
-    if (skipped <= UP_DOWN_THRESHOLD) {
-      addStyleName(style.noExpand());
-    } else {
-      upArrow.setHTML(PatchUtil.M.expandBefore(NUM_ROWS_TO_EXPAND));
-      downArrow.setHTML(PatchUtil.M.expandAfter(NUM_ROWS_TO_EXPAND));
-    }
-    skipNum.setText(PatchUtil.M.patchSkipRegion(Integer
-        .toString(skipped)));
-  }
-
-  static void link(SideBySideSkipBar barA, SideBySideSkipBar barB) {
-    barA.otherBar = barB;
-    barB.otherBar = barA;
-  }
-
-  private void clearMarkerAndWidget() {
-    textMarker.clear();
-    lineWidget.clear();
-  }
-
-  @Override
-  void expandBefore(int cnt) {
-    expandSideBefore(cnt);
-
-    if (otherBar != null) {
-      otherBar.expandSideBefore(cnt);
-    }
-  }
-
-  private void expandSideBefore(int cnt) {
-    FromTo range = textMarker.find();
-    int oldStart = range.from().line();
-    int newStart = oldStart + cnt;
-    int end = range.to().line();
-    clearMarkerAndWidget();
-    collapse(newStart, end, true);
-    updateSelection();
-  }
-
-  @Override
-  void expandSideAll() {
-    clearMarkerAndWidget();
-    removeFromParent();
-  }
-
-  private void expandAfter() {
-    FromTo range = textMarker.find();
-    int start = range.from().line();
-    int oldEnd = range.to().line();
-    int newEnd = oldEnd - NUM_ROWS_TO_EXPAND;
-    boolean attach = start == 0;
-    if (attach) {
-      clearMarkerAndWidget();
-    } else {
-      textMarker.clear();
-    }
-    collapse(start, newEnd, attach);
-    updateSelection();
-  }
-
-  private void updateSelection() {
-    if (cm.somethingSelected()) {
-      FromTo sel = cm.getSelectedRange();
-      cm.setSelection(sel.from(), sel.to());
-    }
-  }
-
-  @UiHandler("skipNum")
-  void onExpandAll(@SuppressWarnings("unused") ClickEvent e) {
-    expandAll();
-    updateSelection();
-    if (otherBar != null) {
-      otherBar.expandAll();
-      otherBar.updateSelection();
-    }
-    cm.focus();
-  }
-
-  private void expandAll() {
-    expandSideAll();
-    if (otherBar != null) {
-      otherBar.expandSideAll();
-    }
-    manager.remove(this, otherBar);
-  }
-
-  @UiHandler("upArrow")
-  void onExpandBefore(@SuppressWarnings("unused") ClickEvent e) {
-    expandBefore(NUM_ROWS_TO_EXPAND);
-    if (otherBar != null) {
-      otherBar.expandBefore(NUM_ROWS_TO_EXPAND);
-    }
-    cm.focus();
-  }
-
-  @UiHandler("downArrow")
-  void onExpandAfter(@SuppressWarnings("unused") ClickEvent e) {
-    expandAfter();
-
-    if (otherBar != null) {
-      otherBar.expandAfter();
-    }
-    cm.focus();
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideSkipManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideSkipManager.java
deleted file mode 100644
index 64f10fc..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideSkipManager.java
+++ /dev/null
@@ -1,85 +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.client.diff;
-
-import com.google.gerrit.client.patches.SkippedLine;
-
-import net.codemirror.lib.CodeMirror;
-
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-/** Collapses common regions with {@link SideBySideSkipBar} for {@link SideBySide}. */
-class SideBySideSkipManager extends SkipManager {
-  private SideBySide host;
-
-  SideBySideSkipManager(SideBySide host, SideBySideCommentManager commentManager) {
-    super(commentManager);
-    this.host = host;
-  }
-
-  @Override
-  void render(int context, DiffInfo diff) {
-    List<SkippedLine> skips = getSkippedLines(context, diff);
-
-    if (!skips.isEmpty()) {
-      CodeMirror cmA = host.getCmFromSide(DisplaySide.A);
-      CodeMirror cmB = host.getCmFromSide(DisplaySide.B);
-
-      Set<SkipBar> skipBars = new HashSet<>();
-      setSkipBars(skipBars);
-      for (SkippedLine skip : skips) {
-        SideBySideSkipBar barA = newSkipBar(cmA, DisplaySide.A, skip);
-        SideBySideSkipBar barB = newSkipBar(cmB, DisplaySide.B, skip);
-        SideBySideSkipBar.link(barA, barB);
-        skipBars.add(barA);
-        skipBars.add(barB);
-
-        if (skip.getStartA() == 0 || skip.getStartB() == 0) {
-          barA.upArrow.setVisible(false);
-          barB.upArrow.setVisible(false);
-          setLine0(barB);
-        } else if (skip.getStartA() + skip.getSize() == getLineA()
-            || skip.getStartB() + skip.getSize() == getLineB()) {
-          barA.downArrow.setVisible(false);
-          barB.downArrow.setVisible(false);
-        }
-      }
-    }
-  }
-
-  void remove(SideBySideSkipBar a, SideBySideSkipBar b) {
-    Set<SkipBar> skipBars = getSkipBars();
-    skipBars.remove(a);
-    skipBars.remove(b);
-    if (getLine0() == a || getLine0() == b) {
-      setLine0(null);
-    }
-    if (skipBars.isEmpty()) {
-      setSkipBars(null);
-    }
-  }
-
-  private SideBySideSkipBar newSkipBar(CodeMirror cm, DisplaySide side, SkippedLine skip) {
-    int start = side == DisplaySide.A ? skip.getStartA() : skip.getStartB();
-    int end = start + skip.getSize() - 1;
-
-    SideBySideSkipBar bar = new SideBySideSkipBar(this, cm);
-    host.getDiffTable().add(bar);
-    bar.collapse(start, end, true);
-    return bar;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.java
index a286356..2296796 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.java
@@ -34,7 +34,6 @@
   interface DiffTableStyle extends CssResource {
     String intralineBg();
     String diff();
-    String showLineNumbers();
     String hideA();
     String hideB();
     String padding();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.ui.xml
index 63a202b..1eaf67f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.ui.xml
@@ -17,15 +17,15 @@
 <ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'
     xmlns:d='urn:import:com.google.gerrit.client.diff'>
-  <ui:with field='res' type='com.google.gerrit.client.diff.Resources'/>       
+  <ui:with field='res' type='com.google.gerrit.client.diff.Resources'/>
   <ui:style gss='false' type='com.google.gerrit.client.diff.SideBySideTable.DiffTableStyle'>
     @external .CodeMirror, .CodeMirror-selectedtext;
     @external .CodeMirror-linenumber;
     @external .CodeMirror-overlayscroll-vertical, .CodeMirror-scroll;
     @external .CodeMirror-dialog-bottom;
     @external .CodeMirror-cursor;
-    
-    @external .dark, .noIntraline;
+
+    @external .dark, .noIntraline, .showLineNumbers;
 
     .difftable .patchSetNav,
     .difftable .CodeMirror {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.java
index 24aedad..5f86955 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.java
@@ -14,9 +14,205 @@
 
 package com.google.gerrit.client.diff;
 
+import com.google.gerrit.client.patches.PatchUtil;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+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.ui.Anchor;
 import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.HTMLPanel;
 
-abstract class SkipBar extends Composite {
-  abstract void expandSideAll();
-  abstract void expandBefore(int cnt);
+import net.codemirror.lib.CodeMirror;
+import net.codemirror.lib.Configuration;
+import net.codemirror.lib.LineWidget;
+import net.codemirror.lib.Pos;
+import net.codemirror.lib.TextMarker;
+import net.codemirror.lib.TextMarker.FromTo;
+
+class SkipBar extends Composite {
+  interface Binder extends UiBinder<HTMLPanel, SkipBar> {}
+  private static final Binder uiBinder = GWT.create(Binder.class);
+  private static final int NUM_ROWS_TO_EXPAND = 10;
+  private static final int UP_DOWN_THRESHOLD = 30;
+
+  interface SkipBarStyle extends CssResource {
+    String noExpand();
+  }
+
+  @UiField(provided = true) Anchor skipNum;
+  @UiField(provided = true) Anchor upArrow;
+  @UiField(provided = true) Anchor downArrow;
+  @UiField SkipBarStyle style;
+
+  private final SkipManager manager;
+  private final CodeMirror cm;
+
+  private LineWidget lineWidget;
+  private TextMarker textMarker;
+  private SkipBar otherBar;
+
+  SkipBar(SkipManager manager, final CodeMirror cm) {
+    this.manager = manager;
+    this.cm = cm;
+
+    skipNum = new Anchor(true);
+    upArrow = new Anchor(true);
+    downArrow = new Anchor(true);
+    initWidget(uiBinder.createAndBindUi(this));
+    addDomHandler(new ClickHandler() {
+      @Override
+      public void onClick(ClickEvent event) {
+        cm.focus();
+      }
+    }, ClickEvent.getType());
+  }
+
+  void collapse(int start, int end, boolean attach) {
+    if (attach) {
+      boolean isNew = lineWidget == null;
+      Configuration cfg = Configuration.create()
+          .set("coverGutter", true)
+          .set("noHScroll", true);
+      if (start == 0) { // First line workaround
+        lineWidget = cm.addLineWidget(end + 1, getElement(), cfg.set("above", true));
+      } else {
+        lineWidget = cm.addLineWidget(start - 1, getElement(), cfg);
+      }
+      if (isNew) {
+        lineWidget.onFirstRedraw(new Runnable() {
+          @Override
+          public void run() {
+            int w = cm.getGutterElement().getOffsetWidth();
+            getElement().getStyle().setPaddingLeft(w, Unit.PX);
+          }
+        });
+      }
+    }
+
+    textMarker = cm.markText(
+        Pos.create(start, 0),
+        Pos.create(end),
+        Configuration.create()
+          .set("collapsed", true)
+          .set("inclusiveLeft", true)
+          .set("inclusiveRight", true));
+
+    textMarker.on("beforeCursorEnter", new Runnable() {
+      @Override
+      public void run() {
+        expandAll();
+      }
+    });
+
+    int skipped = end - start + 1;
+    if (skipped <= UP_DOWN_THRESHOLD) {
+      addStyleName(style.noExpand());
+    } else {
+      upArrow.setHTML(PatchUtil.M.expandBefore(NUM_ROWS_TO_EXPAND));
+      downArrow.setHTML(PatchUtil.M.expandAfter(NUM_ROWS_TO_EXPAND));
+    }
+    skipNum.setText(PatchUtil.M.patchSkipRegion(Integer
+        .toString(skipped)));
+  }
+
+  static void link(SkipBar barA, SkipBar barB) {
+    barA.otherBar = barB;
+    barB.otherBar = barA;
+  }
+
+  private void clearMarkerAndWidget() {
+    textMarker.clear();
+    lineWidget.clear();
+  }
+
+  void expandBefore(int cnt) {
+    expandSideBefore(cnt);
+
+    if (otherBar != null) {
+      otherBar.expandSideBefore(cnt);
+    }
+  }
+
+  private void expandSideBefore(int cnt) {
+    FromTo range = textMarker.find();
+    int oldStart = range.from().line();
+    int newStart = oldStart + cnt;
+    int end = range.to().line();
+    clearMarkerAndWidget();
+    collapse(newStart, end, true);
+    updateSelection();
+  }
+
+  void expandSideAll() {
+    clearMarkerAndWidget();
+    removeFromParent();
+  }
+
+  private void expandAfter() {
+    FromTo range = textMarker.find();
+    int start = range.from().line();
+    int oldEnd = range.to().line();
+    int newEnd = oldEnd - NUM_ROWS_TO_EXPAND;
+    boolean attach = start == 0;
+    if (attach) {
+      clearMarkerAndWidget();
+    } else {
+      textMarker.clear();
+    }
+    collapse(start, newEnd, attach);
+    updateSelection();
+  }
+
+  private void updateSelection() {
+    if (cm.somethingSelected()) {
+      FromTo sel = cm.getSelectedRange();
+      cm.setSelection(sel.from(), sel.to());
+    }
+  }
+
+  @UiHandler("skipNum")
+  void onExpandAll(@SuppressWarnings("unused") ClickEvent e) {
+    expandAll();
+    updateSelection();
+    if (otherBar != null) {
+      otherBar.expandAll();
+      otherBar.updateSelection();
+    }
+    cm.refresh();
+    cm.focus();
+  }
+
+  private void expandAll() {
+    expandSideAll();
+    if (otherBar != null) {
+      otherBar.expandSideAll();
+    }
+    manager.remove(this, otherBar);
+  }
+
+  @UiHandler("upArrow")
+  void onExpandBefore(@SuppressWarnings("unused") ClickEvent e) {
+    expandBefore(NUM_ROWS_TO_EXPAND);
+    if (otherBar != null) {
+      otherBar.expandBefore(NUM_ROWS_TO_EXPAND);
+    }
+    cm.refresh();
+    cm.focus();
+  }
+
+  @UiHandler("downArrow")
+  void onExpandAfter(@SuppressWarnings("unused") ClickEvent e) {
+    expandAfter();
+
+    if (otherBar != null) {
+      otherBar.expandAfter();
+    }
+    cm.refresh();
+    cm.focus();
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideSkipBar.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.ui.xml
similarity index 94%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideSkipBar.ui.xml
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.ui.xml
index d51f3d8..bf3c425 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideSkipBar.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.ui.xml
@@ -16,7 +16,7 @@
 -->
 <ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-  <ui:style gss='false' type='com.google.gerrit.client.diff.SideBySideSkipBar.SkipBarStyle'>
+  <ui:style gss='false' type='com.google.gerrit.client.diff.SkipBar.SkipBarStyle'>
     .skipBar {
       background-color: #def;
       height: 1.3em;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipManager.java
index 8290d98..cf23694 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipManager.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipManager.java
@@ -19,34 +19,34 @@
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gwt.core.client.JsArray;
 
+import net.codemirror.lib.CodeMirror;
+
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
-/** Collapses common regions with {@link SideBySideSkipBar} for {@link SideBySide}
+/** Collapses common regions with {@link SkipBar} for {@link SideBySide}
  *  and {@link Unified}. */
-abstract class SkipManager {
-  private Set<SkipBar> skipBars;
+class SkipManager {
+  private final Set<SkipBar> skipBars;
+  private final DiffScreen host;
   private SkipBar line0;
-  private CommentManager commentManager;
-  private int lineA;
-  private int lineB;
 
-  SkipManager(CommentManager commentManager) {
-    this.commentManager = commentManager;
+  SkipManager(DiffScreen host) {
+    this.host = host;
+    this.skipBars = new HashSet<>();
   }
 
-  abstract void render(int context, DiffInfo diff);
-
-  List<SkippedLine> getSkippedLines(int context, DiffInfo diff) {
+  void render(int context, DiffInfo diff) {
     if (context == DiffPreferencesInfo.WHOLE_FILE_CONTEXT) {
-      return new ArrayList<>();
+      return;
     }
 
-    lineA = 0;
-    lineB = 0;
-    JsArray<Region> regions = diff.content();
     List<SkippedLine> skips = new ArrayList<>();
+    int lineA = 0;
+    int lineB = 0;
+    JsArray<Region> regions = diff.content();
     for (int i = 0; i < regions.length(); i++) {
       Region current = regions.get(i);
       if (current.ab() != null || current.common() || current.skip() > 0) {
@@ -69,7 +69,56 @@
         lineB += current.b() != null ? current.b().length() : 0;
       }
     }
-    return commentManager.splitSkips(context, skips);
+    skips = host.getCommentManager().splitSkips(context, skips);
+    renderSkips(skips, lineA, lineB);
+  }
+
+  private void renderSkips(List<SkippedLine> skips, int lineA, int lineB) {
+    if (!skips.isEmpty()) {
+      boolean isSideBySide = host.isSideBySide();
+      CodeMirror cmA = null;
+      if (isSideBySide) {
+        cmA = host.getCmFromSide(DisplaySide.A);
+      }
+      CodeMirror cmB = host.getCmFromSide(DisplaySide.B);
+
+      for (SkippedLine skip : skips) {
+        SkipBar barA = null;
+        SkipBar barB = newSkipBar(cmB, DisplaySide.B, skip);
+        skipBars.add(barB);
+        if (isSideBySide) {
+          barA = newSkipBar(cmA, DisplaySide.A, skip);
+          SkipBar.link(barA, barB);
+          skipBars.add(barA);
+        }
+
+        if (skip.getStartA() == 0 || skip.getStartB() == 0) {
+          if (isSideBySide) {
+            barA.upArrow.setVisible(false);
+          }
+          barB.upArrow.setVisible(false);
+          setLine0(barB);
+        } else if (skip.getStartA() + skip.getSize() == lineA
+            || skip.getStartB() + skip.getSize() == lineB) {
+          if (isSideBySide) {
+            barA.downArrow.setVisible(false);
+          }
+          barB.downArrow.setVisible(false);
+        }
+      }
+    }
+  }
+
+  private SkipBar newSkipBar(CodeMirror cm, DisplaySide side,
+      SkippedLine skip) {
+    int start = host.getCmLine(
+        side == DisplaySide.A ? skip.getStartA() : skip.getStartB(), side);
+    int end = start + skip.getSize() - 1;
+
+    SkipBar bar = new SkipBar(this, cm);
+    host.getDiffTable().add(bar);
+    bar.collapse(start, end, true);
+    return bar;
   }
 
   void ensureFirstLineIsVisible() {
@@ -80,36 +129,27 @@
   }
 
   void removeAll() {
-    if (skipBars != null) {
+    if (!skipBars.isEmpty()) {
       for (SkipBar bar : skipBars) {
         bar.expandSideAll();
       }
-      skipBars = null;
       line0 = null;
     }
   }
 
+  void remove(SkipBar a, SkipBar b) {
+    skipBars.remove(a);
+    skipBars.remove(b);
+    if (getLine0() == a || getLine0() == b) {
+      setLine0(null);
+    }
+  }
+
   SkipBar getLine0() {
     return line0;
   }
 
-  int getLineA() {
-    return lineA;
-  }
-
-  int getLineB() {
-    return lineB;
-  }
-
   void setLine0(SkipBar bar) {
     line0 = bar;
   }
-
-  void setSkipBars(Set<SkipBar> bars) {
-    skipBars = bars;
-  }
-
-  Set<SkipBar> getSkipBars() {
-    return skipBars;
-  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Unified.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Unified.java
index 904d47d..006788f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Unified.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Unified.java
@@ -18,7 +18,7 @@
 
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.diff.UnifiedChunkManager.LineSidePair;
+import com.google.gerrit.client.diff.UnifiedChunkManager.LineRegionInfo;
 import com.google.gerrit.client.patches.PatchUtil;
 import com.google.gerrit.client.projects.ConfigInfoCache;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
@@ -32,9 +32,6 @@
 import com.google.gwt.core.client.Scheduler;
 import com.google.gwt.core.client.Scheduler.ScheduledCommand;
 import com.google.gwt.dom.client.Element;
-import com.google.gwt.dom.client.NativeEvent;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.dom.client.FocusEvent;
 import com.google.gwt.event.dom.client.FocusHandler;
 import com.google.gwt.event.dom.client.KeyPressEvent;
@@ -44,11 +41,11 @@
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.ImageResourceRenderer;
-import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.InlineHTML;
 import com.google.gwtexpui.globalkey.client.GlobalKey;
+import com.google.gwtexpui.safehtml.client.SafeHtml;
 
 import net.codemirror.lib.CodeMirror;
-import net.codemirror.lib.CodeMirror.GutterClickHandler;
 import net.codemirror.lib.CodeMirror.LineHandle;
 import net.codemirror.lib.Configuration;
 import net.codemirror.lib.Pos;
@@ -68,7 +65,6 @@
 
   private UnifiedChunkManager chunkManager;
   private UnifiedCommentManager commentManager;
-  private UnifiedSkipManager skipManager;
 
   private boolean autoHideDiffTableHeader;
 
@@ -98,6 +94,7 @@
             getChangeStatus().isOpen());
         setTheme(result.getTheme());
         display(comments);
+        header.setupPrevNextFiles(comments);
       }
     };
   }
@@ -119,12 +116,12 @@
     if (getStartLine() == 0) {
       DiffChunkInfo d = chunkManager.getFirst();
       if (d != null) {
-        if (d.isEdit() && d.getSide() == DisplaySide.A) {
+        if (d.edit && d.side == DisplaySide.A) {
           setStartSide(DisplaySide.B);
         } else {
-          setStartSide(d.getSide());
+          setStartSide(d.side);
         }
-        setStartLine(chunkManager.getCmLine(d.getStart(), d.getSide()) + 1);
+        setStartLine(chunkManager.getCmLine(d.start, d.side) + 1);
       }
     }
     if (getStartSide() != null && getStartLine() > 0) {
@@ -178,7 +175,9 @@
     final DiffInfo diff = getDiff();
     setThemeStyles(prefs.theme().isDark());
     setShowIntraline(prefs.intralineDifference());
-    // TODO: Handle showLineNumbers preference
+    if (prefs.showLineNumbers()) {
+      diffTable.addStyleName(Resources.I.diffTableStyle().showLineNumbers());
+    }
 
     cm = newCm(
         diff.metaA() == null ? diff.metaB() : diff.metaA(),
@@ -187,7 +186,6 @@
     setShowTabs(prefs.showTabs());
 
     chunkManager = new UnifiedChunkManager(this, cm, diffTable.scrollbar);
-    skipManager = new UnifiedSkipManager(this, commentManager);
 
     operation(new Runnable() {
       @Override
@@ -253,44 +251,29 @@
 
   @Override
   void setShowLineNumbers(boolean b) {
-    // TODO: Implement this
+    super.setShowLineNumbers(b);
+
+    cm.refresh();
   }
 
-  private GutterClickHandler onGutterClick(final int cmLine) {
-    return new GutterClickHandler() {
-      @Override
-      public void handle(CodeMirror instance, int line, String gutter,
-          NativeEvent clickEvent) {
-        if (clickEvent.getButton() == NativeEvent.BUTTON_LEFT
-            && !clickEvent.getMetaKey()
-            && !clickEvent.getAltKey()
-            && !clickEvent.getCtrlKey()
-            && !clickEvent.getShiftKey()) {
-          cm.setCursor(Pos.create(cmLine));
-          Scheduler.get().scheduleDeferred(new ScheduledCommand() {
-            @Override
-            public void execute() {
-              commentManager.newDraftCallback(cm).run();
-            }
-          });
-        }
-      }
-    };
-  }
-
-  LineHandle setLineNumber(DisplaySide side, final int cmLine, int line) {
-    Label gutter = new Label(String.valueOf(line));
-    gutter.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(ClickEvent event) {
-        onGutterClick(cmLine);
-      }
-    });
+  private void setLineNumber(DisplaySide side, int cmLine, Integer line,
+      String styleName) {
+    SafeHtml html = SafeHtml.asis(line != null ? line.toString() : "&nbsp;");
+    InlineHTML gutter = new InlineHTML(html);
     diffTable.add(gutter);
-    gutter.setStyleName(UnifiedTable.style.unifiedLineNumber());
-    return cm.setGutterMarker(cmLine,
-        side == DisplaySide.A ? UnifiedTable.style.lineNumbersLeft()
-            : UnifiedTable.style.lineNumbersRight(), gutter.getElement());
+    gutter.setStyleName(styleName);
+    cm.setGutterMarker(cmLine, side == DisplaySide.A
+        ? UnifiedTable.style.lineNumbersLeft()
+        : UnifiedTable.style.lineNumbersRight(), gutter.getElement());
+  }
+
+  void setLineNumber(DisplaySide side, int cmLine, int line) {
+    setLineNumber(side, cmLine, line, UnifiedTable.style.unifiedLineNumber());
+  }
+
+  void setLineNumberEmpty(DisplaySide side, int cmLine) {
+    setLineNumber(side, cmLine, null,
+        UnifiedTable.style.unifiedLineNumberEmpty());
   }
 
   @Override
@@ -362,12 +345,13 @@
     return cm;
   }
 
+  @Override
   int getCmLine(int line, DisplaySide side) {
     return chunkManager.getCmLine(line, side);
   }
 
-  LineSidePair getLineSidePairFromCmLine(int cmLine) {
-    return chunkManager.getLineSidePairFromCmLine(cmLine);
+  LineRegionInfo getLineRegionInfoFromCmLine(int cmLine) {
+    return chunkManager.getLineRegionInfoFromCmLine(cmLine);
   }
 
   @Override
@@ -385,10 +369,6 @@
     return new CodeMirror[] {cm};
   }
 
-  CodeMirror getCm() {
-    return cm;
-  }
-
   @Override
   UnifiedTable getDiffTable() {
     return diffTable;
@@ -405,7 +385,12 @@
   }
 
   @Override
-  UnifiedSkipManager getSkipManager() {
-    return skipManager;
+  boolean isSideBySide() {
+    return false;
+  }
+
+  @Override
+  String getLineNumberClassName() {
+    return UnifiedTable.style.unifiedLineNumber();
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedChunkManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedChunkManager.java
index 866f0f7..9e7b5b5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedChunkManager.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedChunkManager.java
@@ -38,7 +38,7 @@
 /** Colors modified regions for {@link Unified}. */
 class UnifiedChunkManager extends ChunkManager {
   private static final JavaScriptObject focus = initOnClick();
-  private static final native JavaScriptObject initOnClick() /*-{
+  private static native JavaScriptObject initOnClick() /*-{
     return $entry(function(e){
       @com.google.gerrit.client.diff.UnifiedChunkManager::focus(
         Lcom/google/gwt/dom/client/NativeEvent;)(e)
@@ -83,41 +83,39 @@
   void render(DiffInfo diff) {
     super.render();
 
-    LineMapper mapper = getLineMapper();
-
     chunks = new ArrayList<>();
 
     int cmLine = 0;
     boolean useIntralineBg = diff.metaA() == null || diff.metaB() == null;
 
     for (Region current : Natives.asList(diff.content())) {
-      int origLineA = mapper.getLineA();
-      int origLineB = mapper.getLineB();
+      int origLineA = lineMapper.getLineA();
+      int origLineB = lineMapper.getLineB();
       if (current.ab() != null) {
         int length = current.ab().length();
-        mapper.appendCommon(length);
+        lineMapper.appendCommon(length);
         for (int i = 0; i < length; i++) {
           host.setLineNumber(DisplaySide.A, cmLine + i, origLineA + i + 1);
           host.setLineNumber(DisplaySide.B, cmLine + i, origLineB + i + 1);
         }
         cmLine += length;
       } else if (current.skip() > 0) {
-        mapper.appendCommon(current.skip());
+        lineMapper.appendCommon(current.skip());
         cmLine += current.skip(); // Maybe current.ab().length();
       } else if (current.common()) {
-        mapper.appendCommon(current.b().length());
+        lineMapper.appendCommon(current.b().length());
         cmLine += current.b().length();
       } else {
         cmLine += render(current, cmLine, useIntralineBg);
       }
     }
+    host.setLineNumber(DisplaySide.A, cmLine, lineMapper.getLineA() + 1);
+    host.setLineNumber(DisplaySide.B, cmLine, lineMapper.getLineB() + 1);
   }
 
   private int render(Region region, int cmLine, boolean useIntralineBg) {
-    LineMapper mapper = getLineMapper();
-
-    int startA = mapper.getLineA();
-    int startB = mapper.getLineB();
+    int startA = lineMapper.getLineA();
+    int startB = lineMapper.getLineB();
 
     JsArrayString a = region.a();
     JsArrayString b = region.b();
@@ -137,19 +135,21 @@
     markEdit(DisplaySide.A, cmLine, a, region.editA());
     markEdit(DisplaySide.B, cmLine + aLen, b, region.editB());
     addGutterTag(region, cmLine); // TODO: verify addGutterTag
-    mapper.appendReplace(aLen, bLen);
+    lineMapper.appendReplace(aLen, bLen);
 
-    int endA = mapper.getLineA() - 1;
-    int endB = mapper.getLineB() - 1;
+    int endA = lineMapper.getLineA() - 1;
+    int endB = lineMapper.getLineB() - 1;
     if (aLen > 0) {
-      addDiffChunk(DisplaySide.A, endA, aLen, cmLine, bLen > 0);
+      addDiffChunk(DisplaySide.A, endA, endB, aLen, cmLine, bLen > 0);
       for (int j = 0; j < aLen; j++) {
         host.setLineNumber(DisplaySide.A, cmLine + j, startA + j + 1);
+        host.setLineNumberEmpty(DisplaySide.B, cmLine + j);
       }
     }
     if (bLen > 0) {
-      addDiffChunk(DisplaySide.B, endB, bLen, cmLine + aLen, aLen > 0);
+      addDiffChunk(DisplaySide.B, endB, endA, bLen, cmLine + aLen, aLen > 0);
       for (int j = 0; j < bLen; j++) {
+        host.setLineNumberEmpty(DisplaySide.A, cmLine + aLen + j);
         host.setLineNumber(DisplaySide.B, cmLine + aLen + j, startB + j + 1);
       }
     }
@@ -157,7 +157,6 @@
   }
 
   private void addGutterTag(Region region, int cmLine) {
-    Scrollbar scrollbar = getScrollbar();
     if (region.a() == null) {
       scrollbar.insert(cm, cmLine, region.b().length());
     } else if (region.b() == null) {
@@ -209,10 +208,10 @@
         : UnifiedTable.style.diffInsert();
   }
 
-  private void addDiffChunk(DisplaySide side, int chunkEnd, int chunkSize,
-      int cmLine, boolean edit) {
-    chunks.add(new UnifiedDiffChunkInfo(side, chunkEnd - chunkSize + 1, chunkEnd,
-        cmLine, edit));
+  private void addDiffChunk(DisplaySide side, int chunkEnd, int otherChunkEnd,
+      int chunkSize, int cmLine, boolean edit) {
+    chunks.add(new UnifiedDiffChunkInfo(side, chunkEnd - chunkSize + 1,
+        otherChunkEnd - chunkSize + 1, chunkEnd, cmLine, edit));
   }
 
   @Override
@@ -225,7 +224,7 @@
             : 0;
         int res = Collections.binarySearch(
                 chunks,
-                new UnifiedDiffChunkInfo(cm.side(), 0, 0, line, false),
+                new UnifiedDiffChunkInfo(cm.side(), 0, 0, 0, line, false),
                 getDiffChunkComparatorCmLine());
         diffChunkNavHelper(chunks, host, res, dir);
       }
@@ -237,7 +236,7 @@
     return new Comparator<UnifiedDiffChunkInfo>() {
       @Override
       public int compare(UnifiedDiffChunkInfo o1, UnifiedDiffChunkInfo o2) {
-        return o1.getCmLine() - o2.getCmLine();
+        return o1.cmLine - o2.cmLine;
       }
     };
   }
@@ -247,31 +246,30 @@
     int res =
         Collections.binarySearch(chunks,
             new UnifiedDiffChunkInfo(
-                side, line, 0, 0, false), // Dummy DiffChunkInfo
-            getDiffChunkComparator());
+                side, line, 0, 0, 0, false)); // Dummy DiffChunkInfo
     if (res >= 0) {
-      return chunks.get(res).getCmLine();
+      return chunks.get(res).cmLine;
     } else { // The line might be within a DiffChunk
       res = -res - 1;
       if (res > 0) {
         UnifiedDiffChunkInfo info = chunks.get(res - 1);
-        if (side == DisplaySide.A && info.isEdit()
-            && info.getSide() == DisplaySide.B) {
+        if (side == DisplaySide.A && info.edit
+            && info.side == DisplaySide.B) {
           // Need to use the start and cmLine of the deletion chunk
           UnifiedDiffChunkInfo delete = chunks.get(res - 2);
-          if (line <= delete.getEnd()) {
-            return delete.getCmLine() + line - delete.getStart();
+          if (line <= delete.end) {
+            return delete.cmLine + line - delete.start;
           } else {
             // Need to add the length of the insertion chunk
-            return delete.getCmLine() + line - delete.getStart()
-                + info.getEnd() - info.getStart() + 1;
+            return delete.cmLine + line - delete.start
+                + info.end - info.start + 1;
           }
-        } else if (side == info.getSide()) {
-          return info.getCmLine() + line - info.getStart();
+        } else if (side == info.side) {
+          return info.cmLine + line - info.start;
         } else {
-          return info.getCmLine()
-              + getLineMapper().lineOnOther(side, line).getLine()
-              - info.getStart();
+          return info.cmLine
+              + lineMapper.lineOnOther(side, line).getLine()
+              - info.start;
         }
       } else {
         return line;
@@ -279,52 +277,72 @@
     }
   }
 
-  LineSidePair getLineSidePairFromCmLine(int cmLine) {
+  LineRegionInfo getLineRegionInfoFromCmLine(int cmLine) {
     int res =
         Collections.binarySearch(chunks,
             new UnifiedDiffChunkInfo(
-                DisplaySide.A, 0, 0, cmLine, false), // Dummy DiffChunkInfo
+                DisplaySide.A, 0, 0, 0, cmLine, false), // Dummy DiffChunkInfo
             getDiffChunkComparatorCmLine());
-    if (res >= 0) {
+    if (res >= 0) {  // The line is right at the start of a diff chunk.
       UnifiedDiffChunkInfo info = chunks.get(res);
-      return new LineSidePair(info.getStart(), info.getSide());
-    } else {  // The line might be within a DiffChunk
+      return new LineRegionInfo(
+          info.start, displaySideToRegionType(info.side));
+    } else {  // The line might be within or after a diff chunk.
       res = -res - 1;
       if (res > 0) {
         UnifiedDiffChunkInfo info = chunks.get(res - 1);
-        int lineOnInfoSide = info.getStart() + cmLine - info.getCmLine();
-        if (lineOnInfoSide > info.getEnd()
-            && info.getSide() == DisplaySide.A) {
-          // For the common region after a deletion chunk, return the line and
-          // side info on side B
-          return new LineSidePair(
-              getLineMapper().lineOnOther(DisplaySide.A, lineOnInfoSide)
-                  .getLine(), DisplaySide.B);
-        } else {
-          return new LineSidePair(lineOnInfoSide, info.getSide());
+        int lineOnInfoSide = info.start + cmLine - info.cmLine;
+        if (lineOnInfoSide > info.end) { // After a diff chunk
+          if (info.side == DisplaySide.A) {
+            // For the common region after a deletion chunk, associate the line
+            // on side B with a common region.
+            return new LineRegionInfo(
+                lineMapper.lineOnOther(DisplaySide.A, lineOnInfoSide)
+                    .getLine(), RegionType.COMMON);
+          } else {
+            return new LineRegionInfo(lineOnInfoSide, RegionType.COMMON);
+          }
+        } else { // Within a diff chunk
+          return new LineRegionInfo(
+              lineOnInfoSide, displaySideToRegionType(info.side));
         }
       } else {
-        // Always return side B
-        return new LineSidePair(cmLine, DisplaySide.B);
+        // The line is before any diff chunk, so it always equals cmLine and
+        // belongs to a common region.
+        return new LineRegionInfo(cmLine, RegionType.COMMON);
       }
     }
   }
 
-  static class LineSidePair {
-    private int line;
-    private DisplaySide side;
+  enum RegionType {
+    INSERT, DELETE, COMMON,
+  }
 
-    LineSidePair(int line, DisplaySide side) {
+  private static RegionType displaySideToRegionType(DisplaySide side) {
+    return side == DisplaySide.A ? RegionType.DELETE : RegionType.INSERT;
+  }
+
+  /**
+   * Helper class to associate a line in the original file with the type of the
+   * region it belongs to.
+   *
+   * @field line The 0-based line number in the original file. Note that this
+   *     might be different from the line number shown in CodeMirror.
+   * @field type The type of the region the line belongs to. Can be INSERT,
+   *     DELETE or COMMON.
+   */
+  static class LineRegionInfo {
+    final int line;
+    final RegionType type;
+
+    LineRegionInfo(int line, RegionType type) {
       this.line = line;
-      this.side = side;
-    }
-
-    int getLine() {
-      return line;
+      this.type = type;
     }
 
     DisplaySide getSide() {
-      return side;
+      // Always return DisplaySide.B for INSERT or COMMON
+      return type == RegionType.DELETE ? DisplaySide.A : DisplaySide.B;
     }
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedCommentManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedCommentManager.java
index 9038fb4..8968bc7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedCommentManager.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedCommentManager.java
@@ -16,357 +16,197 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.changes.CommentInfo;
-import com.google.gerrit.client.diff.UnifiedChunkManager.LineSidePair;
-import com.google.gerrit.client.patches.SkippedLine;
-import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.client.diff.LineMapper.LineOnOtherInfo;
+import com.google.gerrit.client.diff.UnifiedChunkManager.LineRegionInfo;
+import com.google.gerrit.client.diff.UnifiedChunkManager.RegionType;
 import com.google.gerrit.client.ui.CommentLinkProcessor;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gwt.core.client.JsArray;
 
 import net.codemirror.lib.CodeMirror;
-import net.codemirror.lib.CodeMirror.LineHandle;
 import net.codemirror.lib.Pos;
+import net.codemirror.lib.TextMarker.FromTo;
 
-import java.util.ArrayList;
-import java.util.List;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.SortedMap;
 import java.util.TreeMap;
-import java.util.TreeSet;
 
 /** Tracks comment widgets for {@link Unified}. */
 class UnifiedCommentManager extends CommentManager {
-  private final Unified host;
-  private final SortedMap<Integer, UnifiedCommentGroup> sideA;
-  private final SortedMap<Integer, UnifiedCommentGroup> sideB;
+
+  private final SortedMap<Integer, CommentGroup> mergedMap;
+
+  // In Unified, a CodeMirror line can have up to two CommentGroups - one for
+  // the base side and one for the revision, so we need to keep track of the
+  // duplicates and replace the entries in mergedMap on draft removal.
+  private final Map<Integer, CommentGroup> duplicates;
 
   UnifiedCommentManager(Unified host,
       PatchSet.Id base, PatchSet.Id revision,
       String path,
       CommentLinkProcessor clp,
       boolean open) {
-    super(base, revision, path, clp, open);
-
-    this.host = host;
-    sideA = new TreeMap<>();
-    sideB = new TreeMap<>();
+    super(host, base, revision, path, clp, open);
+    mergedMap = new TreeMap<>();
+    duplicates = new HashMap<>();
   }
 
   @Override
-  Unified getDiffScreen() {
-    return host;
+  SortedMap<Integer, CommentGroup> getMapForNav(DisplaySide side) {
+    return mergedMap;
   }
 
   @Override
-  void setExpandAllComments(boolean b) {
-    setExpandAll(b);
-    for (UnifiedCommentGroup g : sideA.values()) {
-      g.setOpenAll(b);
-    }
-    for (UnifiedCommentGroup g : sideB.values()) {
-      g.setOpenAll(b);
-    }
-  }
+  void clearLine(DisplaySide side, int line, CommentGroup group) {
+    super.clearLine(side, line, group);
 
-  @Override
-  Runnable commentNav(final CodeMirror src, final Direction dir) {
-    return new Runnable() {
-      @Override
-      public void run() {
-        SortedMap<Integer, UnifiedCommentGroup> map = map(src.side());
-        int line = src.extras().hasActiveLine()
-            ? src.getLineNumber(src.extras().activeLine()) + 1
-            : 0;
-        if (dir == Direction.NEXT) {
-          map = map.tailMap(line + 1);
-          if (map.isEmpty()) {
-            return;
-          }
-          line = map.firstKey();
-        } else {
-          map = map.headMap(line);
-          if (map.isEmpty()) {
-            return;
-          }
-          line = map.lastKey();
-        }
-
-        UnifiedCommentGroup g = map.get(line);
-        CodeMirror cm = g.getCm();
-        double y = cm.heightAtLine(g.getLine() - 1, "local");
-        cm.setCursor(Pos.create(g.getLine() - 1));
-        cm.scrollToY(y - 0.5 * cm.scrollbarV().getClientHeight());
-        cm.focus();
-      }
-    };
-  }
-
-  void render(CommentsCollections in, boolean expandAll) {
-    if (in.publishedBase != null) {
-      renderPublished(DisplaySide.A, in.publishedBase);
-    }
-    if (in.publishedRevision != null) {
-      renderPublished(DisplaySide.B, in.publishedRevision);
-    }
-    if (in.draftsBase != null) {
-      renderDrafts(DisplaySide.A, in.draftsBase);
-    }
-    if (in.draftsRevision != null) {
-      renderDrafts(DisplaySide.B, in.draftsRevision);
-    }
-    if (expandAll) {
-      setExpandAllComments(true);
-    }
-    for (CommentGroup g : sideA.values()) {
-      g.init(host.getDiffTable());
-    }
-    for (CommentGroup g : sideB.values()) {
-      g.init(host.getDiffTable());
-      g.handleRedraw();
-    }
-    setAttached(true);
-  }
-
-  @Override
-  void renderPublished(DisplaySide forSide, JsArray<CommentInfo> in) {
-    for (CommentInfo info : Natives.asList(in)) {
-      DisplaySide side = displaySide(info, forSide);
-      if (side != null) {
-        int cmLinePlusOne = host.getCmLine(info.line() - 1, side) + 1;
-        UnifiedCommentGroup group = group(side, cmLinePlusOne);
-        PublishedBox box = new PublishedBox(
-            group,
-            getCommentLinkProcessor(),
-            getPatchSetIdFromSide(side),
-            info,
-            side,
-            isOpen());
-        group.add(box);
-        box.setAnnotation(getDiffScreen().getDiffTable().scrollbar.comment(
-            host.getCm(),
-            cmLinePlusOne));
-        getPublished().put(info.id(), box);
+    if (mergedMap.get(line) == group) {
+      mergedMap.remove(line);
+      if (duplicates.containsKey(line)) {
+        mergedMap.put(line, duplicates.remove(line));
       }
     }
   }
 
   @Override
-  void newDraftOnGutterClick(CodeMirror cm, String gutterClass, int cmLinePlusOne) {
-    DisplaySide side = gutterClass.equals(UnifiedTable.style.lineNumbersLeft())
-        ? DisplaySide.A
-        : DisplaySide.B;
-    insertNewDraft(side, cmLinePlusOne);
-  }
-
-  /**
-   * Create a new {@link DraftBox} at the specified line and focus it.
-   *
-   * @param side which side the draft will appear on.
-   * @param cmLinePlusOne the line the draft will be at, plus one.
-   *        Lines are 1-based. Line 0 is a special case creating a file level comment.
-   */
-  @Override
-  void insertNewDraft(DisplaySide side, int cmLinePlusOne) {
-    if (cmLinePlusOne == 0) {
-      getDiffScreen().getSkipManager().ensureFirstLineIsVisible();
-    }
-
-    CommentGroup group = group(side, cmLinePlusOne);
-    if (0 < group.getBoxCount()) {
-      CommentBox last = group.getCommentBox(group.getBoxCount() - 1);
-      if (last instanceof DraftBox) {
-        ((DraftBox)last).setEdit(true);
-      } else {
-        ((PublishedBox)last).doReply();
-      }
-    } else {
-      LineSidePair pair = host.getLineSidePairFromCmLine(cmLinePlusOne - 1);
-      int line = pair.getLine();
-      if (pair.getSide() != side) {
-        line = host.lineOnOther(pair.getSide(), line).getLine();
-      }
-      addDraftBox(side, CommentInfo.create(
-          getPath(),
-          getStoredSideFromDisplaySide(side),
-          line + 1,
-          null)).setEdit(true);
-    }
-  }
-
-  @Override
-  DraftBox addDraftBox(DisplaySide side, CommentInfo info) {
-    int cmLinePlusOne = host.getCmLine(info.line() - 1, side) + 1;
-    UnifiedCommentGroup group = group(side, cmLinePlusOne);
-    DraftBox box = new DraftBox(
-        group,
-        getCommentLinkProcessor(),
-        getPatchSetIdFromSide(side),
-        info,
-        isExpandAll());
-
-    if (info.inReplyTo() != null) {
-      PublishedBox r = getPublished().get(info.inReplyTo());
-      if (r != null) {
-        r.setReplyBox(box);
-      }
-    }
-
-    group.add(box);
-    box.setAnnotation(getDiffScreen().getDiffTable().scrollbar.draft(
-        host.getCm(),
-        cmLinePlusOne));
-    return box;
-  }
-
-  @Override
-  List<SkippedLine> splitSkips(int context, List<SkippedLine> skips) {
-    if (sideA.containsKey(0) || sideB.containsKey(0)) {
-      // Special case of file comment; cannot skip first line.
-      for (SkippedLine skip : skips) {
-        if (skip.getStartA() == 0) {
-          skip.incrementStart(1);
-        }
-      }
-    }
-
-    TreeSet<Integer> allBoxLines = new TreeSet<>(sideA.tailMap(1).keySet());
-    allBoxLines.addAll(sideB.tailMap(1).keySet());
-    for (int boxLine : allBoxLines) {
-      List<SkippedLine> temp = new ArrayList<>(skips.size() + 2);
-      for (SkippedLine skip : skips) {
-        int startLine = host.getCmLine(skip.getStartA(), DisplaySide.A);
-        int deltaBefore = boxLine - startLine;
-        int deltaAfter = startLine + skip.getSize() - boxLine;
-        if (deltaBefore < -context || deltaAfter < -context) {
-          temp.add(skip); // Size guaranteed to be greater than 1
-        } else if (deltaBefore > context && deltaAfter > context) {
-          SkippedLine before = new SkippedLine(
-              skip.getStartA(), skip.getStartB(),
-              skip.getSize() - deltaAfter - context);
-          skip.incrementStart(deltaBefore + context);
-          checkAndAddSkip(temp, before);
-          checkAndAddSkip(temp, skip);
-        } else if (deltaAfter > context) {
-          skip.incrementStart(deltaBefore + context);
-          checkAndAddSkip(temp, skip);
-        } else if (deltaBefore > context) {
-          skip.reduceSize(deltaAfter + context);
-          checkAndAddSkip(temp, skip);
-        }
-      }
-      if (temp.isEmpty()) {
-        return temp;
-      }
-      skips = temp;
-    }
-    return skips;
-  }
-
-  private static void checkAndAddSkip(List<SkippedLine> out, SkippedLine s) {
-    if (s.getSize() > 1) {
-      out.add(s);
-    }
-  }
-
-  @Override
-  void clearLine(DisplaySide side, int cmLinePlusOne, CommentGroup group) {
-    SortedMap<Integer, UnifiedCommentGroup> map = map(side);
-    if (map.get(cmLinePlusOne) == group) {
-      map.remove(cmLinePlusOne);
-    }
-  }
-
-  @Override
-  Runnable toggleOpenBox(final CodeMirror cm) {
-    return new Runnable() {
-      @Override
-      public void run() {
-        if (cm.extras().hasActiveLine()) {
-          UnifiedCommentGroup w = map(cm.side()).get(
-              cm.getLineNumber(cm.extras().activeLine()) + 1);
-          if (w != null) {
-            w.openCloseLast();
-          }
-        }
-      }
-    };
-  }
-
-  @Override
-  Runnable openCloseAll(final CodeMirror cm) {
-    return new Runnable() {
-      @Override
-      public void run() {
-        if (cm.extras().hasActiveLine()) {
-          CommentGroup w = map(cm.side()).get(
-              cm.getLineNumber(cm.extras().activeLine()) + 1);
-          if (w != null) {
-            w.openCloseAll();
-          }
-        }
-      }
-    };
-  }
-
-  @Override
-  Runnable newDraftCallback(final CodeMirror cm) {
+  void newDraftOnGutterClick(CodeMirror cm, String gutterClass,
+      int cmLinePlusOne) {
     if (!Gerrit.isSignedIn()) {
-      return new Runnable() {
-        @Override
-        public void run() {
-          String token = host.getToken();
-          if (cm.extras().hasActiveLine()) {
-            LineHandle handle = cm.extras().activeLine();
-            int line = cm.getLineNumber(handle) + 1;
-            token += "@" + line;
-          }
-          Gerrit.doSignIn(token);
-        }
-     };
-    }
-
-    return new Runnable() {
-      @Override
-      public void run() {
-        if (cm.extras().hasActiveLine()) {
-          newDraft(cm);
-        }
+      signInCallback(cm).run();
+    } else {
+      LineRegionInfo info =
+          ((Unified) host).getLineRegionInfoFromCmLine(cmLinePlusOne - 1);
+      DisplaySide side =
+          gutterClass.equals(UnifiedTable.style.lineNumbersLeft())
+              ? DisplaySide.A
+              : DisplaySide.B;
+      int line = info.line;
+      if (info.getSide() != side) {
+        line = host.lineOnOther(info.getSide(), line).getLine();
       }
-    };
+      insertNewDraft(side, line + 1);
+    }
   }
 
-  private void newDraft(CodeMirror cm) {
-    int cmLine = cm.getLineNumber(cm.extras().activeLine());
-    LineSidePair pair = host.getLineSidePairFromCmLine(cmLine);
-    DisplaySide side = pair.getSide();
+  @Override
+  CommentGroup getCommentGroupOnActiveLine(CodeMirror cm) {
+    CommentGroup group = null;
+    if (cm.extras().hasActiveLine()) {
+      int cmLinePlusOne = cm.getLineNumber(cm.extras().activeLine()) + 1;
+      LineRegionInfo info =
+          ((Unified) host).getLineRegionInfoFromCmLine(cmLinePlusOne - 1);
+      CommentGroup forSide = map(info.getSide()).get(cmLinePlusOne);
+      group = forSide == null
+          ? map(info.getSide().otherSide()).get(cmLinePlusOne)
+          : forSide;
+    }
+    return group;
+  }
+
+  @Override
+  Collection<Integer> getLinesWithCommentGroups() {
+    return mergedMap.tailMap(1).keySet();
+  }
+
+  @Override
+  String getTokenSuffixForActiveLine(CodeMirror cm) {
+    int cmLinePlusOne = cm.getLineNumber(cm.extras().activeLine()) + 1;
+    LineRegionInfo info =
+        ((Unified) host).getLineRegionInfoFromCmLine(cmLinePlusOne - 1);
+    return (info.getSide() == DisplaySide.A ? "a" : "") + cmLinePlusOne;
+  }
+
+  @Override
+  void newDraft(CodeMirror cm) {
     if (cm.somethingSelected()) {
-      // TODO: Handle range comment
+      FromTo fromTo = adjustSelection(cm);
+      Pos from = fromTo.from();
+      Pos to = fromTo.to();
+      Unified unified = (Unified) host;
+      UnifiedChunkManager manager = unified.getChunkManager();
+      LineRegionInfo fromInfo =
+          unified.getLineRegionInfoFromCmLine(from.line());
+      LineRegionInfo toInfo =
+          unified.getLineRegionInfoFromCmLine(to.line());
+      DisplaySide side = toInfo.getSide();
+
+      // Handle special cases in selections that span multiple regions. Force
+      // start line to be on the same side as the end line.
+      if ((fromInfo.type == RegionType.INSERT
+          || fromInfo.type == RegionType.COMMON)
+          && toInfo.type == RegionType.DELETE) {
+        LineOnOtherInfo infoOnSideA = manager.lineMapper
+            .lineOnOther(DisplaySide.B, fromInfo.line);
+        int startLineOnSideA = infoOnSideA.getLine();
+        if (infoOnSideA.isAligned()) {
+          from.line(startLineOnSideA);
+        } else {
+          from.line(startLineOnSideA + 1);
+        }
+        from.ch(0);
+        to.line(toInfo.line);
+      } else if (fromInfo.type == RegionType.DELETE
+          && toInfo.type == RegionType.INSERT) {
+        LineOnOtherInfo infoOnSideB = manager.lineMapper
+            .lineOnOther(DisplaySide.A, fromInfo.line);
+        int startLineOnSideB = infoOnSideB.getLine();
+        if (infoOnSideB.isAligned()) {
+          from.line(startLineOnSideB);
+        } else {
+          from.line(startLineOnSideB + 1);
+        }
+        from.ch(0);
+        to.line(toInfo.line);
+      } else if (fromInfo.type == RegionType.DELETE
+          && toInfo.type == RegionType.COMMON) {
+        int toLineOnSideA = manager.lineMapper
+            .lineOnOther(DisplaySide.B, toInfo.line).getLine();
+        from.line(fromInfo.line);
+        // Force the end line to be on the same side as the start line.
+        to.line(toLineOnSideA);
+        side = DisplaySide.A;
+      } else { // Common case
+        from.line(fromInfo.line);
+        to.line(toInfo.line);
+      }
+
+      addDraftBox(side, CommentInfo.create(
+              getPath(),
+              getStoredSideFromDisplaySide(side),
+              to.line() + 1,
+              CommentRange.create(fromTo))).setEdit(true);
+      cm.setCursor(Pos.create(host.getCmLine(to.line(), side), to.ch()));
+      cm.setSelection(cm.getCursor());
     } else {
-      insertNewDraft(side, cmLine + 1);
+      int cmLine = cm.getLineNumber(cm.extras().activeLine());
+      LineRegionInfo info =
+          ((Unified) host).getLineRegionInfoFromCmLine(cmLine);
+      insertNewDraft(info.getSide(), cmLine + 1);
     }
   }
 
-  private UnifiedCommentGroup group(DisplaySide side, int cmLinePlusOne) {
-    UnifiedCommentGroup w = map(side).get(cmLinePlusOne);
-    if (w != null) {
-      return w;
+  @Override
+  CommentGroup group(DisplaySide side, int cmLinePlusOne) {
+    Map<Integer, CommentGroup> map = map(side);
+    CommentGroup existing = map.get(cmLinePlusOne);
+    if (existing != null) {
+      return existing;
     }
 
-    UnifiedCommentGroup g = new UnifiedCommentGroup(this, host.getCm(), side, cmLinePlusOne);
-    if (side == DisplaySide.A) {
-      sideA.put(cmLinePlusOne, g);
-    } else {
-      sideB.put(cmLinePlusOne, g);
+    UnifiedCommentGroup g = new UnifiedCommentGroup(
+        this, host.getCmFromSide(side), side, cmLinePlusOne);
+    map.put(cmLinePlusOne, g);
+    if (mergedMap.containsKey(cmLinePlusOne)) {
+      duplicates.put(cmLinePlusOne, mergedMap.remove(cmLinePlusOne));
     }
+    mergedMap.put(cmLinePlusOne, g);
 
     if (isAttached()) {
-      g.init(getDiffScreen().getDiffTable());
+      g.init(host.getDiffTable());
       g.handleRedraw();
     }
 
     return g;
   }
-
-  private SortedMap<Integer, UnifiedCommentGroup> map(DisplaySide side) {
-    return side == DisplaySide.A ? sideA : sideB;
-  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedDiffChunkInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedDiffChunkInfo.java
index 844be78..542a153 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedDiffChunkInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedDiffChunkInfo.java
@@ -16,15 +16,11 @@
 
 public class UnifiedDiffChunkInfo extends DiffChunkInfo {
 
-  private int cmLine;
+  final int cmLine;
 
-  UnifiedDiffChunkInfo(DisplaySide side,
-      int start, int end, int cmLine, boolean edit) {
-    super(side, start, end, edit);
+  UnifiedDiffChunkInfo(DisplaySide side, int start, int startOnOther, int end,
+      int cmLine, boolean edit) {
+    super(side, start, startOnOther, end, edit);
     this.cmLine = cmLine;
   }
-
-  int getCmLine() {
-    return cmLine;
-  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedSkipBar.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedSkipBar.java
deleted file mode 100644
index 4cbf9b0..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedSkipBar.java
+++ /dev/null
@@ -1,193 +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.client.diff;
-
-import com.google.gerrit.client.patches.PatchUtil;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.dom.client.Style.Unit;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-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.ui.Anchor;
-import com.google.gwt.user.client.ui.HTMLPanel;
-
-import net.codemirror.lib.CodeMirror;
-import net.codemirror.lib.Configuration;
-import net.codemirror.lib.LineWidget;
-import net.codemirror.lib.Pos;
-import net.codemirror.lib.TextMarker;
-import net.codemirror.lib.TextMarker.FromTo;
-
-/** The Widget that handles expanding of skipped lines */
-class UnifiedSkipBar extends SkipBar {
-  interface Binder extends UiBinder<HTMLPanel, UnifiedSkipBar> {}
-  private static final Binder uiBinder = GWT.create(Binder.class);
-  private static final int NUM_ROWS_TO_EXPAND = 10;
-  private static final int UP_DOWN_THRESHOLD = 30;
-
-  interface SkipBarStyle extends CssResource {
-    String noExpand();
-  }
-
-  @UiField(provided=true) Anchor skipNum;
-  @UiField(provided=true) Anchor upArrow;
-  @UiField(provided=true) Anchor downArrow;
-  @UiField SkipBarStyle style;
-
-  private final UnifiedSkipManager manager;
-  private final CodeMirror cm;
-
-  private LineWidget lineWidget;
-  private TextMarker textMarker;
-
-  UnifiedSkipBar(UnifiedSkipManager manager, final CodeMirror cm) {
-    this.manager = manager;
-    this.cm = cm;
-
-    skipNum = new Anchor(true);
-    upArrow = new Anchor(true);
-    downArrow = new Anchor(true);
-    initWidget(uiBinder.createAndBindUi(this));
-    addDomHandler(new ClickHandler() {
-      @Override
-      public void onClick(ClickEvent event) {
-        cm.focus();
-      }
-    }, ClickEvent.getType());
-  }
-
-  void collapse(int start, int end, boolean attach) {
-    if (attach) {
-      boolean isNew = lineWidget == null;
-      Configuration cfg = Configuration.create()
-          .set("coverGutter", true)
-          .set("noHScroll", true);
-      if (start == 0) { // First line workaround
-        lineWidget = cm.addLineWidget(end + 1, getElement(), cfg.set("above", true));
-      } else {
-        lineWidget = cm.addLineWidget(start - 1, getElement(), cfg);
-      }
-      if (isNew) {
-        lineWidget.onFirstRedraw(new Runnable() {
-          @Override
-          public void run() {
-            int w = cm.getGutterElement().getOffsetWidth();
-            getElement().getStyle().setPaddingLeft(w, Unit.PX);
-          }
-        });
-      }
-    }
-
-    textMarker = cm.markText(
-        Pos.create(start, 0),
-        Pos.create(end),
-        Configuration.create()
-          .set("collapsed", true)
-          .set("inclusiveLeft", true)
-          .set("inclusiveRight", true));
-
-    textMarker.on("beforeCursorEnter", new Runnable() {
-      @Override
-      public void run() {
-        expandAll();
-      }
-    });
-
-    int skipped = end - start + 1;
-    if (skipped <= UP_DOWN_THRESHOLD) {
-      addStyleName(style.noExpand());
-    } else {
-      upArrow.setHTML(PatchUtil.M.expandBefore(NUM_ROWS_TO_EXPAND));
-      downArrow.setHTML(PatchUtil.M.expandAfter(NUM_ROWS_TO_EXPAND));
-    }
-    skipNum.setText(PatchUtil.M.patchSkipRegion(Integer
-        .toString(skipped)));
-  }
-
-  private void clearMarkerAndWidget() {
-    textMarker.clear();
-    lineWidget.clear();
-  }
-
-  @Override
-  void expandBefore(int cnt) {
-    expandSideBefore(cnt);
-  }
-
-  private void expandSideBefore(int cnt) {
-    FromTo range = textMarker.find();
-    int oldStart = range.from().line();
-    int newStart = oldStart + cnt;
-    int end = range.to().line();
-    clearMarkerAndWidget();
-    collapse(newStart, end, true);
-    updateSelection();
-  }
-
-  @Override
-  void expandSideAll() {
-    clearMarkerAndWidget();
-    removeFromParent();
-  }
-
-  private void expandAfter() {
-    FromTo range = textMarker.find();
-    int start = range.from().line();
-    int oldEnd = range.to().line();
-    int newEnd = oldEnd - NUM_ROWS_TO_EXPAND;
-    boolean attach = start == 0;
-    if (attach) {
-      clearMarkerAndWidget();
-    } else {
-      textMarker.clear();
-    }
-    collapse(start, newEnd, attach);
-    updateSelection();
-  }
-
-  private void updateSelection() {
-    if (cm.somethingSelected()) {
-      FromTo sel = cm.getSelectedRange();
-      cm.setSelection(sel.from(), sel.to());
-    }
-  }
-
-  @UiHandler("skipNum")
-  void onExpandAll(@SuppressWarnings("unused") ClickEvent e) {
-    expandAll();
-    updateSelection();
-    cm.focus();
-  }
-
-  private void expandAll() {
-    expandSideAll();
-    manager.remove(this);
-  }
-
-  @UiHandler("upArrow")
-  void onExpandBefore(@SuppressWarnings("unused") ClickEvent e) {
-    expandBefore(NUM_ROWS_TO_EXPAND);
-    cm.focus();
-  }
-
-  @UiHandler("downArrow")
-  void onExpandAfter(@SuppressWarnings("unused") ClickEvent e) {
-    expandAfter();
-    cm.focus();
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedSkipBar.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedSkipBar.ui.xml
deleted file mode 100644
index ad05ada..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedSkipBar.ui.xml
+++ /dev/null
@@ -1,52 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
-    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-  <ui:style gss='false' type='com.google.gerrit.client.diff.UnifiedSkipBar.SkipBarStyle'>
-    .skipBar {
-      background-color: #def;
-      height: 1.3em;
-      overflow: hidden;
-    }
-    .text {
-      display: table;
-      margin: 0 auto;
-      color: #777;
-      font-style: italic;
-      overflow: hidden;
-    }
-    .anchor {
-      color: inherit;
-      text-decoration: none;
-    }
-    .noExpand .arrow {
-      display: none;
-    }
-    .arrow {
-      font-family: Arial Unicode MS, sans-serif;
-    }
-  </ui:style>
-  <g:HTMLPanel addStyleNames='{style.skipBar}'>
-  <div class='{style.text}'>
-    <ui:msg>
-      <g:Anchor ui:field='upArrow' addStyleNames='{style.arrow} {style.anchor}' />
-      <g:Anchor ui:field='skipNum' addStyleNames='{style.anchor}' />
-      <g:Anchor ui:field='downArrow' addStyleNames=' {style.arrow} {style.anchor}' />
-    </ui:msg>
-  </div>
-  </g:HTMLPanel>
-</ui:UiBinder>
\ No newline at end of file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedSkipManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedSkipManager.java
deleted file mode 100644
index 7554a87..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedSkipManager.java
+++ /dev/null
@@ -1,78 +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.client.diff;
-
-import com.google.gerrit.client.patches.SkippedLine;
-
-import net.codemirror.lib.CodeMirror;
-
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-/** Collapses common regions with {@link UnifiedSkipBar} for {@link Unified}. */
-class UnifiedSkipManager extends SkipManager {
-  private Unified host;
-
-  UnifiedSkipManager(Unified host, UnifiedCommentManager commentManager) {
-    super(commentManager);
-    this.host = host;
-  }
-
-  @Override
-  void render(int context, DiffInfo diff) {
-    List<SkippedLine> skips = getSkippedLines(context, diff);
-
-    if (!skips.isEmpty()) {
-      CodeMirror cm = host.getCm();
-
-      Set<SkipBar> skipBars = new HashSet<>();
-      setSkipBars(skipBars);
-      for (SkippedLine skip : skips) {
-        UnifiedSkipBar bar = newSkipBar(cm, skip);
-        skipBars.add(bar);
-
-        if (skip.getStartA() == 0 || skip.getStartB() == 0) {
-          bar.upArrow.setVisible(false);
-          setLine0(bar);
-        } else if (skip.getStartA() + skip.getSize() == getLineA()
-            || skip.getStartB() + skip.getSize() == getLineB()) {
-          bar.downArrow.setVisible(false);
-        }
-      }
-    }
-  }
-
-  void remove(UnifiedSkipBar bar) {
-    Set<SkipBar> skipBars = getSkipBars();
-    skipBars.remove(bar);
-    if (getLine0() == bar) {
-      setLine0(null);
-    }
-    if (skipBars.isEmpty()) {
-      setSkipBars(null);
-    }
-  }
-
-  private UnifiedSkipBar newSkipBar(CodeMirror cm, SkippedLine skip) {
-    int start = host.getCmLine(skip.getStartA(), DisplaySide.A);
-    int end = start + skip.getSize() - 1;
-
-    UnifiedSkipBar bar = new UnifiedSkipBar(this, cm);
-    host.getDiffTable().add(bar);
-    bar.collapse(start, end, true);
-    return bar;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedTable.java
index c936c7a..72b3e49 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedTable.java
@@ -36,6 +36,7 @@
     String diffInsert();
     String diffDelete();
     String unifiedLineNumber();
+    String unifiedLineNumberEmpty();
     String lineNumbersLeft();
     String lineNumbersRight();
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedTable.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedTable.ui.xml
index ab520be..973fc8f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedTable.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedTable.ui.xml
@@ -24,7 +24,7 @@
     @external .CodeMirror-dialog-bottom;
     @external .CodeMirror-cursor;
 
-    @external .dark, .unifiedLineNumber, .noIntraline;
+    @external .dark, .unifiedLineNumber, .noIntraline, .showLineNumbers;
 
     .difftable .patchSetNav,
     .difftable .CodeMirror {
@@ -94,21 +94,33 @@
       bottom: auto;
       left: auto;
     }
-
-    .lineNumbersLeft, .lineNumbersRight {
+    .showLineNumbers .lineNumbersLeft, .showLineNumbers .lineNumbersRight {
       min-width: 20px;
       width: 3em; /* TODO: This needs to be set based on number of lines */
     }
-    .lineNumbersLeft {
+    .showLineNumbers .lineNumbersLeft {
       border-right: 1px solid #ddd;
     }
     .unifiedLineNumber {
+      display: none;
+    }
+    .showLineNumbers .unifiedLineNumber {
+      display: block;
       cursor: pointer;
       padding: 0 3px 0 5px;
       min-width: 20px;
       text-align: right;
       color: #999;
     }
+    .unifiedLineNumberEmpty {
+      display: none;
+    }
+    .showLineNumbers .unifiedLineNumberEmpty {
+      display: block;
+      margin-left: 28px;
+      border-left: 2px solid #d64040;
+      padding-bottom: 1px;
+    }
   </ui:style>
   <g:HTMLPanel styleName='{style.difftable}'>
     <table class='{style.table}'>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/documentation/DocTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/documentation/DocTable.java
index b57cdac..0c5be8e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/documentation/DocTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/documentation/DocTable.java
@@ -28,10 +28,10 @@
 class DocTable extends NavigationTable<DocInfo> {
   private static final int C_TITLE = 1;
 
-  private int rows = 0;
-  private int dataBeginRow = 0;
+  private int rows;
+  private int dataBeginRow;
 
-  public DocTable() {
+  DocTable() {
     super(Util.C.docItemHelp());
 
     table.setText(0, C_TITLE, Util.C.docTableColumnTitle());
@@ -117,7 +117,7 @@
   }
 
   public static class DocLink extends Anchor {
-    public DocLink(DocInfo d) {
+    DocLink(DocInfo d) {
       super(com.google.gerrit.client.changes.Util.cropSubject(d.title()));
       setHref(d.getFullUrl());
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditConstants.java
index e95fdfc..8dc4137 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditConstants.java
@@ -18,7 +18,7 @@
 import com.google.gwt.i18n.client.Constants;
 
 interface EditConstants extends Constants {
-  static final EditConstants I = GWT.create(EditConstants.class);
+  EditConstants I = GWT.create(EditConstants.class);
 
   String closeUnsavedChanges();
   String cancelUnsavedChanges();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesBox.java
index edfb15e..0a27ea1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesBox.java
@@ -60,6 +60,7 @@
   @UiField Anchor close;
   @UiField NpIntTextBox tabWidth;
   @UiField NpIntTextBox lineLength;
+  @UiField NpIntTextBox indentUnit;
   @UiField NpIntTextBox cursorBlinkRate;
   @UiField ToggleButton topMenu;
   @UiField ToggleButton syntaxHighlighting;
@@ -68,6 +69,7 @@
   @UiField ToggleButton lineNumbers;
   @UiField ToggleButton matchBrackets;
   @UiField ToggleButton autoCloseBrackets;
+  @UiField ToggleButton showBase;
   @UiField ListBox theme;
   @UiField ListBox keyMap;
   @UiField Button apply;
@@ -94,6 +96,7 @@
 
     tabWidth.setIntValue(prefs.tabSize());
     lineLength.setIntValue(prefs.lineLength());
+    indentUnit.setIntValue(prefs.indentUnit());
     cursorBlinkRate.setIntValue(prefs.cursorBlinkRate());
     topMenu.setValue(!prefs.hideTopMenu());
     syntaxHighlighting.setValue(prefs.syntaxHighlighting());
@@ -102,6 +105,7 @@
     lineNumbers.setValue(prefs.hideLineNumbers());
     matchBrackets.setValue(prefs.matchBrackets());
     autoCloseBrackets.setValue(prefs.autoCloseBrackets());
+    showBase.setValue(prefs.showBase());
     setTheme(prefs.theme());
     setKeyMapType(prefs.keyMapType());
   }
@@ -112,7 +116,7 @@
     if (v != null && v.length() > 0) {
       prefs.tabSize(Math.max(1, Integer.parseInt(v)));
       if (view != null) {
-        view.getEditor().setOption("tabSize", v);
+        view.setOption("tabSize", v);
       }
     }
   }
@@ -128,6 +132,17 @@
     }
   }
 
+  @UiHandler("indentUnit")
+  void onIndentUnit(ValueChangeEvent<String> e) {
+    String v = e.getValue();
+    if (v != null && v.length() > 0) {
+      prefs.indentUnit(Math.max(0, Integer.parseInt(v)));
+      if (view != null) {
+        view.setIndentUnit(prefs.indentUnit());
+      }
+    }
+  }
+
   @UiHandler("cursorBlinkRate")
   void onCursoBlinkRate(ValueChangeEvent<String> e) {
     String v = e.getValue();
@@ -136,7 +151,7 @@
       // don't let user shoot himself in the foot.
       prefs.cursorBlinkRate(Math.max(0, Integer.parseInt(v)));
       if (view != null) {
-        view.getEditor().setOption("cursorBlinkRate", prefs.cursorBlinkRate());
+        view.setOption("cursorBlinkRate", prefs.cursorBlinkRate());
       }
     }
   }
@@ -146,7 +161,7 @@
     prefs.hideTopMenu(!e.getValue());
     if (view != null) {
       Gerrit.setHeaderVisible(!prefs.hideTopMenu());
-      view.resizeCodeMirror();
+      view.adjustHeight();
     }
   }
 
@@ -186,7 +201,7 @@
   void onMatchBrackets(ValueChangeEvent<Boolean> e) {
     prefs.matchBrackets(e.getValue());
     if (view != null) {
-      view.getEditor().setOption("matchBrackets", prefs.matchBrackets());
+      view.setOption("matchBrackets", prefs.matchBrackets());
     }
   }
 
@@ -198,6 +213,15 @@
     }
   }
 
+  @UiHandler("showBase")
+  void onShowBase(ValueChangeEvent<Boolean> e) {
+    Boolean value = e.getValue();
+    prefs.showBase(value);
+    if (view != null) {
+      view.showBase.setValue(value, true);
+    }
+  }
+
   @UiHandler("theme")
   void onTheme(@SuppressWarnings("unused") ChangeEvent e) {
     final Theme newTheme = Theme.valueOf(theme.getValue(theme.getSelectedIndex()));
@@ -206,13 +230,7 @@
       ThemeLoader.loadTheme(newTheme, new GerritCallback<Void>() {
         @Override
         public void onSuccess(Void result) {
-          view.getEditor().operation(new Runnable() {
-            @Override
-            public void run() {
-              String t = newTheme.name().toLowerCase();
-              view.getEditor().setOption("theme", t);
-            }
-          });
+          view.setTheme(newTheme);
         }
       });
     }
@@ -224,7 +242,7 @@
         keyMap.getValue(keyMap.getSelectedIndex()));
     prefs.keyMapType(keyMapType);
     if (view != null) {
-      view.getEditor().setOption("keyMap", keyMapType.name().toLowerCase());
+      view.setOption("keyMap", keyMapType.name().toLowerCase());
     }
   }
 
@@ -267,27 +285,9 @@
   }
 
   private void initTheme() {
-    theme.addItem(
-        Theme.DEFAULT.name().toLowerCase(),
-        Theme.DEFAULT.name());
-    theme.addItem(
-        Theme.ECLIPSE.name().toLowerCase(),
-        Theme.ECLIPSE.name());
-    theme.addItem(
-        Theme.ELEGANT.name().toLowerCase(),
-        Theme.ELEGANT.name());
-    theme.addItem(
-        Theme.NEAT.name().toLowerCase(),
-        Theme.NEAT.name());
-    theme.addItem(
-        Theme.MIDNIGHT.name().toLowerCase(),
-        Theme.MIDNIGHT.name());
-    theme.addItem(
-        Theme.NIGHT.name().toLowerCase(),
-        Theme.NIGHT.name());
-    theme.addItem(
-        Theme.TWILIGHT.name().toLowerCase(),
-        Theme.TWILIGHT.name());
+    for (Theme t : Theme.values()) {
+      theme.addItem(t.name().toLowerCase(), t.name());
+    }
   }
 
   private void setKeyMapType(KeyMapType v) {
@@ -302,14 +302,8 @@
   }
 
   private void initKeyMapType() {
-    keyMap.addItem(
-        KeyMapType.DEFAULT.name().toLowerCase(),
-        KeyMapType.DEFAULT.name());
-    keyMap.addItem(
-        KeyMapType.EMACS.name().toLowerCase(),
-        KeyMapType.EMACS.name());
-    keyMap.addItem(
-        KeyMapType.VIM.name().toLowerCase(),
-        KeyMapType.VIM.name());
+    for (KeyMapType t : KeyMapType.values()) {
+      keyMap.addItem(t.name().toLowerCase(), t.name());
+    }
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesBox.ui.xml
index ec8ad39..b5b0b01 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesBox.ui.xml
@@ -184,6 +184,12 @@
             alignment='RIGHT'/></td>
       </tr>
       <tr>
+        <th><ui:msg>Indent Unit</ui:msg></th>
+        <td><x:NpIntTextBox ui:field='indentUnit'
+            visibleLength='4'
+            alignment='RIGHT'/></td>
+      </tr>
+      <tr>
         <th><ui:msg>Cursor Blink Rate</ui:msg></th>
         <td><x:NpIntTextBox ui:field='cursorBlinkRate'
             visibleLength='4'
@@ -239,6 +245,13 @@
         </g:ToggleButton></td>
       </tr>
       <tr>
+        <th><ui:msg>Show Base Version</ui:msg></th>
+        <td><g:ToggleButton ui:field='showBase'>
+          <g:upFace><ui:msg>Hide</ui:msg></g:upFace>
+          <g:downFace><ui:msg>Show</ui:msg></g:downFace>
+        </g:ToggleButton></td>
+      </tr>
+      <tr>
         <td></td>
         <td>
           <g:Button ui:field='apply' styleName='{style.apply}'>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
index 5799433..b7adf3b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
@@ -43,6 +43,7 @@
 import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.extensions.client.KeyMapType;
+import com.google.gerrit.extensions.client.Theme;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.core.client.GWT;
@@ -50,11 +51,14 @@
 import com.google.gwt.core.client.Scheduler;
 import com.google.gwt.core.client.Scheduler.ScheduledCommand;
 import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.Style.Unit;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.KeyPressEvent;
 import com.google.gwt.event.logical.shared.ResizeEvent;
 import com.google.gwt.event.logical.shared.ResizeHandler;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
 import com.google.gwt.event.shared.HandlerRegistration;
+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;
@@ -63,17 +67,21 @@
 import com.google.gwt.user.client.Window.ClosingHandler;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.CheckBox;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.HTMLPanel;
 import com.google.gwt.user.client.ui.ImageResourceRenderer;
 import com.google.gwtexpui.globalkey.client.GlobalKey;
 import com.google.gwtexpui.safehtml.client.SafeHtml;
 
+import net.codemirror.addon.AddonInjector;
+import net.codemirror.addon.Addons;
 import net.codemirror.lib.CodeMirror;
 import net.codemirror.lib.CodeMirror.ChangesHandler;
 import net.codemirror.lib.CodeMirror.CommandRunner;
 import net.codemirror.lib.Configuration;
 import net.codemirror.lib.KeyMap;
+import net.codemirror.lib.MergeView;
 import net.codemirror.lib.Pos;
 import net.codemirror.mode.ModeInfo;
 import net.codemirror.mode.ModeInjector;
@@ -85,14 +93,23 @@
   interface Binder extends UiBinder<HTMLPanel, EditScreen> {}
   private static final Binder uiBinder = GWT.create(Binder.class);
 
+  interface Style extends CssResource {
+    String fullWidth();
+    String base();
+    String hideBase();
+  }
+
   private final PatchSet.Id base;
   private final PatchSet.Id revision;
   private final String path;
   private final int startLine;
   private EditPreferences prefs;
   private EditPreferencesAction editPrefsAction;
-  private CodeMirror cm;
+  private MergeView mv;
+  private CodeMirror cmBase;
+  private CodeMirror cmEdit;
   private HttpResponse<NativeString> content;
+  private HttpResponse<NativeString> baseContent;
   private EditFileInfo editFileInfo;
   private JsArray<DiffWebLinkInfo> diffLinks;
 
@@ -103,9 +120,11 @@
   @UiField Element cursLine;
   @UiField Element cursCol;
   @UiField Element dirty;
+  @UiField CheckBox showBase;
   @UiField Button close;
   @UiField Button save;
   @UiField Element editor;
+  @UiField Style style;
 
   private HandlerRegistration resizeHandler;
   private HandlerRegistration closeHandler;
@@ -145,9 +164,21 @@
       public void onSuccess(Void result) {
         // Load theme after CM library to ensure theme can override CSS.
         ThemeLoader.loadTheme(prefs.theme(), themeCallback);
-
         group2.done();
-        group3.done();
+
+        new AddonInjector().add(Addons.I.merge_bundled().getName()).inject(
+            new AsyncCallback<Void>() {
+          @Override
+          public void onFailure(Throwable caught) {
+          }
+
+          @Override
+          public void onSuccess(Void result) {
+            if (!prefs.showBase() || revision.get() > 0) {
+              group3.done();
+            }
+          }
+        });
       }
 
       @Override
@@ -180,13 +211,30 @@
             public void onFailure(Throwable e) {
             }
           }));
+
+      if (prefs.showBase()) {
+        ChangeEditApi.get(revision, path, true /* base */,
+            group1.addFinal(new HttpCallback<NativeString>() {
+              @Override
+              public void onSuccess(HttpResponse<NativeString> fc) {
+                baseContent = fc;
+                group3.done();
+              }
+
+              @Override
+              public void onFailure(Throwable e) {
+              }
+            }));
+      } else {
+        group1.done();
+      }
     } else {
       // TODO(davido): We probably want to create dedicated GET EditScreenMeta
       // REST endpoint. Abuse GET diff for now, as it retrieves links we need.
       DiffApi.diff(revision, path)
         .base(base)
         .webLinksOnly()
-        .get(group1.add(new AsyncCallback<DiffInfo>() {
+        .get(group1.addFinal(new AsyncCallback<DiffInfo>() {
           @Override
           public void onSuccess(DiffInfo diffInfo) {
             diffLinks = diffInfo.webLinks();
@@ -205,6 +253,10 @@
           @Override
           public void onSuccess(HttpResponse<NativeString> fc) {
             content = fc;
+            if (revision.get() > 0) {
+              baseContent = fc;
+            }
+
             if (prefs.syntaxHighlighting()) {
               injectMode(fc.getContentType(), modeCallback);
             } else {
@@ -227,14 +279,16 @@
     group3.addListener(new ScreenLoadCallback<Void>(this) {
       @Override
       protected void preDisplay(Void result) {
-        initEditor(content);
+        initEditor();
 
         renderLinks(editFileInfo, diffLinks);
         editFileInfo = null;
         diffLinks = null;
+
+        showBase.setValue(prefs.showBase(), true);
+        cmBase.refresh();
       }
     });
-    group1.done();
   }
 
   @Override
@@ -251,27 +305,15 @@
       localKeyMap.on("Ctrl-S", save());
     }
 
-    cm.addKeyMap(localKeyMap);
+    cmBase.addKeyMap(localKeyMap);
+    cmEdit.addKeyMap(localKeyMap);
   }
 
   private Runnable gotoLine() {
     return new Runnable() {
       @Override
       public void run() {
-        String n = Window.prompt(EditConstants.I.gotoLineNumber(), "");
-        if (n != null) {
-          try {
-            int line = Integer.parseInt(n);
-            line--;
-            if (line >= 0) {
-              cm.scrollToLine(line);
-            }
-          } catch (NumberFormatException e) {
-            // ignore non valid numbers
-            // We don't want to popup another ugly dialog just to say
-            // "The number you've provided is invalid, try again"
-          }
-        }
+        cmEdit.execCommand("jumpToLine");
       }
     };
   }
@@ -287,36 +329,36 @@
     resizeHandler = Window.addResizeHandler(new ResizeHandler() {
       @Override
       public void onResize(ResizeEvent event) {
-        cm.adjustHeight(header.getOffsetHeight());
+        adjustHeight();
       }
     });
     closeHandler = Window.addWindowClosingHandler(new ClosingHandler() {
       @Override
       public void onWindowClosing(ClosingEvent event) {
-        if (!cm.isClean(generation)) {
+        if (!cmEdit.isClean(generation)) {
           event.setMessage(EditConstants.I.closeUnsavedChanges());
         }
       }
     });
 
-    generation = cm.changeGeneration(true);
+    generation = cmEdit.changeGeneration(true);
     setClean(true);
-    cm.on(new ChangesHandler() {
+    cmEdit.on(new ChangesHandler() {
       @Override
       public void handle(CodeMirror cm) {
         setClean(cm.isClean(generation));
       }
     });
 
-    cm.adjustHeight(header.getOffsetHeight());
-    cm.on("cursorActivity", updateCursorPosition());
+    adjustHeight();
+    cmEdit.on("cursorActivity", updateCursorPosition());
     setShowTabs(prefs.showTabs());
     setLineLength(prefs.lineLength());
-    cm.refresh();
-    cm.focus();
+    cmEdit.refresh();
+    cmEdit.focus();
 
     if (startLine > 0) {
-      cm.scrollToLine(startLine);
+      cmEdit.scrollToLine(startLine);
     }
     updateActiveLine();
     editPrefsAction = new EditPreferencesAction(this, prefs);
@@ -325,8 +367,11 @@
   @Override
   protected void onUnload() {
     super.onUnload();
-    if (cm != null) {
-      cm.getWrapperElement().removeFromParent();
+    if (cmBase != null) {
+      cmBase.getWrapperElement().removeFromParent();
+    }
+    if (cmEdit != null) {
+      cmEdit.getWrapperElement().removeFromParent();
     }
     if (resizeHandler != null) {
       resizeHandler.removeHandler();
@@ -340,7 +385,7 @@
   }
 
   CodeMirror getEditor() {
-    return cm;
+    return cmEdit;
   }
 
   @UiHandler("editSettings")
@@ -355,46 +400,138 @@
 
   @UiHandler("close")
   void onClose(@SuppressWarnings("unused") ClickEvent e) {
-    if (cm.isClean(generation)
+    if (cmEdit.isClean(generation)
         || Window.confirm(EditConstants.I.cancelUnsavedChanges())) {
       upToChange();
     }
   }
 
+  private void displayBase() {
+    cmBase.getWrapperElement().getParentElement()
+        .removeClassName(style.hideBase());
+    cmEdit.getWrapperElement().getParentElement()
+        .removeClassName(style.fullWidth());
+    mv.getGapElement().removeClassName(style.hideBase());
+    setCmBaseValue();
+    setLineLength(prefs.lineLength());
+    cmBase.refresh();
+  }
+
+  @UiHandler("showBase")
+  void onShowBase(ValueChangeEvent<Boolean> e) {
+    boolean shouldShow = e.getValue();
+    if (shouldShow) {
+      if (baseContent == null) {
+        ChangeEditApi.get(revision, path, true /* base */,
+            new HttpCallback<NativeString>() {
+              @Override
+              public void onSuccess(HttpResponse<NativeString> fc) {
+                baseContent = fc;
+                displayBase();
+              }
+
+              @Override
+              public void onFailure(Throwable e) {
+              }
+            });
+      } else {
+        displayBase();
+      }
+    } else {
+      cmBase.getWrapperElement().getParentElement()
+          .addClassName(style.hideBase());
+      cmEdit.getWrapperElement().getParentElement()
+          .addClassName(style.fullWidth());
+      mv.getGapElement().addClassName(style.hideBase());
+    }
+    mv.setShowDifferences(shouldShow);
+  }
+
+  void setOption(String option, String value) {
+    cmBase.setOption(option, value);
+    cmEdit.setOption(option, value);
+  }
+
+  void setOption(String option, boolean value) {
+    cmBase.setOption(option, value);
+    cmEdit.setOption(option, value);
+  }
+
+  void setOption(String option, double value) {
+    cmBase.setOption(option, value);
+    cmEdit.setOption(option, value);
+  }
+
+  void setTheme(final Theme newTheme) {
+    cmBase.operation(new Runnable() {
+      @Override
+      public void run() {
+        cmBase.setOption("theme", newTheme.name().toLowerCase());
+      }
+    });
+    cmEdit.operation(new Runnable() {
+      @Override
+      public void run() {
+        cmEdit.setOption("theme", newTheme.name().toLowerCase());
+      }
+    });
+  }
+
   void setLineLength(int length) {
-    cm.extras().lineLength(
-        Patch.COMMIT_MSG.equals(path) ? 72 : length);
+    int adjustedLength = Patch.COMMIT_MSG.equals(path) ? 72 : length;
+    cmBase.extras().lineLength(adjustedLength);
+    cmEdit.extras().lineLength(adjustedLength);
+  }
+
+  void setIndentUnit(int indent) {
+    cmEdit.setOption("indentUnit", Patch.COMMIT_MSG.equals(path) ? 2 : indent);
   }
 
   void setShowLineNumbers(boolean show) {
-    cm.setOption("lineNumbers", show);
+    cmBase.setOption("lineNumbers", show);
+    cmEdit.setOption("lineNumbers", show);
   }
 
   void setShowWhitespaceErrors(final boolean show) {
-    cm.operation(new Runnable() {
+    cmBase.operation(new Runnable() {
       @Override
       public void run() {
-        cm.setOption("showTrailingSpace", show);
+        cmBase.setOption("showTrailingSpace", show);
+      }
+    });
+    cmEdit.operation(new Runnable() {
+      @Override
+      public void run() {
+        cmEdit.setOption("showTrailingSpace", show);
       }
     });
   }
 
   void setShowTabs(boolean show) {
-    cm.extras().showTabs(show);
+    cmBase.extras().showTabs(show);
+    cmEdit.extras().showTabs(show);
   }
 
-  void resizeCodeMirror() {
-    cm.adjustHeight(header.getOffsetHeight());
+  void adjustHeight() {
+    int height = header.getOffsetHeight();
+    int rest = Gerrit.getHeaderFooterHeight()
+        + height
+        + 5; // Estimate
+    mv.getGapElement().getStyle().setHeight(
+        Window.getClientHeight() - rest, Unit.PX);
+    cmBase.adjustHeight(height);
+    cmEdit.adjustHeight(height);
   }
 
   void setSyntaxHighlighting(boolean b) {
     ModeInfo modeInfo = ModeInfo.findMode(content.getContentType(), path);
-    final String mode = modeInfo != null ? modeInfo.mode() : null;
+    final String mode = modeInfo != null ? modeInfo.mime() : null;
     if (b && mode != null && !mode.isEmpty()) {
       injectMode(mode, new AsyncCallback<Void>() {
         @Override
         public void onSuccess(Void result) {
-          cm.setOption("mode", mode);
+          cmBase.setOption("mode", mode);
+          cmEdit.setOption("mode", mode);
         }
 
         @Override
@@ -403,7 +540,8 @@
         }
       });
     } else {
-      cm.setOption("mode", (String) null);
+      cmBase.setOption("mode", (String) null);
+      cmEdit.setOption("mode", (String) null);
     }
   }
 
@@ -411,31 +549,38 @@
     Gerrit.display(PageLinks.toChangeInEditMode(revision.getParentKey()));
   }
 
-  private void initEditor(HttpResponse<NativeString> file) {
+  private void initEditor() {
     ModeInfo mode = null;
-    String content = "";
-    if (file != null && file.getResult() != null) {
-      content = file.getResult().asString();
+    String editContent = "";
+    if (content != null && content.getResult() != null) {
+      editContent = content.getResult().asString();
       if (prefs.syntaxHighlighting()) {
-        mode = ModeInfo.findMode(file.getContentType(), path);
+        mode = ModeInfo.findMode(content.getContentType(), path);
       }
     }
-    cm = CodeMirror.create(editor, Configuration.create()
-        .set("value", content)
-        .set("readOnly", false)
+    mv = MergeView.create(editor, Configuration.create()
+        .set("autoCloseBrackets", prefs.autoCloseBrackets())
         .set("cursorBlinkRate", prefs.cursorBlinkRate())
         .set("cursorHeight", 0.85)
+        .set("indentUnit", prefs.indentUnit())
+        .set("keyMap", prefs.keyMapType().name().toLowerCase())
         .set("lineNumbers", prefs.hideLineNumbers())
-        .set("tabSize", prefs.tabSize())
         .set("lineWrapping", false)
         .set("matchBrackets", prefs.matchBrackets())
-        .set("autoCloseBrackets", prefs.autoCloseBrackets())
+        .set("mode", mode != null ? mode.mime() : null)
+        .set("origLeft", editContent)
         .set("scrollbarStyle", "overlay")
-        .set("styleSelectedText", true)
         .set("showTrailingSpace", prefs.showWhitespaceErrors())
-        .set("keyMap", prefs.keyMapType().name().toLowerCase())
+        .set("styleSelectedText", true)
+        .set("tabSize", prefs.tabSize())
         .set("theme", prefs.theme().name().toLowerCase())
-        .set("mode", mode != null ? mode.mode() : null));
+        .set("value", ""));
+
+    cmBase = mv.leftOriginal();
+    cmBase.getWrapperElement().addClassName(style.base());
+    cmEdit = mv.editor();
+    setCmBaseValue();
+    cmEdit.setValue(editContent);
 
     CodeMirror.addCommand("save", new CommandRunner() {
       @Override
@@ -494,7 +639,7 @@
         Scheduler.get().scheduleDeferred(new ScheduledCommand() {
           @Override
           public void execute() {
-            cm.operation(new Runnable() {
+            cmEdit.operation(new Runnable() {
               @Override
               public void run() {
                 updateActiveLine();
@@ -507,10 +652,10 @@
   }
 
   private void updateActiveLine() {
-    Pos p = cm.getCursor("end");
+    Pos p = cmEdit.getCursor("end");
     cursLine.setInnerText(Integer.toString(p.line() + 1));
     cursCol.setInnerText(Integer.toString(p.ch() + 1));
-    cm.extras().activeLine(cm.getLineHandleVisualStart(p.line()));
+    cmEdit.extras().activeLine(cmEdit.getLineHandleVisualStart(p.line()));
   }
 
   private void setClean(boolean clean) {
@@ -523,23 +668,23 @@
     return new Runnable() {
       @Override
       public void run() {
-        if (!cm.isClean(generation)) {
+        if (!cmEdit.isClean(generation)) {
           close.setEnabled(false);
-          String text = cm.getValue();
+          String text = cmEdit.getValue();
           if (Patch.COMMIT_MSG.equals(path)) {
             String trimmed = text.trim() + "\r";
             if (!trimmed.equals(text)) {
               text = trimmed;
-              cm.setValue(text);
+              cmEdit.setValue(text);
             }
           }
-          final int g = cm.changeGeneration(false);
+          final int g = cmEdit.changeGeneration(false);
           ChangeEditApi.put(revision.getParentKey().get(), path, text,
               new GerritCallback<VoidResult>() {
                 @Override
                 public void onSuccess(VoidResult result) {
                   generation = g;
-                  setClean(cm.isClean(g));
+                  setClean(cmEdit.isClean(g));
                 }
                 @Override
                 public void onFailure(final Throwable caught) {
@@ -554,4 +699,10 @@
   private void injectMode(String type, AsyncCallback<Void> cb) {
     new ModeInjector().add(type).inject(cb);
   }
+
+  private void setCmBaseValue() {
+    cmBase.setValue(baseContent != null && baseContent.getResult() != null
+        ? baseContent.getResult().asString()
+        : "");
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.ui.xml
index 88af398..34282c8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.ui.xml
@@ -17,8 +17,11 @@
 <ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
   <ui:with field='ico' type='com.google.gerrit.client.GerritResources'/>
-  <ui:style gss='false'>
+  <ui:style gss='false' type='com.google.gerrit.client.editor.EditScreen.Style'>
     @external .CodeMirror, .CodeMirror-cursor;
+    @external .CodeMirror-merge-2pane, .CodeMirror-merge-pane;
+    @external .CodeMirror-merge-gap;
+    @external .CodeMirror-scroll, .CodeMirror-overlayscroll-vertical;
 
     .header {
       position: relative;
@@ -123,12 +126,28 @@
       cursor: pointer;
       outline: none;
     }
+
+    .hideBase.CodeMirror-merge-pane {
+      display: none;
+    }
+
+    .hideBase.CodeMirror-merge-gap {
+      display: none;
+    }
+
+    .CodeMirror-merge-2pane .fullWidth.CodeMirror-merge-pane {
+      width: 100%;
+    }
+
+    /* Hide the vertical scrollbar on the base side. The edit side controls
+       both views */
+    .base .CodeMirror-scroll { margin-right: -42px; }
+    .base .CodeMirror-overlayscroll-vertical { display: none !important; }
   </ui:style>
   <g:HTMLPanel styleName='{style.header}'>
     <div class='{style.headerLine}' ui:field='header'>
        <div class='{style.headerButtons}'>
          <g:Button ui:field='close'
-             styleName=''
              title='Close file and return to change'>
            <ui:attribute name='title'/>
            <div><ui:msg>Close</ui:msg></div>
@@ -142,6 +161,11 @@
        </div>
        <span class='{style.path}'><span ui:field='project'/> / <span ui:field='filePath'/></span>
        <div class='{style.navigation}'>
+         <g:Label text='Show Base' styleName='{style.linkPanel}'></g:Label>
+         <g:CheckBox ui:field='showBase' checked='true' styleName='{style.linkPanel}'
+             title='Show Base Version'>
+           <ui:attribute name='title'/>
+         </g:CheckBox>
          <g:FlowPanel ui:field='linkPanel' styleName='{style.linkPanel}'/>
          <g:Image
              ui:field='editSettings'
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 6d4162e..8653a95 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
@@ -148,57 +148,6 @@
   background: selectionColor;
 }
 
-/** CommentPanel **/
-.commentPanelHeader {
-  cursor: pointer;
-  width: 100%;
-}
-.commentPanelSummary {
-  color: #777777;
-  white-space: nowrap;
-  overflow: hidden;
-}
-.commentPanelAuthorCell {
-  font-weight: bold;
-  white-space: nowrap;
-}
-.commentPanelSummaryCell {
-  width: 100%;
-}
-.commentPanelDateCell {
-  white-space: nowrap;
-}
-.commentPanelContent {
-  padding-bottom: 1px;
-}
-.commentPanelMessage {
-  font-size: small;
-  padding-left: 0.5em;
-  padding-right: 0.5em;
-}
-.commentPanelMessage p {
-  margin-top: 0px;
-  margin-bottom: 0px;
-  padding-top: 0.5em;
-  padding-bottom: 0.5em;
-  max-height: 100000px;
-}
-.commentPanelButtons {
-  margin-left: 0.5em;
-}
-.commentPanelButtons .gwt-Button {
-  margin-right: 2em;
-  font-size: 7pt;
-  padding: 1px;
-}
-
-.commentEditorPanel textarea {
-  margin-left: 0.5em;
-  font-size: small;
-  font-family: norm-font;
-}
-
-
 /** Menu **/
 .linkMenuBar {
   font-size: 9pt;
@@ -442,57 +391,10 @@
   border-bottom: 1px solid trimColor;
 }
 
-.changeTable .changeTypeCell {
-  width: 1px;
-  padding-left: 5px;
-  padding-right: 5px;
-  border-right: 1px solid trimColor;
-  border-bottom: 1px solid trimColor;
-  vertical-align: top;
-}
-
-.changeTable .commentCell {
-  text-align: right;
-  font-weight: bold;
-  white-space: nowrap;
-}
-.changeTable .commentCell span.drafts {
-  color: #ff5555;
-}
-
-.changeTable .patchCellReverseDiff {
-  color: red;
-}
-
-.changeTable .patchSizeCell {
-  text-align: right;
-  white-space: nowrap;
-}
-.changeTable td.noborder {
-  border: none;
-}
-
-.changeTable .filePathCell {
-  white-space: nowrap;
-}
-
-.changeTable .sourceFilePath {
-  font-style: italic;
-  font-size: 9pt;
-}
-
-.changeTable .diffLinkCell {
-  white-space: nowrap;
-}
-
 .changeTable .leftMostCell {
   border-left: 1px solid trimColor;
 }
 
-.changeTable .topMostCell {
-  border-top: 1px solid trimColor;
-}
-
 .changeTable .dataCell {
   padding-left: 5px;
   padding-right: 5px;
@@ -627,205 +529,7 @@
   color: #2a5db0;
 }
 
-
-/** PatchScreen **/
-.reviewedPanelBottom {
-  float: right;
-  font-size: small;
-}
-
-.linkPanel img {
-  padding-right: 3px;
-}
-
-.nowrap {
-  white-space: nowrap;
-}
-
-
-/** PatchContentTable **/
-.patchContentTable {
-  width: 100%;
-  border-collapse: separate;
-  border-spacing: 0;
-  background: white;
-  color: black;
-}
-.patchContentTable td {
-  padding-top: 0;
-  padding-bottom: 0;
-  font-size: 9pt;
-  font-family: mono-font;
-}
-
-.patchContentTable .iconCell {
-  width: 1px;
-  padding: 0px;
-  vertical-align: middle;
-}
-
-.patchContentTable .diffText {
-  white-space: pre;
-  padding-left: 0.2em;
-  border-left: thin solid #b0bdcc;
-}
-
-.patchContentTable .diffTextFileHeader {
-  color: grey;
-  font-weight: bold;
-}
-.patchContentTable .diffTextNoLF {
-  color: grey;
-  font-weight: bold;
-}
-.patchContentTable .diffTextHunkHeader {
-  color: blue;
-}
-.patchContentTable .diffTextDELETE {
-  color: #a00000;
-}
-.patchContentTable .diffTextCONTEXT {
-  color: grey;
-}
-.patchContentTable .diffTextINSERT {
-  color: #006000;
-}
-
-.patchContentTable tr.commentHolder {
-  background: #E5ECF9;
-}
-.patchContentTable tr.commentHolder .iconCell {
-  background: white;
-}
-.patchContentTable tr.commentHolder .iconCellOfFileCommentRow {
-  background: trimColor;
-}
-.patchContentTable td.commentHolder {
-  padding-left: 0;
-  padding-right: 0;
-  border-top: 1px solid black;
-  border-right: 1px solid black;
-}
-.patchContentTable td.commentHolderLeftmost {
-  border-left: 1px solid black;
-}
-.patchContentTable td.commentHolder.commentPanelLast {
-  border-bottom: 1px solid black;
-}
-.patchContentTable .commentPanel {
-  font-family: norm-font;
-}
-.patchContentTable .commentPanel td {
-  font-family: norm-font;
-}
-.patchContentTable .commentPanelMessage {
-  padding-left: 1px;
-  padding-right: 1px;
-  white-space: normal;
-}
-.patchContentTable .commentPanelButtons,
-.patchContentTable .commentPanel textarea {
-  margin-left: 1px;
-}
-
-.lineNumber {
-  padding-left: 0.2em;
-  white-space: pre;
-  width: 1.5em;
-  text-align: center;
-  padding-right: 0.2em;
-  background: white;
-  border-bottom: 1px solid white;
-}
-.lineNumber.rightBorder {
-  border-right: thin solid #b0bdcc;
-}
-.lineNumber a {
-  color: #888;
-  text-decoration: none;
-}
-.patchContentTable td.fileColumnHeader {
-  background: trimColor;
-  font-family: norm-font;
-  font-weight: bold;
-  text-align: center;
-}
-.patchContentTable td.fileColumnHeader.unifiedTableHeader {
-  text-align: left;
-}
-.lineNumber.fileColumnHeader {
-  border-bottom: 1px solid trimColor;
-}
-
-.fileLine {
-  padding-left: 0;
-  padding-right: 0;
-  white-space: pre;
-  border-left: thin solid #b0bdcc;
-}
-.fileLineDELETE,
-.fileLineDELETE .wdc {
-  background: #ffeeee;
-  border-bottom: 1px solid #ffeeee;
-}
-.fileLineINSERT,
-.fileLineINSERT .wdc {
-  background: #ddffdd;
-  border-bottom: 1px solid #ddffdd;
-}
-.patchContentTable .wdd {
-  border-bottom: 1px solid #FAA;
-}
-.patchContentTable .wdi {
-  border-bottom: 1px solid #9F9;
-}
-
-.patchContentTable td.cellsNextToFileComment {
-  background: trimColor;
-  border-top: trimColor;
-  border-bottom: trimColor;
-}
-.patchContentTable .activeRow .iconCell,
-.patchContentTable .activeRow .lineNumber {
-  background: selectionColor;
-}
-.patchContentTable .activeRow .iconCell,
-.patchContentTable .activeRow .lineNumber,
-.patchContentTable .activeRow .fileLine,
-.patchContentTable .activeRow .diffText,
-.patchContentTable .activeRow td.commentHolder,
-.patchContentTable .activeRow .wdc,
-.patchContentTable .activeRow .wdd,
-.patchContentTable .activeRow .wdi,
-.patchContentTable .activeRow .iconCellOfFileCommentRow,
-.patchContentTable .activeRow td.commentHolder.commentPanelLast  {
-  border-bottom: 1px solid blue;
-}
-
-.patchContentTable .fileCommentBorder .iconCellOfFileCommentRow,
-.patchContentTable .fileCommentBorder .lineNumber,
-.patchContentTable .fileCommentBorder .diffText {
-  height: 20px;
-  background: trimColor;
-  border-bottom: 1px solid trimColor;
-}
-
 /** Change **/
-.changeScreenStarIcon {
-  margin-right: 5px;
-}
-
-.changeScreenDescription,
-.changeScreenDescription textarea {
-  white-space: pre;
-  font-family: mono-font;
-  font-size: 9pt;
-}
-.changeScreenDescription p {
-  margin-top: 0px;
-  padding-top: 0.5em;
-}
-
 .avatarInfoPanel {
   margin-right: 10px;
 }
@@ -861,10 +565,6 @@
   text-align: right;
 }
 
-.infoBlock td.noborder {
-  border-right: none;
-}
-
 .infoBlock td.bottomheader {
   border-bottom: 1px solid trimColor;
 }
@@ -968,45 +668,6 @@
   text-overflow: ellipsis;
 }
 
-/** UnifiedScreen **/
-.unifiedTable {
-  width: 100%;
-  border: 1px solid #B0BDCC;
-  display: table;
-}
-
-.sideBySideScreenLinkTable {
-  width: 100%;
-}
-.sideBySideScreenLinkTable td {
-  width: 33%;
-}
-
-.patchNoDifference {
-  margin-top: 1em;
-  margin-bottom: 2em;
-  margin-left: 1em;
-  margin-right: 5em;
-  font-weight: bold;
-  font-size: medium;
-  font-family: norm-font;
-}
-
-/** Patch History Table **/
-.patchHistoryTable {
-  width: auto;
-  margin-bottom: 10px;
-}
-
-.patchHistoryTable .dataCell {
-  white-space: nowrap;
-}
-
-.patchHistoryTablePatchSetHeader {
-  text-align: right;
-}
-
-
 /** AccountSettings  **/
 .usernameField {
   white-space: nowrap;
@@ -1201,17 +862,6 @@
   width: 100%;
 }
 
-/** PatchBrowserPopup **/
-.patchBrowserPopup {
-  opacity: 0.90;
-}
-.patchBrowserPopupBody {
-  background: backgroundColor;
-  margin: 4px;
-  opacity: 0.90;
-}
-
-
 /** AccountGroupInfoScreen **/
 .groupUUIDPanel {
   margin-bottom: 10px;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupAuditEventInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupAuditEventInfo.java
index f62ae22..ed41b65 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupAuditEventInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupAuditEventInfo.java
@@ -37,8 +37,8 @@
   public final native AccountInfo memberAsUser() /*-{ return this.member; }-*/;
   public final native GroupInfo memberAsGroup() /*-{ return this.member; }-*/;
 
-  private final native String dateRaw() /*-{ return this.date; }-*/;
-  private final native String typeRaw() /*-{ return this.type; }-*/;
+  private native String dateRaw() /*-{ return this.date; }-*/;
+  private native String typeRaw() /*-{ return this.type; }-*/;
 
   protected GroupAuditEventInfo() {
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java
index 6142e5b..c3fd4ed 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java
@@ -33,9 +33,9 @@
   public final native JsArray<AccountInfo> members() /*-{ return this.members; }-*/;
   public final native JsArray<GroupInfo> includes() /*-{ return this.includes; }-*/;
 
-  private final native int group_id() /*-{ return this.group_id; }-*/;
-  private final native String owner_id() /*-{ return this.owner_id; }-*/;
-  private final native void owner_id(String o) /*-{ if(o)this.owner_id=o; }-*/;
+  private native int group_id() /*-{ return this.group_id; }-*/;
+  private native String owner_id() /*-{ return this.owner_id; }-*/;
+  private native void owner_id(String o) /*-{ if(o)this.owner_id=o; }-*/;
 
   public final AccountGroup.UUID getOwnerUUID() {
     String owner = owner_id();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/MemberList.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/MemberList.java
deleted file mode 100644
index d63c212..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/MemberList.java
+++ /dev/null
@@ -1,31 +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.client.groups;
-
-import com.google.gerrit.client.info.AccountInfo;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-
-public class MemberList extends JsArray<AccountInfo> {
-  public static void all(AccountGroup.UUID group,
-      AsyncCallback<MemberList> callback) {
-    new RestApi("/groups/").id(group.get()).view("members").get(callback);
-  }
-
-  protected MemberList() {
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java
deleted file mode 100644
index 5b9203a..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java
+++ /dev/null
@@ -1,970 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.patches;
-
-import com.google.gerrit.client.FormatUtil;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.CommentApi;
-import com.google.gerrit.client.changes.CommentInfo;
-import com.google.gerrit.client.info.AccountInfo;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.CommentLinkProcessor;
-import com.google.gerrit.client.ui.CommentPanel;
-import com.google.gerrit.client.ui.NavigationTable;
-import com.google.gerrit.client.ui.NeedsSignInKeyCommand;
-import com.google.gerrit.common.data.AccountInfoCache;
-import com.google.gerrit.common.data.CommentDetail;
-import com.google.gerrit.common.data.PatchScript;
-import com.google.gerrit.common.data.PatchSetDetail;
-import com.google.gerrit.extensions.client.DiffPreferencesInfo;
-import com.google.gerrit.prettify.client.ClientSideFormatter;
-import com.google.gerrit.prettify.client.PrettyFormatter;
-import com.google.gerrit.prettify.client.SparseHtmlFile;
-import com.google.gerrit.prettify.common.SparseFileContent;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.event.dom.client.BlurEvent;
-import com.google.gwt.event.dom.client.BlurHandler;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.DoubleClickEvent;
-import com.google.gwt.event.dom.client.DoubleClickHandler;
-import com.google.gwt.event.dom.client.FocusEvent;
-import com.google.gwt.event.dom.client.FocusHandler;
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.shared.HandlerRegistration;
-import com.google.gwt.user.client.DOM;
-import com.google.gwt.user.client.History;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.Focusable;
-import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
-import com.google.gwt.user.client.ui.Image;
-import com.google.gwt.user.client.ui.UIObject;
-import com.google.gwt.user.client.ui.Widget;
-import com.google.gwtexpui.globalkey.client.GlobalKey;
-import com.google.gwtexpui.globalkey.client.KeyCommand;
-import com.google.gwtexpui.globalkey.client.KeyCommandSet;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-import com.google.gwtorm.client.KeyUtil;
-
-import org.eclipse.jgit.diff.Edit;
-
-import java.sql.Timestamp;
-import java.util.ArrayList;
-import java.util.List;
-
-abstract class AbstractPatchContentTable extends NavigationTable<Object>
-    implements CommentEditorContainer, FocusHandler, BlurHandler {
-  public static final int R_HEAD = 0;
-  static final short FILE_SIDE_A = (short) 0;
-  static final short FILE_SIDE_B = (short) 1;
-  protected PatchTable fileList;
-  protected AccountInfoCache accountCache = AccountInfoCache.empty();
-  protected Patch.Key patchKey;
-  protected PatchSet.Id idSideA;
-  protected PatchSet.Id idSideB;
-  protected boolean onlyOneHunk;
-  protected PatchSetSelectBox headerSideA;
-  protected PatchSetSelectBox headerSideB;
-  protected Image iconA;
-  protected Image iconB;
-
-  private final KeyCommandSet keysComment;
-  private HandlerRegistration regComment;
-  private final KeyCommandSet keysOpenByEnter;
-  private HandlerRegistration regOpenByEnter;
-  private CommentLinkProcessor commentLinkProcessor;
-  boolean isDisplayBinary;
-
-  protected AbstractPatchContentTable() {
-    keysNavigation.add(new PrevKeyCommand(0, 'k', PatchUtil.C.linePrev()));
-    keysNavigation.add(new NextKeyCommand(0, 'j', PatchUtil.C.lineNext()));
-    keysNavigation.add(new PrevChunkKeyCmd(0, 'p', PatchUtil.C.chunkPrev()));
-    keysNavigation.add(new NextChunkKeyCmd(0, 'n', PatchUtil.C.chunkNext()));
-    keysNavigation.add(new PrevCommentCmd(0, 'P', PatchUtil.C.commentPrev()));
-    keysNavigation.add(new NextCommentCmd(0, 'N', PatchUtil.C.commentNext()));
-
-    keysAction.add(new OpenKeyCommand(0, 'o', PatchUtil.C.expandComment()));
-    keysOpenByEnter = new KeyCommandSet(Gerrit.C.sectionNavigation());
-    keysOpenByEnter.add(new OpenKeyCommand(0, KeyCodes.KEY_ENTER, PatchUtil.C.expandComment()));
-
-    if (Gerrit.isSignedIn()) {
-      keysAction.add(new InsertCommentCommand(0, 'c', PatchUtil.C
-          .commentInsert()));
-
-      // See CommentEditorPanel
-      //
-      keysComment = new KeyCommandSet(PatchUtil.C.commentEditorSet());
-      keysComment.add(new NoOpKeyCommand(KeyCommand.M_CTRL, 's', PatchUtil.C
-          .commentSaveDraft()));
-      keysComment.add(new NoOpKeyCommand(0, KeyCodes.KEY_ESCAPE, PatchUtil.C
-          .commentCancelEdit()));
-    } else {
-      keysComment = null;
-    }
-
-    table.setStyleName(Gerrit.RESOURCES.css().patchContentTable());
-  }
-
-  abstract void createFileCommentEditorOnSideA();
-
-  abstract void createFileCommentEditorOnSideB();
-
-  protected void initHeaders(PatchScript script, PatchSetDetail detail) {
-    headerSideA = new PatchSetSelectBox(PatchSetSelectBox.Side.A);
-    headerSideA.display(detail, script, patchKey, idSideA, idSideB);
-    headerSideA.addDoubleClickHandler(new DoubleClickHandler() {
-      @Override
-      public void onDoubleClick(DoubleClickEvent event) {
-        if (headerSideA.isFileOrCommitMessage()) {
-          createFileCommentEditorOnSideA();
-        }
-      }
-    });
-    headerSideB = new PatchSetSelectBox(PatchSetSelectBox.Side.B);
-    headerSideB.display(detail, script, patchKey, idSideA, idSideB);
-    headerSideB.addDoubleClickHandler(new DoubleClickHandler() {
-      @Override
-      public void onDoubleClick(DoubleClickEvent event) {
-        if (headerSideB.isFileOrCommitMessage()) {
-          createFileCommentEditorOnSideB();
-        }
-      }
-    });
-
-    // Prepare icons.
-    iconA = new Image(Gerrit.RESOURCES.addFileComment());
-    iconA.setTitle(PatchUtil.C.addFileCommentToolTip());
-    iconA.addStyleName(Gerrit.RESOURCES.css().link());
-    iconA.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(ClickEvent event) {
-        createFileCommentEditorOnSideA();
-      }
-    });
-    iconB = new Image(Gerrit.RESOURCES.addFileComment());
-    iconB.setTitle(PatchUtil.C.addFileCommentToolTip());
-    iconB.addStyleName(Gerrit.RESOURCES.css().link());
-    iconB.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(ClickEvent event) {
-        createFileCommentEditorOnSideB();
-      }
-    });
-  }
-
-  @Override
-  public void notifyDraftDelta(final int delta) {
-    if (fileList != null) {
-      fileList.notifyDraftDelta(patchKey, delta);
-    }
-
-    Widget p = getParent();
-    while (p != null) {
-      if (p instanceof CommentEditorContainer) {
-        ((CommentEditorContainer) p).notifyDraftDelta(delta);
-        break;
-      }
-      p = p.getParent();
-    }
-  }
-
-  @Override
-  public void remove(CommentEditorPanel panel) {
-    final int nRows = table.getRowCount();
-    for (int row = 0; row < nRows; row++) {
-      final int nCells = table.getCellCount(row);
-      for (int cell = 0; cell < nCells; cell++) {
-        if (table.getWidget(row, cell) == panel) {
-          destroyEditor(row, cell);
-          Widget p = table;
-          while (p != null) {
-            if (p instanceof Focusable) {
-              ((Focusable) p).setFocus(true);
-              break;
-            }
-            p = p.getParent();
-          }
-
-          if (table.getCellFormatter().getStyleName(row - 1, cell)
-              .contains(Gerrit.RESOURCES.css().commentHolder())) {
-            table.getCellFormatter().addStyleName(row - 1, cell,
-                Gerrit.RESOURCES.css().commentPanelLast());
-          }
-          return;
-        }
-      }
-    }
-  }
-
-  @Override
-  public void setRegisterKeys(final boolean on) {
-    super.setRegisterKeys(on);
-    if (on && keysComment != null && regComment == null) {
-      regComment = GlobalKey.add(this, keysComment);
-    } else if (!on && regComment != null) {
-      regComment.removeHandler();
-      regComment = null;
-    }
-
-    if (on && keysOpenByEnter != null && regOpenByEnter == null) {
-      regOpenByEnter = GlobalKey.add(this, keysOpenByEnter);
-    } else if (!on && regOpenByEnter != null) {
-      regOpenByEnter.removeHandler();
-      regOpenByEnter = null;
-    }
-  }
-
-  public void display(final Patch.Key k, final PatchSet.Id a,
-      final PatchSet.Id b, final PatchScript s, final PatchSetDetail d) {
-    patchKey = k;
-    idSideA = a;
-    idSideB = b;
-
-    render(s, d);
-  }
-
-  void setCommentLinkProcessor(CommentLinkProcessor commentLinkProcessor) {
-    this.commentLinkProcessor = commentLinkProcessor;
-  }
-
-  protected boolean hasDifferences(PatchScript script) {
-    return hasEdits(script) || hasMeta(script) || hasComments(script);
-  }
-
-  public boolean isPureMetaChange(PatchScript script) {
-    return !hasEdits(script) && hasMeta(script);
-  }
-
-  // True if there are differences between the two patch sets
-  private boolean hasEdits(PatchScript script) {
-    for (Edit e : script.getEdits()) {
-      if (e.getType() != Edit.Type.EMPTY) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  // True if one of the two patch sets has comments
-  private boolean hasComments(PatchScript script) {
-    return !script.getCommentDetail().getCommentsA().isEmpty()
-        || !script.getCommentDetail().getCommentsB().isEmpty();
-  }
-
-  // True if this change is a mode change or a pure rename/copy
-  private boolean hasMeta(PatchScript script) {
-    return !script.getPatchHeader().isEmpty();
-  }
-
-  protected void appendNoDifferences(SafeHtmlBuilder m) {
-    m.openTr();
-    m.openTd();
-    m.setAttribute("colspan", 5);
-    m.openDiv();
-    m.addStyleName(Gerrit.RESOURCES.css().patchNoDifference());
-    m.append(PatchUtil.C.noDifference());
-    m.closeDiv();
-    m.closeTd();
-    m.closeTr();
-  }
-
-  protected SparseHtmlFile getSparseHtmlFileA(PatchScript s) {
-    DiffPreferencesInfo dp = s.getDiffPrefs();
-    dp.showWhitespaceErrors = false;
-
-    PrettyFormatter f = ClientSideFormatter.FACTORY.get();
-    f.setDiffPrefs(dp);
-    f.setFileName(s.getA().getPath());
-    f.setEditFilter(PrettyFormatter.A);
-    f.setEditList(s.getEdits());
-    f.format(s.getA());
-    return f;
-  }
-
-  protected SparseHtmlFile getSparseHtmlFileB(PatchScript s) {
-    DiffPreferencesInfo dp = s.getDiffPrefs();
-
-    SparseFileContent b = s.getB();
-    PrettyFormatter f = ClientSideFormatter.FACTORY.get();
-    f.setDiffPrefs(dp);
-    f.setFileName(b.getPath());
-    f.setEditFilter(PrettyFormatter.B);
-    f.setEditList(s.getEdits());
-
-    if (s.getA().isWholeFile() && !b.isWholeFile()) {
-      b = b.apply(s.getA(), s.getEdits());
-    }
-    f.format(b);
-    return f;
-  }
-
-  protected String getUrlA() {
-    final String rawBase = GWT.getHostPageBaseURL() + "cat/";
-    final String url;
-    if (idSideA == null) {
-      url = rawBase + KeyUtil.encode(patchKey.toString()) + "^1";
-    } else {
-      Patch.Key k = new Patch.Key(idSideA, patchKey.get());
-      url = rawBase + KeyUtil.encode(k.toString()) + "^0";
-    }
-    return url;
-  }
-
-  protected String getUrlB() {
-    final String rawBase = GWT.getHostPageBaseURL() + "cat/";
-    return rawBase + KeyUtil.encode(patchKey.toString()) + "^0";
-  }
-
-  protected abstract void render(PatchScript script, final PatchSetDetail detail);
-
-  protected abstract void onInsertComment(PatchLine pl);
-
-  public abstract void display(CommentDetail comments, boolean expandComments);
-
-  @Override
-  protected Object getRowItemKey(final Object item) {
-    return null;
-  }
-
-  protected void initScript(final PatchScript script) {
-    if (script.getEdits().size() == 1) {
-      final SparseFileContent a = script.getA();
-      final SparseFileContent b = script.getB();
-      onlyOneHunk = a.size() == 0 || b.size() == 0;
-    } else {
-      onlyOneHunk = false;
-    }
-  }
-
-  private boolean isChunk(final int row) {
-    final Object o = getRowItem(row);
-    if (!onlyOneHunk && o instanceof PatchLine) {
-      final PatchLine pl = (PatchLine) o;
-      switch (pl.getType()) {
-        case DELETE:
-        case INSERT:
-        case REPLACE:
-          return true;
-        case CONTEXT:
-          break;
-      }
-    } else if (o instanceof CommentList) {
-      return true;
-    }
-    return false;
-  }
-
-  private int findChunkStart(int row) {
-    while (0 <= row && isChunk(row)) {
-      row--;
-    }
-    return row + 1;
-  }
-
-  private int findChunkEnd(int row) {
-    final int max = table.getRowCount();
-    while (row < max && isChunk(row)) {
-      row++;
-    }
-    return row - 1;
-  }
-
-  private static int oneBefore(final int begin) {
-    return 1 <= begin ? begin - 1 : begin;
-  }
-
-  private int oneAfter(final int end) {
-    return end + 1 < table.getRowCount() ? end + 1 : end;
-  }
-
-  private void moveToPrevChunk(int row) {
-    while (0 <= row && isChunk(row)) {
-      row--;
-    }
-    for (; 0 <= row; row--) {
-      if (isChunk(row)) {
-        final int start = findChunkStart(row);
-        movePointerTo(start, false);
-        scrollIntoView(oneBefore(start), oneAfter(row));
-        return;
-      }
-    }
-
-    // No prior hunk found? Try to hit the first line in the file.
-    //
-    for (row = 0; row < table.getRowCount(); row++) {
-      if (getRowItem(row) != null) {
-        movePointerTo(row);
-        break;
-      }
-    }
-  }
-
-  private void moveToNextChunk(int row) {
-    final int max = table.getRowCount();
-    while (row < max && isChunk(row)) {
-      row++;
-    }
-    for (; row < max; row++) {
-      if (isChunk(row)) {
-        movePointerTo(row, false);
-        scrollIntoView(oneBefore(row), oneAfter(findChunkEnd(row)));
-        return;
-      }
-    }
-
-    // No next hunk found? Try to hit the last line in the file.
-    //
-    for (row = max - 1; row >= 0; row--) {
-      if (getRowItem(row) != null) {
-        movePointerTo(row);
-        break;
-      }
-    }
-  }
-
-  private void moveToPrevComment(int row) {
-    while (0 <= row && isComment(row)) {
-      row--;
-    }
-    for (; 0 <= row; row--) {
-      if (isComment(row)) {
-        movePointerTo(row, false);
-        scrollIntoView(oneBefore(row), oneAfter(row));
-        return;
-      }
-    }
-
-    // No prior comment found? Try to hit the first line in the file.
-    //
-    for (row = 0; row < table.getRowCount(); row++) {
-      if (getRowItem(row) != null) {
-        movePointerTo(row);
-        break;
-      }
-    }
-  }
-
-  private void moveToNextComment(int row) {
-    final int max = table.getRowCount();
-    while (row < max && isComment(row)) {
-      row++;
-    }
-    for (; row < max; row++) {
-      if (isComment(row)) {
-        movePointerTo(row, false);
-        scrollIntoView(oneBefore(row), oneAfter(row));
-        return;
-      }
-    }
-
-    // No next comment found? Try to hit the last line in the file.
-    //
-    for (row = max - 1; row >= 0; row--) {
-      if (getRowItem(row) != null) {
-        movePointerTo(row);
-        break;
-      }
-    }
-  }
-
-  private boolean isComment(int row) {
-    return getRowItem(row) instanceof CommentList;
-  }
-
-  /**
-   * Invokes createCommentEditor() with an empty string as value for the comment
-   * parent UUID. This method is invoked by callers that want to create an
-   * editor for a comment that is not a reply.
-   */
-  protected void createCommentEditor(final int suggestRow, final int column,
-      final int line, final short file) {
-    if (Gerrit.isSignedIn()) {
-      if (R_HEAD <= line) {
-        final Patch.Key parentKey;
-        final short side;
-        switch (file) {
-          case 0:
-            if (idSideA == null) {
-              parentKey = new Patch.Key(idSideB, patchKey.get());
-              side = (short) 0;
-            } else {
-              parentKey = new Patch.Key(idSideA, patchKey.get());
-              side = (short) 1;
-            }
-            break;
-          case 1:
-            parentKey = new Patch.Key(idSideB, patchKey.get());
-            side = (short) 1;
-            break;
-          default:
-            throw new RuntimeException("unexpected file id " + file);
-        }
-
-        final PatchLineComment newComment = new PatchLineComment(
-            new PatchLineComment.Key(parentKey, null), line,
-            Gerrit.getUserAccount().getId(), null,
-            new Timestamp(System.currentTimeMillis()));
-        newComment.setSide(side);
-        newComment.setMessage("");
-
-        findOrCreateCommentEditor(suggestRow, column, newComment, true)
-            .setFocus(true);
-      }
-    } else {
-      Gerrit.doSignIn(History.getToken());
-    }
-  }
-
-  /**
-   * Update cursor after selecting a comment.
-   *
-   * @param newComment comment that was selected.
-   */
-  protected void updateCursor(final PatchLineComment newComment) {
-  }
-
-  abstract void insertFileCommentRow(final int row);
-
-  private CommentEditorPanel findOrCreateCommentEditor(final int suggestRow,
-      final int column, final PatchLineComment newComment, final boolean create) {
-    int row = suggestRow;
-    int[] spans = new int[column + 1];
-    FIND_ROW: while (row < table.getRowCount()) {
-      int col = 0;
-      for (int cell = 0; row < table.getRowCount()
-          && cell < table.getCellCount(row); cell++) {
-        while (col < column && 0 < spans[col]) {
-          spans[col++]--;
-        }
-        spans[col] = table.getFlexCellFormatter().getRowSpan(row, cell);
-        if (col == column) {
-          final Widget w = table.getWidget(row, cell);
-          if (w instanceof CommentEditorPanel
-              && ((CommentEditorPanel) w).getComment().getKey().getParentKey()
-                  .equals(newComment.getKey().getParentKey())) {
-            // Don't insert two editors on the same position, it doesn't make
-            // any sense to the user.
-            //
-            return ((CommentEditorPanel) w);
-
-          } else if (w instanceof CommentPanel) {
-            if (newComment != null && newComment.getParentUuid() != null) {
-              // If we are a reply, we were given the exact row to insert
-              // ourselves at. We should be before this panel so break.
-              //
-              break FIND_ROW;
-            }
-            row++;
-            cell--;
-          } else {
-            break FIND_ROW;
-          }
-        }
-      }
-    }
-
-    if (newComment == null || !create) {
-      return null;
-    }
-
-    final CommentEditorPanel ed =
-        new CommentEditorPanel(newComment, commentLinkProcessor);
-    ed.addFocusHandler(this);
-    ed.addBlurHandler(this);
-    boolean isCommentRow = false;
-    boolean needInsert = false;
-    if (row < table.getRowCount()) {
-      for (int cell = 0; cell < table.getCellCount(row); cell++) {
-        final Widget w = table.getWidget(row, cell);
-        if (w instanceof CommentEditorPanel || w instanceof CommentPanel) {
-          if (column == cell) {
-            needInsert = true;
-          }
-          isCommentRow = true;
-        }
-      }
-    }
-    if (needInsert || !isCommentRow) {
-      if (newComment.getLine() == R_HEAD) {
-        insertFileCommentRow(row);
-      } else {
-        insertRow(row);
-      }
-      styleCommentRow(row);
-    }
-    table.setWidget(row, column, ed);
-    styleLastCommentCell(row, column);
-
-    int span = 1;
-    for (int r = row + 1; r < table.getRowCount(); r++) {
-      boolean hasComment = false;
-      for (int c = 0; c < table.getCellCount(r); c++) {
-        final Widget w = table.getWidget(r, c);
-        if (w instanceof CommentPanel || w instanceof CommentEditorPanel) {
-          if (c != column) {
-            hasComment = true;
-            break;
-          }
-        }
-      }
-      if (hasComment) {
-        table.removeCell(r, column);
-        span++;
-      } else {
-        break;
-      }
-    }
-    if (span > 1) {
-      table.getFlexCellFormatter().setRowSpan(row, column, span);
-    }
-
-    for (int r = row - 1; r > 0; r--) {
-      if (getRowItem(r) instanceof CommentList) {
-        continue;
-      } else if (getRowItem(r) != null) {
-        movePointerTo(r);
-        break;
-      }
-    }
-
-    updateCursor(newComment);
-    return ed;
-  }
-
-  protected void insertRow(final int row) {
-    table.insertRow(row);
-    table.getCellFormatter().setStyleName(row, 0,
-        Gerrit.RESOURCES.css().iconCell());
-  }
-
-  @Override
-  protected void onOpenRow(final int row) {
-    final Object item = getRowItem(row);
-    if (item instanceof CommentList) {
-      for (final CommentPanel p : ((CommentList) item).panels) {
-        p.setOpen(!p.isOpen());
-      }
-    }
-  }
-
-  public void setAccountInfoCache(final AccountInfoCache aic) {
-    assert aic != null;
-    accountCache = aic;
-  }
-
-  private void destroyEditor(final int row, final int col) {
-    table.clearCell(row, col);
-    final int span = table.getFlexCellFormatter().getRowSpan(row, col);
-    boolean removeRow = true;
-    final int nCells = table.getCellCount(row);
-    for (int cell = 0; cell < nCells; cell++) {
-      if (table.getWidget(row, cell) != null) {
-        removeRow = false;
-        break;
-      }
-    }
-    if (removeRow) {
-      destroyCommentRow(row);
-    } else {
-      destroyComment(row, col, span);
-    }
-  }
-
-  protected void destroyCommentRow(int row) {
-    for (int r = row - 1; 0 <= r; r--) {
-      boolean data = false;
-      for (int c = 0; c < table.getCellCount(r); c++) {
-        data |= table.getWidget(r, c) != null;
-        final int s = table.getFlexCellFormatter().getRowSpan(r, c) - 1;
-        if (r + s == row) {
-          table.getFlexCellFormatter().setRowSpan(r, c, s);
-        }
-      }
-      if (!data) {
-        break;
-      }
-    }
-    table.removeRow(row);
-  }
-
-  private void destroyComment(int row, int col, int span) {
-    table.getFlexCellFormatter().setStyleName(//
-        row, col, Gerrit.RESOURCES.css().diffText());
-
-    if (span != 1) {
-      table.getFlexCellFormatter().setRowSpan(row, col, 1);
-      for (int r = row + 1; r < row + span; r++) {
-        table.insertCell(r, col);
-
-        table.getFlexCellFormatter().setStyleName(//
-            r, col, Gerrit.RESOURCES.css().diffText());
-      }
-    }
-  }
-
-  protected void bindComment(final int row, final int col,
-      final PatchLineComment line, boolean expandComment) {
-    if (line.getStatus() == PatchLineComment.Status.DRAFT) {
-      final CommentEditorPanel plc =
-          new CommentEditorPanel(line, commentLinkProcessor);
-      plc.addFocusHandler(this);
-      plc.addBlurHandler(this);
-      table.setWidget(row, col, plc);
-      styleLastCommentCell(row, col);
-
-    } else {
-      final AccountInfo author = FormatUtil.asInfo(accountCache.get(line.getAuthor()));
-      final PublishedCommentPanel panel =
-          new PublishedCommentPanel(author, line);
-      panel.setOpen(expandComment);
-      panel.addFocusHandler(this);
-      panel.addBlurHandler(this);
-      table.setWidget(row, col, panel);
-      styleLastCommentCell(row, col);
-
-      CommentList l = (CommentList) getRowItem(row);
-      if (l == null) {
-        l = new CommentList();
-        setRowItem(row, l);
-      }
-      l.comments.add(line);
-      l.panels.add(panel);
-    }
-
-    styleCommentRow(row);
-  }
-
-  @Override
-  public void onFocus(FocusEvent event) {
-    // when the comment panel gets focused (actually when a button inside the
-    // comment panel gets focused) we have to unregister the key binding for
-    // ENTER that expands/collapses the comment panel, if we don't do this the
-    // focused button in the comment panel cannot be triggered by pressing ENTER
-    // since ENTER would then be already consumed by this key binding
-    if (regOpenByEnter != null) {
-      regOpenByEnter.removeHandler();
-      regOpenByEnter = null;
-    }
-  }
-
-  @Override
-  public void onBlur(BlurEvent event) {
-    // when the comment panel gets blurred (actually when a button inside the
-    // comment panel gets blurred) we have to re-register the key binding for
-    // ENTER that expands/collapses the comment panel
-    if (keysOpenByEnter != null && regOpenByEnter == null) {
-      regOpenByEnter = GlobalKey.add(this, keysOpenByEnter);
-    }
-  }
-
-  private void styleCommentRow(final int row) {
-    final CellFormatter fmt = table.getCellFormatter();
-    final Element iconCell = fmt.getElement(row, 0);
-    UIObject.setStyleName(DOM.getParent(iconCell), Gerrit.RESOURCES.css()
-        .commentHolder(), true);
-  }
-
-  private void styleLastCommentCell(final int row, final int col) {
-    final CellFormatter fmt = table.getCellFormatter();
-    fmt.removeStyleName(row - 1, col, //
-        Gerrit.RESOURCES.css().commentPanelLast());
-    fmt.setStyleName(row, col, Gerrit.RESOURCES.css().commentHolder());
-    fmt.addStyleName(row, col, Gerrit.RESOURCES.css().commentPanelLast());
-    if (!fmt.getStyleName(row, col - 1).contains(Gerrit.RESOURCES.css().commentHolder())) {
-      fmt.addStyleName(row, col, Gerrit.RESOURCES.css().commentHolderLeftmost());
-    }
-  }
-
-  protected static class CommentList {
-    final List<PatchLineComment> comments = new ArrayList<>();
-    final List<PublishedCommentPanel> panels = new ArrayList<>();
-  }
-
-  public static class NoOpKeyCommand extends NeedsSignInKeyCommand {
-    public NoOpKeyCommand(int mask, int key, String help) {
-      super(mask, key, help);
-    }
-
-    @Override
-    public void onKeyPress(final KeyPressEvent event) {
-    }
-  }
-
-  public class InsertCommentCommand extends NeedsSignInKeyCommand {
-    public InsertCommentCommand(int mask, int key, String help) {
-      super(mask, key, help);
-    }
-
-    @Override
-    public void onKeyPress(final KeyPressEvent event) {
-      ensurePointerVisible();
-      for (int row = getCurrentRow(); 0 <= row; row--) {
-        final Object item = getRowItem(row);
-        if (item instanceof PatchLine) {
-          onInsertComment((PatchLine) item);
-          return;
-        } else if (item instanceof CommentList) {
-          continue;
-        } else {
-          return;
-        }
-      }
-    }
-  }
-
-  public class PrevChunkKeyCmd extends KeyCommand {
-    public PrevChunkKeyCmd(int mask, int key, String help) {
-      super(mask, key, help);
-    }
-
-    @Override
-    public void onKeyPress(final KeyPressEvent event) {
-      ensurePointerVisible();
-      moveToPrevChunk(getCurrentRow());
-    }
-  }
-
-  public class NextChunkKeyCmd extends KeyCommand {
-    public NextChunkKeyCmd(int mask, int key, String help) {
-      super(mask, key, help);
-    }
-
-    @Override
-    public void onKeyPress(final KeyPressEvent event) {
-      ensurePointerVisible();
-      moveToNextChunk(getCurrentRow());
-    }
-  }
-
-  public class PrevCommentCmd extends KeyCommand {
-    public PrevCommentCmd(int mask, int key, String help) {
-      super(mask, key, help);
-    }
-
-    @Override
-    public void onKeyPress(final KeyPressEvent event) {
-      ensurePointerVisible();
-      moveToPrevComment(getCurrentRow());
-    }
-  }
-
-  public class NextCommentCmd extends KeyCommand {
-    public NextCommentCmd(int mask, int key, String help) {
-      super(mask, key, help);
-    }
-
-    @Override
-    public void onKeyPress(final KeyPressEvent event) {
-      ensurePointerVisible();
-      moveToNextComment(getCurrentRow());
-    }
-  }
-
-  private class PublishedCommentPanel extends CommentPanel implements
-      ClickHandler {
-    final PatchLineComment comment;
-    final Button reply;
-    final Button replyDone;
-
-    PublishedCommentPanel(final AccountInfo author, final PatchLineComment c) {
-      super(author, c.getWrittenOn(), c.getMessage(), commentLinkProcessor);
-      this.comment = c;
-
-      reply = new Button(PatchUtil.C.buttonReply());
-      reply.addClickHandler(this);
-      addButton(reply);
-
-      replyDone = new Button(PatchUtil.C.buttonReplyDone());
-      replyDone.addClickHandler(this);
-      addButton(replyDone);
-    }
-
-    @Override
-    public void onClick(final ClickEvent event) {
-      if (Gerrit.isSignedIn()) {
-        if (reply == event.getSource()) {
-          createReplyEditor();
-        } else if (replyDone == event.getSource()) {
-          cannedReply(PatchUtil.C.cannedReplyDone());
-        }
-
-      } else {
-        Gerrit.doSignIn(History.getToken());
-      }
-    }
-
-    private void createReplyEditor() {
-      final PatchLineComment newComment = newComment();
-      newComment.setMessage("");
-      findOrCreateEditor(newComment, true).setFocus(true);
-    }
-
-    private void cannedReply(String message) {
-      final PatchLineComment newComment = newComment();
-      newComment.setMessage(message);
-      CommentEditorPanel p = findOrCreateEditor(newComment, false);
-      if (p == null) {
-        enableButtons(false);
-        final PatchSet.Id psId = newComment.getKey().getParentKey().getParentKey();
-        CommentInfo in = CommentEditorPanel.toInput(newComment);
-        CommentApi.createDraft(psId, in,
-            new GerritCallback<CommentInfo>() {
-              @Override
-              public void onSuccess(CommentInfo result) {
-                enableButtons(true);
-                notifyDraftDelta(1);
-                findOrCreateEditor(CommentEditorPanel.toComment(
-                    psId, newComment.getKey().getParentKey().get(), result),
-                  true).setOpen(false);
-              }
-
-              @Override
-              public void onFailure(Throwable caught) {
-                enableButtons(true);
-                super.onFailure(caught);
-              }
-            });
-      } else {
-        if (!p.isOpen()) {
-          p.setOpen(true);
-        }
-        p.setFocus(true);
-      }
-    }
-
-    private CommentEditorPanel findOrCreateEditor(
-        PatchLineComment newComment, boolean create) {
-      int row = rowOf(getElement());
-      int column = columnOf(getElement());
-      return findOrCreateCommentEditor(row + 1, column, newComment, create);
-    }
-
-    private PatchLineComment newComment() {
-      PatchLineComment newComment =
-          new PatchLineComment(new PatchLineComment.Key(comment.getKey()
-              .getParentKey(), null), comment.getLine(), Gerrit
-              .getUserAccount().getId(), comment.getKey().get(),
-              new Timestamp(System.currentTimeMillis()));
-      newComment.setSide(comment.getSide());
-      return newComment;
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java
deleted file mode 100644
index 3cf29e5..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java
+++ /dev/null
@@ -1,371 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.patches;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.change.LocalComments;
-import com.google.gerrit.client.changes.CommentApi;
-import com.google.gerrit.client.changes.CommentInfo;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.CommentLinkProcessor;
-import com.google.gerrit.client.ui.CommentPanel;
-import com.google.gerrit.extensions.client.Side;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.DoubleClickEvent;
-import com.google.gwt.event.dom.client.DoubleClickHandler;
-import com.google.gwt.event.dom.client.KeyDownEvent;
-import com.google.gwt.event.dom.client.KeyDownHandler;
-import com.google.gwt.user.client.Timer;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.Widget;
-import com.google.gwtexpui.globalkey.client.NpTextArea;
-import com.google.gwtjsonrpc.common.AsyncCallback;
-import com.google.gwtjsonrpc.common.VoidResult;
-
-import java.sql.Timestamp;
-
-public class CommentEditorPanel extends CommentPanel implements ClickHandler,
-    DoubleClickHandler {
-  private static final int INITIAL_COLS = 60;
-  private static final int INITIAL_LINES = 5;
-  private static final int MAX_LINES = 30;
-  private static final AsyncCallback<VoidResult> NULL_CALLBACK =
-      new AsyncCallback<VoidResult>() {
-        @Override
-        public void onFailure(Throwable caught) {
-        }
-
-        @Override
-        public void onSuccess(VoidResult result) {
-        }
-      };
-
-  private PatchLineComment comment;
-
-  private final NpTextArea text;
-  private final Button edit;
-  private final Button save;
-  private final Button cancel;
-  private final Button discard;
-  private final Timer expandTimer;
-
-  public CommentEditorPanel(final PatchLineComment plc,
-      final CommentLinkProcessor commentLinkProcessor) {
-    super(commentLinkProcessor);
-    comment = plc;
-
-    addStyleName(Gerrit.RESOURCES.css().commentEditorPanel());
-    setAuthorNameText(Gerrit.getUserAccount(), PatchUtil.C.draft());
-    setMessageText(plc.getMessage());
-    addDoubleClickHandler(this);
-
-    expandTimer = new Timer() {
-      @Override
-      public void run() {
-        expandText();
-      }
-    };
-    text = new NpTextArea();
-    text.setText(comment.getMessage());
-    text.setCharacterWidth(INITIAL_COLS);
-    text.setVisibleLines(INITIAL_LINES);
-    text.setSpellCheck(true);
-    text.addKeyDownHandler(new KeyDownHandler() {
-      @Override
-      public void onKeyDown(final KeyDownEvent event) {
-        if ((event.isControlKeyDown() || event.isMetaKeyDown())
-            && !event.isAltKeyDown() && !event.isShiftKeyDown()) {
-          switch (event.getNativeKeyCode()) {
-            case 's':
-            case 'S':
-              event.preventDefault();
-              onSave(NULL_CALLBACK);
-              return;
-          }
-        }
-
-        expandTimer.schedule(250);
-      }
-    });
-    addContent(text);
-
-    edit = new Button();
-    edit.setText(PatchUtil.C.buttonEdit());
-    edit.addClickHandler(this);
-    addButton(edit);
-
-    save = new Button();
-    save.setText(PatchUtil.C.buttonSave());
-    save.addClickHandler(this);
-    addButton(save);
-
-    cancel = new Button();
-    cancel.setText(PatchUtil.C.buttonCancel());
-    cancel.addClickHandler(this);
-    addButton(cancel);
-
-    discard = new Button();
-    discard.setText(PatchUtil.C.buttonDiscard());
-    discard.addClickHandler(this);
-    addButton(discard);
-
-    setOpen(true);
-    if (isNew()) {
-      edit();
-    } else {
-      render();
-    }
-  }
-
-  private void expandText() {
-    final double cols = text.getCharacterWidth();
-    int rows = 2;
-    for (final String line : text.getText().split("\n")) {
-      rows += Math.ceil((1.0 + line.length()) / cols);
-    }
-    rows = Math.max(INITIAL_LINES, Math.min(rows, MAX_LINES));
-    if (text.getVisibleLines() != rows) {
-      text.setVisibleLines(rows);
-    }
-  }
-
-  private void edit() {
-    if (!isOpen()) {
-      setOpen(true);
-    }
-    text.setText(comment.getMessage());
-    expandText();
-    stateEdit(true);
-    text.setFocus(true);
-  }
-
-  private void render() {
-    final Timestamp on = comment.getWrittenOn();
-    setDateText(PatchUtil.M.draftSaved(new java.util.Date(on.getTime())));
-    setMessageText(comment.getMessage());
-    stateEdit(false);
-  }
-
-  private void stateEdit(final boolean inEdit) {
-    expandTimer.cancel();
-    setMessageTextVisible(!inEdit);
-    edit.setVisible(!inEdit);
-
-    if (inEdit) {
-      text.setVisible(true);
-    } else {
-      text.setFocus(false);
-      text.setVisible(false);
-    }
-
-    save.setVisible(inEdit);
-    cancel.setVisible(inEdit && !isNew());
-    discard.setVisible(inEdit);
-  }
-
-  void setFocus(final boolean take) {
-    if (take && !isOpen()) {
-      setOpen(true);
-    }
-    if (text.isVisible()) {
-      text.setFocus(take);
-    } else if (take) {
-      edit();
-    }
-  }
-
-  boolean isNew() {
-    return comment.getKey().get() == null;
-  }
-
-  public PatchLineComment getComment() {
-    return comment;
-  }
-
-  @Override
-  public void onDoubleClick(final DoubleClickEvent event) {
-    edit();
-  }
-
-  @Override
-  public void onClick(final ClickEvent event) {
-    final Widget sender = (Widget) event.getSource();
-    if (sender == edit) {
-      edit();
-
-    } else if (sender == save) {
-      onSave(NULL_CALLBACK);
-
-    } else if (sender == cancel) {
-      render();
-
-    } else if (sender == discard) {
-      onDiscard();
-    }
-  }
-
-  public void saveDraft(AsyncCallback<VoidResult> onSave) {
-    if (isOpen() && text.isVisible()) {
-      onSave(onSave);
-    } else {
-      onSave.onSuccess(VoidResult.INSTANCE);
-    }
-  }
-
-  private void onSave(final AsyncCallback<VoidResult> onSave) {
-    expandTimer.cancel();
-    final String txt = text.getText().trim();
-    if ("".equals(txt)) {
-      return;
-    }
-
-    comment.setMessage(txt);
-    text.setFocus(false);
-    text.setReadOnly(true);
-    save.setEnabled(false);
-    cancel.setEnabled(false);
-    discard.setEnabled(false);
-
-    final PatchSet.Id psId = comment.getKey().getParentKey().getParentKey();
-    final LocalComments lc = new LocalComments(psId);
-    final boolean wasNew = isNew();
-    GerritCallback<CommentInfo> cb = new GerritCallback<CommentInfo>() {
-      @Override
-      public void onSuccess(CommentInfo result) {
-        notifyDraftDelta(wasNew ? 1 : 0);
-        comment = toComment(psId, comment.getKey().getParentKey().get(), result);
-        text.setReadOnly(false);
-        save.setEnabled(true);
-        cancel.setEnabled(true);
-        discard.setEnabled(true);
-        render();
-        onSave.onSuccess(VoidResult.INSTANCE);
-      }
-
-      @Override
-      public void onFailure(final Throwable caught) {
-        text.setReadOnly(false);
-        text.setFocus(true);
-        save.setEnabled(true);
-        cancel.setEnabled(true);
-        discard.setEnabled(true);
-        lc.setInlineComment(toInput(comment));
-        super.onFailure(caught);
-        onSave.onFailure(caught);
-      }
-    };
-    CommentInfo input = toInput(comment);
-    if (wasNew) {
-      CommentApi.createDraft(psId, input, cb);
-    } else {
-      CommentApi.updateDraft(psId, input.id(), input, cb);
-    }
-  }
-
-  private void notifyDraftDelta(final int delta) {
-    CommentEditorContainer c = getContainer();
-    if (c != null) {
-      c.notifyDraftDelta(delta);
-    }
-  }
-
-  private void onDiscard() {
-    expandTimer.cancel();
-    if (isNew()) {
-      text.setFocus(false);
-      removeUI();
-      return;
-    }
-
-    text.setFocus(false);
-    text.setReadOnly(true);
-    save.setEnabled(false);
-    cancel.setEnabled(false);
-    discard.setEnabled(false);
-
-    CommentApi.deleteDraft(
-        comment.getKey().getParentKey().getParentKey(),
-        comment.getKey().get(),
-        new GerritCallback<JavaScriptObject>() {
-          @Override
-          public void onSuccess(JavaScriptObject result) {
-            notifyDraftDelta(-1);
-            removeUI();
-          }
-
-          @Override
-          public void onFailure(final Throwable caught) {
-            text.setReadOnly(false);
-            text.setFocus(true);
-            save.setEnabled(true);
-            cancel.setEnabled(true);
-            discard.setEnabled(true);
-            super.onFailure(caught);
-          }
-        });
-  }
-
-  private void removeUI() {
-    CommentEditorContainer c = getContainer();
-    if (c != null) {
-      c.remove(this);
-    }
-  }
-
-  private CommentEditorContainer getContainer() {
-    Widget p = getParent();
-    while (p != null) {
-      if (p instanceof CommentEditorContainer) {
-        return (CommentEditorContainer) p;
-      }
-      p = p.getParent();
-    }
-    return null;
-  }
-
-  public static CommentInfo toInput(PatchLineComment c) {
-    CommentInfo i = CommentInfo.createObject().cast();
-    i.id(c.getKey().get());
-    i.path(c.getKey().getParentKey().get());
-    i.side(c.getSide() == 0 ? Side.PARENT : Side.REVISION);
-    if (c.getLine() > 0) {
-      i.line(c.getLine());
-    }
-    i.inReplyTo(c.getParentUuid());
-    i.message(c.getMessage());
-    return i;
-  }
-
-  public static PatchLineComment toComment(PatchSet.Id ps,
-      String path,
-      CommentInfo i) {
-    PatchLineComment p = new PatchLineComment(
-        new PatchLineComment.Key(
-            new Patch.Key(ps, path),
-            i.id()),
-        i.line(),
-        Gerrit.getUserAccount().getId(),
-        i.inReplyTo(),
-        i.updated());
-    p.setMessage(i.message());
-    p.setSide((short) (i.side() == Side.PARENT ? 0 : 1));
-    return p;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommitMessageBlock.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommitMessageBlock.java
deleted file mode 100644
index f336382..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommitMessageBlock.java
+++ /dev/null
@@ -1,117 +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.client.patches;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.StarredChanges;
-import com.google.gerrit.client.changes.Util;
-import com.google.gerrit.client.ui.ChangeLink;
-import com.google.gerrit.client.ui.CommentLinkProcessor;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.dom.client.PreElement;
-import com.google.gwt.dom.client.Style.Display;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.HTMLPanel;
-import com.google.gwt.user.client.ui.SimplePanel;
-import com.google.gwtexpui.clippy.client.CopyableLabel;
-import com.google.gwtexpui.globalkey.client.KeyCommandSet;
-import com.google.gwtexpui.safehtml.client.SafeHtml;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-
-class CommitMessageBlock extends Composite {
-  interface Binder extends UiBinder<HTMLPanel, CommitMessageBlock> {}
-  private static final Binder uiBinder = GWT.create(Binder.class);
-
-  private KeyCommandSet keysAction;
-
-  @UiField
-  SimplePanel starPanel;
-  @UiField
-  FlowPanel permalinkPanel;
-  @UiField
-  PreElement commitSummaryPre;
-  @UiField
-  PreElement commitBodyPre;
-
-  CommitMessageBlock() {
-    initWidget(uiBinder.createAndBindUi(this));
-  }
-
-  CommitMessageBlock(KeyCommandSet keysAction) {
-    this.keysAction = keysAction;
-    initWidget(uiBinder.createAndBindUi(this));
-  }
-
-  void display(String commitMessage,
-      CommentLinkProcessor commentLinkProcessor) {
-    display(null, null, null, commitMessage, commentLinkProcessor);
-  }
-
-  void display(final PatchSet.Id patchSetId, final String revision,
-      Boolean starred, final String commitMessage,
-      CommentLinkProcessor commentLinkProcessor) {
-    starPanel.clear();
-    if (patchSetId != null && starred != null && Gerrit.isSignedIn()) {
-      Change.Id changeId = patchSetId.getParentKey();
-      StarredChanges.Icon star = StarredChanges.createIcon(changeId, starred);
-      star.setStyleName(Gerrit.RESOURCES.css().changeScreenStarIcon());
-      starPanel.add(star);
-
-      if (keysAction != null) {
-        keysAction.add(StarredChanges.newKeyCommand(star));
-      }
-    }
-
-    permalinkPanel.clear();
-    if (patchSetId != null && revision != null) {
-      final Change.Id changeId = patchSetId.getParentKey();
-      permalinkPanel.add(new ChangeLink(Util.C.changePermalink(), changeId));
-      permalinkPanel.add(new CopyableLabel(ChangeLink.permalink(changeId),
-          false));
-    }
-
-    String[] splitCommitMessage = commitMessage.split("\n", 2);
-
-    String commitSummary = splitCommitMessage[0];
-    String commitBody = "";
-    if (splitCommitMessage.length > 1) {
-      commitBody = splitCommitMessage[1];
-    }
-
-    // Linkify commit summary
-    SafeHtml commitSummaryLinkified = new SafeHtmlBuilder().append(commitSummary);
-    commitSummaryLinkified = commitSummaryLinkified.linkify();
-    commitSummaryLinkified = commentLinkProcessor.apply(commitSummaryLinkified);
-    commitSummaryPre.setInnerHTML(commitSummaryLinkified.asString());
-
-    // Hide commit body if there is no body
-    if (commitBody.trim().isEmpty()) {
-      commitBodyPre.getStyle().setDisplay(Display.NONE);
-    } else {
-      // Linkify commit body
-      SafeHtml commitBodyLinkified = new SafeHtmlBuilder().append(commitBody);
-      commitBodyLinkified = commitBodyLinkified.linkify();
-      commitBodyLinkified = commentLinkProcessor.apply(commitBodyLinkified);
-      commitBodyLinkified = commitBodyLinkified.replaceAll("\n\n", "<p></p>");
-      commitBodyLinkified = commitBodyLinkified.replaceAll("\n", "<br />");
-      commitBodyPre.setInnerHTML(commitBodyLinkified.asString());
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommitMessageBlock.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommitMessageBlock.ui.xml
deleted file mode 100644
index f1bf3de..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommitMessageBlock.ui.xml
+++ /dev/null
@@ -1,102 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-
-<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
-    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-
-
-  <ui:with field='res' type='com.google.gerrit.client.GerritResources'/>
-  <ui:style gss='false'>
-    @eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
-    @eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
-    @eval backgroundColor com.google.gerrit.client.Gerrit.getTheme().backgroundColor;
-
-    .commitMessageTable {
-      border-collapse: separate;
-      border-spacing: 0;
-      margin-bottom: 10px;
-    }
-
-    .header {
-      background-color: trimColor;
-      white-space: nowrap;
-      color: textColor;
-      font-size: 10pt;
-      font-style: italic;
-      padding: 2px 6px 1px;
-    }
-
-    .contents {
-      border-bottom: 1px solid trimColor;
-      border-left: 1px solid trimColor;
-      border-right: 1px solid trimColor;
-      padding: 5px;
-    }
-
-    .contents span {
-      font-weight: bold;
-    }
-
-    .contents pre {
-      margin: 0;
-    }
-
-    .commitSummary {
-      font-weight: bold;
-    }
-
-    .commitBody p {
-      padding-top: 0px;
-    }
-
-    .starPanel {
-      float: left;
-    }
-
-    .boxTitle {
-      float: left;
-      margin-right: 10px;
-    }
-
-    .permalinkPanel {
-      float: right;
-    }
-
-    .permalinkPanel a {
-      float: left;
-    }
-
-    .permalinkPanel div {
-      display: inline;
-    }
-  </ui:style>
-
-  <g:HTMLPanel>
-    <table class='{style.commitMessageTable}'>
-      <tr><td class='{style.header}'>
-        <g:SimplePanel styleName='{style.starPanel}' ui:field='starPanel'></g:SimplePanel>
-        <div class='{style.boxTitle}'>Commit Message</div>
-        <g:FlowPanel styleName='{style.permalinkPanel}' ui:field='permalinkPanel'></g:FlowPanel>
-      </td></tr>
-      <tr><td class='{style.contents}'>
-        <pre class='{style.commitSummary} {res.css.changeScreenDescription}' ui:field='commitSummaryPre'/>
-        <pre class='{style.commitBody} {res.css.changeScreenDescription}' ui:field='commitBodyPre'/>
-      </td></tr>
-    </table>
-  </g:HTMLPanel>
-</ui:UiBinder>
-
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/HistoryTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/HistoryTable.java
deleted file mode 100644
index c4fc1b0..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/HistoryTable.java
+++ /dev/null
@@ -1,155 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.patches;
-
-import com.google.gerrit.client.Dispatcher;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.Util;
-import com.google.gerrit.client.ui.FancyFlexTable;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gwt.user.client.DOM;
-import com.google.gwt.user.client.Event;
-import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
-import com.google.gwt.user.client.ui.HasHorizontalAlignment;
-import com.google.gwt.user.client.ui.RadioButton;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * A table used to specify which two patch sets should be diff'ed.
- */
-class HistoryTable extends FancyFlexTable<Patch> {
-  private final UnifiedPatchScreen screen;
-  final List<HistoryRadio> all = new ArrayList<>();
-
-  HistoryTable(final UnifiedPatchScreen parent) {
-    setStyleName(Gerrit.RESOURCES.css().patchHistoryTable());
-    screen = parent;
-    table.setWidth("auto");
-    table.addStyleName(Gerrit.RESOURCES.css().changeTable());
-  }
-
-  void onClick(final HistoryRadio b) {
-    PatchSet.Id sideA = screen.idSideA;
-    PatchSet.Id sideB = screen.idSideB;
-    switch (b.file) {
-      case 0:
-        sideA = b.patchSetId;
-        break;
-      case 1:
-        sideB = b.patchSetId;
-        break;
-      default:
-        return;
-    }
-    enableAll(false);
-    Patch.Key k = new Patch.Key(sideB, screen.getPatchKey().get());
-    Gerrit.display(Dispatcher.toUnified(sideA, k));
-  }
-
-  void enableAll(final boolean on) {
-    for (final HistoryRadio a : all) {
-      a.setEnabled(on);
-    }
-  }
-
-  void display(final List<Patch> result) {
-    all.clear();
-    final FlexCellFormatter fmt = table.getFlexCellFormatter();
-    table.setText(0, 0, PatchUtil.C.patchHeaderPatchSet());
-    fmt.setStyleName(0, 0, Gerrit.RESOURCES.css().dataHeader());
-    table.setText(1, 0, PatchUtil.C.patchHeaderOld());
-    fmt.setStyleName(1, 0, Gerrit.RESOURCES.css().dataHeader());
-    table.setText(2, 0, PatchUtil.C.patchHeaderNew());
-    fmt.setStyleName(2, 0, Gerrit.RESOURCES.css().dataHeader());
-    table.setText(3, 0, Util.C.patchTableColumnComments());
-    fmt.setStyleName(3, 0, Gerrit.RESOURCES.css().dataHeader());
-
-    if (screen.getPatchSetDetail().getInfo().getParents().size() > 1) {
-      table.setText(0, 1, PatchUtil.C.patchBaseAutoMerge());
-    } else {
-      table.setText(0, 1, PatchUtil.C.patchBase());
-    }
-    fmt.setStyleName(0, 1, Gerrit.RESOURCES.css().dataCell());
-    fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().topMostCell());
-    fmt.setStyleName(1, 1, Gerrit.RESOURCES.css().dataCell());
-    fmt.setStyleName(2, 1, Gerrit.RESOURCES.css().dataCell());
-    fmt.setStyleName(3, 1, Gerrit.RESOURCES.css().dataCell());
-
-    installRadio(1, 1, null, screen.idSideA, 0);
-
-    int col=2;
-    for (final Patch k : result) {
-      final PatchSet.Id psId = k.getKey().getParentKey();
-      table.setText(0, col, String.valueOf(psId.get()));
-      fmt.setStyleName(0, col, Gerrit.RESOURCES.css().patchHistoryTablePatchSetHeader());
-      fmt.addStyleName(0, col, Gerrit.RESOURCES.css().dataCell());
-      fmt.addStyleName(0, col, Gerrit.RESOURCES.css().topMostCell());
-
-      installRadio(1, col, psId, screen.idSideA, 0);
-      installRadio(2, col, psId, screen.idSideB, 1);
-
-      fmt.setStyleName(3, col, Gerrit.RESOURCES.css().dataCell());
-      if (k.getCommentCount() > 0) {
-        table.setText(3, col, Integer.toString(k.getCommentCount()));
-      }
-      col++;
-    }
-  }
-
-  private void installRadio(final int row, final int col, final PatchSet.Id psId,
-      final PatchSet.Id cur, final int file) {
-    final HistoryRadio b = new HistoryRadio(psId, file);
-    b.setValue(eq(cur, psId));
-
-    table.setWidget(row, col, b);
-    final FlexCellFormatter fmt = table.getFlexCellFormatter();
-    fmt.setHorizontalAlignment(row, col, HasHorizontalAlignment.ALIGN_CENTER);
-    fmt.setStyleName(row, col, Gerrit.RESOURCES.css().dataCell());
-    all.add(b);
-  }
-
-  private boolean eq(final PatchSet.Id cur, final PatchSet.Id psid) {
-    if (cur == null && psid == null) {
-      return true;
-    }
-    return psid != null && psid.equals(cur);
-  }
-
-  private class HistoryRadio extends RadioButton {
-    final PatchSet.Id patchSetId;
-    final int file;
-
-    HistoryRadio(final PatchSet.Id ps, final int f) {
-      super(String.valueOf(f));
-      sinkEvents(Event.ONCLICK);
-      patchSetId = ps;
-      file = f;
-    }
-
-    @Override
-    public void onBrowserEvent(final Event event) {
-      switch (DOM.eventGetType(event)) {
-        case Event.ONCLICK:
-          onClick(this);
-          break;
-        default:
-          super.onBrowserEvent(event);
-      }
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/NavLinks.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/NavLinks.java
deleted file mode 100644
index adfaa04..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/NavLinks.java
+++ /dev/null
@@ -1,135 +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.client.patches;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.Util;
-import com.google.gerrit.client.info.WebLinkInfo;
-import com.google.gerrit.client.ui.ChangeLink;
-import com.google.gerrit.client.ui.InlineHyperlink;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.Grid;
-import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
-import com.google.gwt.user.client.ui.HasHorizontalAlignment;
-import com.google.gwt.user.client.ui.Label;
-import com.google.gwtexpui.globalkey.client.KeyCommand;
-import com.google.gwtexpui.globalkey.client.KeyCommandSet;
-import com.google.gwtexpui.safehtml.client.SafeHtml;
-
-import java.util.List;
-
-class NavLinks extends Composite {
-  public enum Nav {
-    PREV (0, '[', PatchUtil.C.previousFileHelp(), 0),
-    NEXT (4, ']', PatchUtil.C.nextFileHelp(), 1);
-
-    public int col;      // Table Cell column to display link in
-    public int key;      // key code shortcut to activate link
-    public String help;  // help string for '?' popup
-    public int cmd;      // index into cmds array
-
-    Nav(int c, int k, String h, int i) {
-      this.col = c;
-      this.key = k;
-      this.help = h;
-      this.cmd = i;
-    }
-  }
-
-  private final PatchSet.Id patchSetId;
-  private final KeyCommandSet keys;
-  private final Grid table;
-
-  private KeyCommand[] cmds = new KeyCommand[2];
-
-  NavLinks(KeyCommandSet kcs, PatchSet.Id forPatch) {
-    patchSetId = forPatch;
-    keys = kcs;
-    table = new Grid(1, 5);
-    initWidget(table);
-
-    final CellFormatter fmt = table.getCellFormatter();
-    table.setStyleName(Gerrit.RESOURCES.css().sideBySideScreenLinkTable());
-    fmt.setHorizontalAlignment(0, 0, HasHorizontalAlignment.ALIGN_LEFT);
-    fmt.setHorizontalAlignment(0, 1, HasHorizontalAlignment.ALIGN_CENTER);
-    fmt.setHorizontalAlignment(0, 2, HasHorizontalAlignment.ALIGN_RIGHT);
-    fmt.setHorizontalAlignment(0, 3, HasHorizontalAlignment.ALIGN_RIGHT);
-    fmt.setHorizontalAlignment(0, 4, HasHorizontalAlignment.ALIGN_RIGHT);
-
-    final ChangeLink up = new ChangeLink("", patchSetId);
-    SafeHtml.set(up, SafeHtml.asis(Util.C.upToChangeIconLink()));
-    table.setWidget(0, 1, up);
-  }
-
-  void display(int patchIndex, PatchTable fileList,
-      List<InlineHyperlink> links, List<WebLinkInfo> webLinks) {
-    if (fileList != null) {
-      Label fileCountLabel =
-          new Label(Gerrit.M.fileCount(patchIndex + 1, fileList.size()));
-      fileCountLabel.setStyleName(Gerrit.RESOURCES.css().nowrap());
-      table.setWidget(0, 3, fileCountLabel);
-      setupNav(Nav.PREV, fileList.getPreviousPatchLink(patchIndex));
-      setupNav(Nav.NEXT, fileList.getNextPatchLink(patchIndex));
-    } else {
-      setupNav(Nav.PREV, null);
-      setupNav(Nav.NEXT, null);
-    }
-
-    FlowPanel linkPanel = new FlowPanel();
-    linkPanel.setStyleName(Gerrit.RESOURCES.css().linkPanel());
-    for (InlineHyperlink link : links) {
-      linkPanel.add(link);
-    }
-    for (WebLinkInfo webLink : webLinks) {
-      linkPanel.add(webLink.toAnchor());
-    }
-    table.setWidget(0, 2, linkPanel);
-  }
-
-  protected void setupNav(final Nav nav, final InlineHyperlink link) {
-
-    /* setup the cells */
-    if (link != null) {
-      link.addStyleName(Gerrit.RESOURCES.css().nowrap());
-      table.setWidget(0, nav.col, link);
-    } else {
-      table.clearCell(0, nav.col);
-    }
-
-    /* setup the keys */
-    if (keys != null) {
-
-      if (cmds[nav.cmd] != null) {
-        keys.remove(cmds[nav.cmd]);
-      }
-
-      if (link != null) {
-        cmds[nav.cmd] = new KeyCommand(0, nav.key, nav.help) {
-          @Override
-          public void onKeyPress(KeyPressEvent event) {
-            link.go();
-          }
-        };
-      } else {
-        cmds[nav.cmd] = new UpToChangeCommand(patchSetId, 0, nav.key);
-      }
-
-      keys.add(cmds[nav.cmd]);
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchBrowserPopup.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchBrowserPopup.java
deleted file mode 100644
index 2962fb1..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchBrowserPopup.java
+++ /dev/null
@@ -1,119 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.patches;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.Util;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gwt.event.logical.shared.ResizeEvent;
-import com.google.gwt.event.logical.shared.ResizeHandler;
-import com.google.gwt.event.shared.HandlerRegistration;
-import com.google.gwt.user.client.Command;
-import com.google.gwt.user.client.Window;
-import com.google.gwt.user.client.ui.DialogBox;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.PopupPanel.PositionCallback;
-import com.google.gwt.user.client.ui.ScrollPanel;
-import com.google.gwtexpui.globalkey.client.GlobalKey;
-import com.google.gwtexpui.globalkey.client.HidePopupPanelCommand;
-
-class PatchBrowserPopup extends DialogBox implements
-    PositionCallback, ResizeHandler {
-  private final Patch.Key callerKey;
-  private final PatchTable fileList;
-  private final ScrollPanel sp;
-  private HandlerRegistration regWindowResize;
-
-  PatchBrowserPopup(final Patch.Key pk, final PatchTable fl) {
-    super(true/* autohide */, false/* modal */);
-
-    callerKey = pk;
-    fileList = fl;
-    sp = new ScrollPanel(fileList);
-
-    final FlowPanel body = new FlowPanel();
-    body.setStyleName(Gerrit.RESOURCES.css().patchBrowserPopupBody());
-    body.add(sp);
-
-    setText(Util.M.patchSetHeader(callerKey.getParentKey().get()));
-    setWidget(body);
-    addStyleName(Gerrit.RESOURCES.css().patchBrowserPopup());
-  }
-
-  @Override
-  public void setPosition(final int myWidth, int myHeight) {
-    final int dLeft = (Window.getClientWidth() - myWidth) >> 1;
-    final int cHeight = Window.getClientHeight();
-    final int cHeight2 = 2 * cHeight / 3;
-    final int sLeft = Window.getScrollLeft();
-    final int sTop = Window.getScrollTop();
-
-    if (myHeight > cHeight2) {
-      sp.setHeight((cHeight2 - 50) + "px");
-      myHeight = getOffsetHeight();
-    }
-    setPopupPosition(sLeft + dLeft, (sTop + cHeight) - (myHeight + 10));
-  }
-
-  @Override
-  public void onResize(final ResizeEvent event) {
-    sp.setWidth((Window.getClientWidth() - 60) + "px");
-    setPosition(getOffsetWidth(), getOffsetHeight());
-  }
-
-  @Override
-  public void hide() {
-    if (regWindowResize != null) {
-      regWindowResize.removeHandler();
-      regWindowResize = null;
-    }
-    super.hide();
-  }
-
-  @Override
-  public void show() {
-    super.show();
-    if (regWindowResize == null) {
-      regWindowResize = Window.addResizeHandler(this);
-    }
-
-    GlobalKey.dialog(this);
-    GlobalKey.addApplication(this, new HidePopupPanelCommand(0, 'f', this));
-
-    if (!fileList.isLoaded()) {
-      fileList.onTableLoaded(new Command() {
-        @Override
-        public void execute() {
-          sp.setHeight("");
-          setPosition(getOffsetWidth(), getOffsetHeight());
-          fileList.setRegisterKeys(true);
-          fileList.movePointerTo(callerKey);
-        }
-      });
-    }
-  }
-
-  public void open() {
-    if (!fileList.isLoaded()) {
-      sp.setHeight("22px");
-    }
-    sp.setWidth((Window.getClientWidth() - 60) + "px");
-    setPopupPositionAndShow(this);
-    if (fileList.isLoaded()) {
-      fileList.setRegisterKeys(true);
-      fileList.movePointerTo(callerKey);
-    }
-  }
-}
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 422a4dd..b199169 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
@@ -17,49 +17,25 @@
 import com.google.gwt.i18n.client.Constants;
 
 public interface PatchConstants extends Constants {
-  String draft();
-
-  String buttonReply();
-  String buttonEdit();
-  String buttonSave();
-  String buttonCancel();
-  String buttonDiscard();
-
-  String noDifference();
   String patchBase();
-  String patchBaseAutoMerge();
-  String patchHeaderPatchSet();
-  String patchHeaderOld();
-  String patchHeaderNew();
   String patchSet();
 
-  String patchHistoryTitle();
-  String disabledOnLargeFiles();
-  String intralineFailure();
-  String intralineTimeout();
-  String illegalNumberOfColumns();
-
   String upToChange();
   String openReply();
   String linePrev();
   String lineNext();
   String chunkPrev();
   String chunkNext();
-  String chunkPrev2();
-  String chunkNext2();
   String commentPrev();
   String commentNext();
   String focusSideA();
   String focusSideB();
-  String fileList();
   String expandComment();
   String expandAllCommentsOnCurrentLine();
   String toggleSideA();
   String toggleIntraline();
   String showPreferences();
 
-  String openEditScreen();
-
   String toggleReviewed();
   String markAsReviewedAndGoToNext();
 
@@ -76,22 +52,13 @@
   String previousFileHelp();
   String nextFileHelp();
 
-  String reviewedAnd();
-  String next();
   String download();
   String edit();
+  String blame();
   String addFileCommentToolTip();
-  String addFileCommentByDoubleClick();
 
-  String buttonReplyDone();
   String cannedReplyDone();
 
-  String fileTypeSymlink();
-  String fileTypeGitlink();
-
-  String patchSkipRegionStart();
-  String patchSkipRegionEnd();
-
   String sideBySideDiff();
   String unifiedDiff();
 }
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 aa6177b..13f0afa 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
@@ -1,46 +1,24 @@
-draft = (Draft)
-
-buttonReply = Reply ...
-buttonReplyDone = Reply 'Done'
 cannedReplyDone = Done
-buttonEdit = Edit
-buttonSave = Save
-buttonCancel = Cancel
-buttonDiscard = Discard
 
-noDifference = No Differences
 patchBase = Base
-patchBaseAutoMerge = Auto Merge
-patchHeaderPatchSet = Patch Set
-patchHeaderOld = Old Version
-patchHeaderNew = New Version
-patchHistoryTitle = Patch History
 patchSet = Patch Set
-disabledOnLargeFiles = Disabled on very large source files.
-intralineFailure = Intraline difference not available due to server error.
-intralineTimeout = Intraline difference not available due to timeout.
-illegalNumberOfColumns = The number of columns cannot be zero or negative
 
 upToChange = Up to change
 openReply = Reply and score
 linePrev = Previous line
 lineNext = Next line
-chunkPrev = Previous diff chunk or comment
-chunkNext = Next diff chunk or comment
-chunkPrev2 = Previous diff chunk
-chunkNext2 = Next diff chunk or search result
+chunkPrev = Previous diff chunk
+chunkNext = 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
 
-openEditScreen = Edit file in browser
 toggleReviewed = Toggle the reviewed flag
 markAsReviewedAndGoToNext = Mark patch as reviewed and go to next unreviewed patch
 
@@ -57,18 +35,10 @@
 previousFileHelp = Previous file
 nextFileHelp = Next file
 
-reviewedAnd = Reviewed &
-next = next
 download = Download
 edit = Edit
+blame = Blame
 addFileCommentToolTip = Click to add file comment
-addFileCommentByDoubleClick = Double click to add file comment
-
-fileTypeSymlink = Type: Symbolic Link
-fileTypeGitlink = Type: Git Commit in Subproject
-
-patchSkipRegionStart = ... skipped
-patchSkipRegionEnd = common lines ...
 
 sideBySideDiff = Side-by-side diff
 unifiedDiff = Unified diff
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchLine.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchLine.java
deleted file mode 100644
index 4863af2..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchLine.java
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.patches;
-
-class PatchLine {
-  static enum Type {
-    DELETE, INSERT, REPLACE, CONTEXT
-  }
-
-  private PatchLine.Type type;
-  private int lineA;
-  private int lineB;
-
-  PatchLine(final PatchLine.Type t, final int a, final int b) {
-    type = t;
-    lineA = a;
-    lineB = b;
-  }
-
-  PatchLine.Type getType() {
-    return type;
-  }
-
-  int getLineA() {
-    return lineA;
-  }
-
-  int getLineB() {
-    return lineB;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages.java
index 822eff7..aaab1c9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages.java
@@ -16,12 +16,9 @@
 
 import com.google.gwt.i18n.client.Messages;
 
-import java.util.Date;
-
 public interface PatchMessages extends Messages {
   String expandBefore(int cnt);
   String expandAfter(int cnt);
-  String draftSaved(Date when);
   String patchSkipRegion(String lineNumber);
   String fileNameWithShortcutKey(String file, String key);
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages.properties
index fbb7d08..8dcebdc 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages.properties
@@ -1,5 +1,4 @@
 expandBefore = +{0}&#x21e7;
 expandAfter = +{0}&#x21e9;
-draftSaved = Draft saved at {0,time,short}
 patchSkipRegion = ... skipped {0} common lines ...
 fileNameWithShortcutKey = {0} (Shortcut: {1})
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages_en.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages_en.properties
index e24333e..8dcebdc 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages_en.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages_en.properties
@@ -1,3 +1,4 @@
 expandBefore = +{0}&#x21e7;
 expandAfter = +{0}&#x21e9;
 patchSkipRegion = ... skipped {0} common lines ...
+fileNameWithShortcutKey = {0} (Shortcut: {1})
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java
deleted file mode 100644
index b7ba64b..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java
+++ /dev/null
@@ -1,347 +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.client.patches;
-
-import com.google.gerrit.client.ErrorDialog;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.account.Util;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.ListenableAccountDiffPreference;
-import com.google.gerrit.client.ui.NpIntTextBox;
-import com.google.gerrit.extensions.client.DiffPreferencesInfo;
-import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.dom.client.NodeList;
-import com.google.gwt.dom.client.OptionElement;
-import com.google.gwt.dom.client.SelectElement;
-import com.google.gwt.event.dom.client.ClickEvent;
-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.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.uibinder.client.UiHandler;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.CheckBox;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.FocusWidget;
-import com.google.gwt.user.client.ui.HasWidgets;
-import com.google.gwt.user.client.ui.ListBox;
-import com.google.gwt.user.client.ui.Widget;
-import com.google.gwtjsonrpc.common.VoidResult;
-
-public class PatchScriptSettingsPanel extends Composite {
-  private static MyUiBinder uiBinder = GWT.create(MyUiBinder.class);
-
-  interface MyUiBinder extends UiBinder<Widget, PatchScriptSettingsPanel> {
-  }
-
-  private final ListenableAccountDiffPreference listenablePrefs;
-  private boolean enableIntralineDifference = true;
-  private boolean enableSmallFileFeatures = true;
-
-  @UiField
-  ListBox ignoreWhitespace;
-
-  @UiField
-  NpIntTextBox tabWidth;
-
-  @UiField
-  NpIntTextBox colWidth;
-
-  @UiField
-  CheckBox syntaxHighlighting;
-
-  @UiField
-  CheckBox intralineDifference;
-
-  @UiField
-  ListBox context;
-
-  @UiField
-  CheckBox whitespaceErrors;
-
-  @UiField
-  CheckBox showLineEndings;
-
-  @UiField
-  CheckBox showTabs;
-
-  @UiField
-  CheckBox manualReview;
-
-  @UiField
-  CheckBox skipDeleted;
-
-  @UiField
-  CheckBox skipUncommented;
-
-  @UiField
-  CheckBox expandAllComments;
-
-  @UiField
-  CheckBox retainHeader;
-
-  @UiField
-  Button update;
-
-  @UiField
-  Button save;
-
-  /**
-   * Counts +1 for every setEnabled(true) and -1 for every setEnabled(false)
-   *
-   * The purpose is to prevent enabling widgets too early. It might happen that
-   * setEnabled(false) is called from this class and from an event handler
-   * of ValueChangeEvent in another class. The first setEnabled(true) would then
-   * enable widgets too early i.e. before the second setEnabled(true) is called.
-   *
-   * With this counter the setEnabled(true) will enable widgets only when
-   * setEnabledCounter == 0. Until it is less than zero setEnabled(true) will
-   * not enable the widgets.
-   */
-  private int setEnabledCounter;
-
-  public PatchScriptSettingsPanel(ListenableAccountDiffPreference prefs) {
-    listenablePrefs = prefs;
-    initWidget(uiBinder.createAndBindUi(this));
-    initIgnoreWhitespace(ignoreWhitespace);
-    initContext(context);
-    if (!Gerrit.isSignedIn()) {
-      save.setVisible(false);
-    }
-
-    KeyPressHandler onEnter = new KeyPressHandler() {
-      @Override
-      public void onKeyPress(KeyPressEvent event) {
-        if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
-          save();
-        }
-      }
-    };
-    tabWidth.addKeyPressHandler(onEnter);
-    colWidth.addKeyPressHandler(onEnter);
-
-    display();
-  }
-
-  public void setEnabled(final boolean on) {
-    if (on) {
-      setEnabledCounter++;
-    } else {
-      setEnabledCounter--;
-    }
-    if (on && setEnabledCounter == 0 || !on) {
-      for (Widget w : (HasWidgets) getWidget()) {
-        if (w instanceof FocusWidget) {
-          ((FocusWidget) w).setEnabled(on);
-        }
-      }
-      toggleEnabledStatus(on);
-    }
-  }
-
-  public void setEnableSmallFileFeatures(final boolean on) {
-    enableSmallFileFeatures = on;
-    if (enableSmallFileFeatures) {
-      syntaxHighlighting.setValue(getValue().syntaxHighlighting);
-    } else {
-      syntaxHighlighting.setValue(false);
-    }
-
-    NodeList<OptionElement> options =
-        context.getElement().<SelectElement>cast().getOptions();
-    // WHOLE_FILE_CONTEXT is the last option in the list.
-    int lastIndex = options.getLength() - 1;
-    OptionElement currOption = options.getItem(lastIndex);
-    if (enableSmallFileFeatures) {
-      currOption.setDisabled(false);
-    } else {
-      currOption.setDisabled(true);
-      if (context.getSelectedIndex() == lastIndex) {
-        // Select the next longest context from WHOLE_FILE_CONTEXT
-        context.setSelectedIndex(lastIndex - 1);
-      }
-    }
-    toggleEnabledStatus(save.isEnabled());
-  }
-
-  public void setEnableIntralineDifference(final boolean on) {
-    enableIntralineDifference = on;
-    if (enableIntralineDifference) {
-      intralineDifference.setValue(getValue().intralineDifference);
-    } else {
-      intralineDifference.setValue(false);
-    }
-    toggleEnabledStatus(save.isEnabled());
-  }
-
-  private void toggleEnabledStatus(final boolean on) {
-    intralineDifference.setEnabled(on & enableIntralineDifference);
-    syntaxHighlighting.setEnabled(on & enableSmallFileFeatures);
-
-    final String title =
-        enableSmallFileFeatures ? null : PatchUtil.C.disabledOnLargeFiles();
-    syntaxHighlighting.setTitle(title);
-  }
-
-  public DiffPreferencesInfo getValue() {
-    return listenablePrefs.get();
-  }
-
-  public void setValue(final DiffPreferencesInfo dp) {
-    listenablePrefs.set(dp);
-    display();
-  }
-
-  protected void display() {
-    final DiffPreferencesInfo dp = getValue();
-    setIgnoreWhitespace(dp.ignoreWhitespace);
-    if (enableSmallFileFeatures) {
-      syntaxHighlighting.setValue(dp.syntaxHighlighting);
-    } else {
-      syntaxHighlighting.setValue(false);
-    }
-    setContext(dp.context);
-
-    tabWidth.setIntValue(dp.tabSize);
-    colWidth.setIntValue(dp.lineLength);
-    intralineDifference.setValue(dp.intralineDifference);
-    whitespaceErrors.setValue(dp.showWhitespaceErrors);
-    showLineEndings.setValue(dp.showLineEndings);
-    showTabs.setValue(dp.showTabs);
-    skipDeleted.setValue(dp.skipDeleted);
-    skipUncommented.setValue(dp.skipUncommented);
-    expandAllComments.setValue(dp.expandAllComments);
-    retainHeader.setValue(dp.retainHeader);
-    manualReview.setValue(dp.manualReview);
-  }
-
-  @UiHandler("update")
-  void onUpdate(@SuppressWarnings("unused") ClickEvent event) {
-    update();
-  }
-
-  @UiHandler("save")
-  void onSave(@SuppressWarnings("unused") ClickEvent event) {
-    save();
-  }
-
-  private void update() {
-    if (colWidth.getIntValue() <= 0) {
-      new ErrorDialog(PatchUtil.C.illegalNumberOfColumns()).center();
-      return;
-    }
-    DiffPreferencesInfo dp = getValue();
-    dp.ignoreWhitespace = getIgnoreWhitespace();
-    dp.context = getContext();
-    dp.tabSize = tabWidth.getIntValue();
-    dp.lineLength = colWidth.getIntValue();
-    dp.syntaxHighlighting = syntaxHighlighting.getValue();
-    dp.intralineDifference = intralineDifference.getValue();
-    dp.showWhitespaceErrors = whitespaceErrors.getValue();
-    dp.showLineEndings = showLineEndings.getValue();
-    dp.showTabs = showTabs.getValue();
-    dp.skipDeleted = skipDeleted.getValue();
-    dp.skipUncommented = skipUncommented.getValue();
-    dp.expandAllComments = expandAllComments.getValue();
-    dp.retainHeader = retainHeader.getValue();
-    dp.manualReview = manualReview.getValue();
-
-    listenablePrefs.set(dp);
-  }
-
-  private void save() {
-    update();
-    if (Gerrit.isSignedIn()) {
-      persistDiffPreferences();
-    }
-  }
-
-  private void persistDiffPreferences() {
-    setEnabled(false);
-    listenablePrefs.save(new GerritCallback<VoidResult>() {
-      @Override
-      public void onSuccess(VoidResult result) {
-        setEnabled(true);
-      }
-
-      @Override
-      public void onFailure(Throwable caught) {
-        setEnabled(true);
-      }
-    });
-  }
-
-  private void initIgnoreWhitespace(ListBox ws) {
-    ws.addItem(PatchUtil.C.whitespaceIGNORE_NONE(), //
-        Whitespace.IGNORE_NONE.name());
-    ws.addItem(PatchUtil.C.whitespaceIGNORE_TRAILING(), //
-        Whitespace.IGNORE_TRAILING.name());
-    ws.addItem(PatchUtil.C.whitespaceIGNORE_LEADING_AND_TRAILING(), //
-        Whitespace.IGNORE_LEADING_AND_TRAILING.name());
-    ws.addItem(PatchUtil.C.whitespaceIGNORE_ALL(), //
-        Whitespace.IGNORE_ALL.name());
-  }
-
-  private void initContext(ListBox context) {
-    for (final short v : DiffPreferencesInfo.CONTEXT_CHOICES) {
-      final String label;
-      if (v == DiffPreferencesInfo.WHOLE_FILE_CONTEXT) {
-        label = Util.C.contextWholeFile();
-      } else {
-        label = Util.M.lines(v);
-      }
-      context.addItem(label, String.valueOf(v));
-    }
-  }
-
-  private Whitespace getIgnoreWhitespace() {
-    final int sel = ignoreWhitespace.getSelectedIndex();
-    if (0 <= sel) {
-      return Whitespace.valueOf(ignoreWhitespace.getValue(sel));
-    }
-    return getValue().ignoreWhitespace;
-  }
-
-  private void setIgnoreWhitespace(Whitespace s) {
-    for (int i = 0; i < ignoreWhitespace.getItemCount(); i++) {
-      if (ignoreWhitespace.getValue(i).equals(s.name())) {
-        ignoreWhitespace.setSelectedIndex(i);
-        return;
-      }
-    }
-    ignoreWhitespace.setSelectedIndex(0);
-  }
-
-  private int getContext() {
-    final int sel = context.getSelectedIndex();
-    if (0 <= sel) {
-      return Short.parseShort(context.getValue(sel));
-    }
-    return getValue().context;
-  }
-
-  private void setContext(int ctx) {
-    String v = String.valueOf(ctx);
-    for (int i = 0; i < context.getItemCount(); i++) {
-      if (context.getValue(i).equals(v)) {
-        context.setSelectedIndex(i);
-        return;
-      }
-    }
-    context.setSelectedIndex(0);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.ui.xml
deleted file mode 100644
index 5164302..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.ui.xml
+++ /dev/null
@@ -1,209 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<ui:UiBinder
-  xmlns:ui='urn:ui:com.google.gwt.uibinder'
-  xmlns:g='urn:import:com.google.gwt.user.client.ui'
-  xmlns:my='urn:import:com.google.gerrit.client.ui'
-  ui:generateFormat='com.google.gwt.i18n.rebind.format.PropertiesFormat'
-  ui:generateKeys='com.google.gwt.i18n.rebind.keygen.MD5KeyGenerator'
-  ui:generateLocales='default,en'
-  >
-<ui:style gss='false'>
-  @external .gwt-TextBox;
-  @external .gwt-ListBox;
-
-  @def fontSize 8pt;
-
-  .controls {
-    border: none;
-    border-collapse: separate;
-    border-spacing: 0;
-  }
-
-  .controls td {
-    font-size: fontSize;
-    padding: 0;
-    white-space: nowrap;
-  }
-
-  .controls .gwt-TextBox {
-    font-size: fontSize;
-    padding: 0;
-    text-align: right;
-  }
-
-  .controls .gwt-ListBox {
-    font-size: fontSize;
-    padding: 0;
-    margin-right: 1em;
-  }
-
-  .updateButton {
-    margin-left: 1em;
-    margin-right: 1em;
-    font-size: fontSize;
-  }
-</ui:style>
-
-<g:HTMLPanel>
-<table class='{style.controls}'>
-  <tr valign='top'>
-    <ui:msg>
-      <td align='right'>Ignore Whitespace:</td>
-      <td align='right'>
-        <g:ListBox
-          ui:field='ignoreWhitespace'
-          visibleItemCount='1'
-          tabIndex='1'/>
-      </td>
-    </ui:msg>
-
-    <td align='right'>
-      <ui:msg>Tab Width:
-      <my:NpIntTextBox
-        ui:field='tabWidth'
-        width='2em'
-        visibleLength='2'
-        maxLength='2'
-        tabIndex='3'/>
-      </ui:msg>
-    </td>
-
-    <td rowspan='2'>
-      <g:CheckBox
-          ui:field='syntaxHighlighting'
-          text='Syntax Coloring'
-          tabIndex='5'>
-        <ui:attribute name='text'/>
-      </g:CheckBox>
-      <br/>
-      <g:CheckBox
-          ui:field='intralineDifference'
-          text='Intraline Difference'
-          tabIndex='6'>
-        <ui:attribute name='text'/>
-      </g:CheckBox>
-    </td>
-
-    <td rowspan='2'>
-      <g:CheckBox
-          ui:field='whitespaceErrors'
-          text='Whitespace Errors'
-          tabIndex='7'>
-        <ui:attribute name='text'/>
-      </g:CheckBox>
-      <br/>
-      <g:CheckBox
-          ui:field='showLineEndings'
-          text='Show Line Endings'
-          tabIndex='8'>
-        <ui:attribute name='text'/>
-      </g:CheckBox>
-    </td>
-
-    <td rowspan='2'>
-      <g:CheckBox
-          ui:field='showTabs'
-          text='Show Tabs'
-          tabIndex='9'>
-        <ui:attribute name='text'/>
-      </g:CheckBox>
-      <br/>
-      <g:CheckBox
-          ui:field='expandAllComments'
-          text='Expand All Comments'
-          tabIndex='10'>
-        <ui:attribute name='text'/>
-      </g:CheckBox>
-    </td>
-
-    <td rowspan='2'>
-      <g:CheckBox
-          ui:field='retainHeader'
-          text='Retain Header On File Switch'
-          tabIndex='11'>
-        <ui:attribute name='text'/>
-      </g:CheckBox>
-      <br/>
-      <g:CheckBox
-          ui:field='skipUncommented'
-          text='Skip Uncommented Files'
-          tabIndex='12'>
-        <ui:attribute name='text'/>
-      </g:CheckBox>
-    </td>
-
-    <td valign='bottom' rowspan='2'>
-      <g:CheckBox
-          ui:field='skipDeleted'
-          text='Skip Deleted Files'
-          tabIndex='13'>
-        <ui:attribute name='text'/>
-      </g:CheckBox>
-      <br/>
-      <g:CheckBox
-          ui:field='manualReview'
-          text='Manual Review'
-          tabIndex='14'>
-        <ui:attribute name='text'/>
-      </g:CheckBox>
-    </td>
-
-    <td rowspan='2'>
-      <br/>
-      <g:Button
-          ui:field='update'
-          text='Update'
-          styleName='{style.updateButton}'
-          tabIndex='15'>
-        <ui:attribute name='text'/>
-      </g:Button>
-      <g:Button
-          ui:field='save'
-          text='Save'
-          styleName='{style.updateButton}'
-          tabIndex='16'>
-        <ui:attribute name='text'/>
-      </g:Button>
-    </td>
-  </tr>
-
-  <tr valign='top'>
-    <ui:msg>
-      <td align='right'>Context:</td>
-      <td align='right'>
-        <g:ListBox
-            ui:field='context'
-            visibleItemCount='1'
-            tabIndex='2'/>
-      </td>
-    </ui:msg>
-
-    <td align='right'>
-      <ui:msg>Columns:
-      <my:NpIntTextBox
-        ui:field='colWidth'
-        width='2.5em'
-        visibleLength='3'
-        maxLength='3'
-        tabIndex='4'/>
-      </ui:msg>
-    </td>
-  </tr>
-</table>
-</g:HTMLPanel>
-</ui:UiBinder>
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
deleted file mode 100644
index 6762383..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.java
+++ /dev/null
@@ -1,188 +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.client.patches;
-
-import com.google.gerrit.client.Dispatcher;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.common.data.PatchScript;
-import com.google.gerrit.common.data.PatchSetDetail;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.DoubleClickEvent;
-import com.google.gwt.event.dom.client.DoubleClickHandler;
-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.user.client.DOM;
-import com.google.gwt.user.client.Event;
-import com.google.gwt.user.client.ui.Anchor;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.HTMLPanel;
-import com.google.gwt.user.client.ui.Image;
-import com.google.gwt.user.client.ui.Label;
-import com.google.gwtorm.client.KeyUtil;
-
-import java.util.HashMap;
-import java.util.Map;
-
-public class PatchSetSelectBox extends Composite {
-  interface Binder extends UiBinder<HTMLPanel, PatchSetSelectBox> {}
-  private static final Binder uiBinder = GWT.create(Binder.class);
-
-  interface BoxStyle extends CssResource {
-    String selected();
-    String hidden();
-    String sideMarker();
-    String patchSetLabel();
-  }
-
-  public enum Side {
-    A, B
-  }
-
-  PatchScript script;
-  Patch.Key patchKey;
-  PatchSet.Id idSideA;
-  PatchSet.Id idSideB;
-  PatchSet.Id idActive;
-  Side side;
-  Map<Integer, Anchor> links;
-  private Label patchSet;
-
-  @UiField
-  HTMLPanel linkPanel;
-
-  @UiField
-  BoxStyle style;
-
-  public PatchSetSelectBox(Side side) {
-    this.side = side;
-
-    initWidget(uiBinder.createAndBindUi(this));
-  }
-
-  public void display(final PatchSetDetail detail, final PatchScript script,
-      Patch.Key key, PatchSet.Id idSideA, PatchSet.Id idSideB) {
-    this.script = script;
-    this.patchKey = key;
-    this.idSideA = idSideA;
-    this.idSideB = idSideB;
-    this.idActive = (side == Side.A) ? idSideA : idSideB;
-    this.links = new HashMap<>();
-
-    linkPanel.clear();
-
-    if (isFileOrCommitMessage()) {
-      linkPanel.setTitle(PatchUtil.C.addFileCommentByDoubleClick());
-    }
-
-    patchSet = new Label(PatchUtil.C.patchSet());
-    patchSet.addStyleName(style.patchSetLabel());
-    linkPanel.add(patchSet);
-
-    Label sideMarker = new Label((side == Side.A) ? "(-)" : "(+)");
-    sideMarker.addStyleName(style.sideMarker());
-    linkPanel.add(sideMarker);
-
-    Anchor baseLink;
-    if (detail.getInfo().getParents().size() > 1) {
-      baseLink = createLink(PatchUtil.C.patchBaseAutoMerge(), null);
-    } else {
-      baseLink = createLink(PatchUtil.C.patchBase(), null);
-    }
-
-    links.put(0, baseLink);
-    linkPanel.add(baseLink);
-
-    if (side == Side.B) {
-      links.get(0).setStyleName(style.hidden());
-    }
-
-    for (Patch patch : script.getHistory()) {
-      PatchSet.Id psId = patch.getKey().getParentKey();
-      Anchor anchor = createLink(psId.getId(), psId);
-      links.put(psId.get(), anchor);
-      linkPanel.add(anchor);
-    }
-
-    if (idActive == null && side == Side.A) {
-      links.get(0).setStyleName(style.selected());
-    } else if (idActive != null) {
-      links.get(idActive.get()).setStyleName(style.selected());
-    }
-
-    Anchor downloadLink = createDownloadLink();
-    if (downloadLink != null) {
-      linkPanel.add(downloadLink);
-    }
-  }
-
-  public void addDoubleClickHandler(DoubleClickHandler handler) {
-    linkPanel.sinkEvents(Event.ONDBLCLICK);
-    linkPanel.addHandler(handler, DoubleClickEvent.getType());
-    patchSet.addDoubleClickHandler(handler);
-  }
-
-  private Anchor createLink(String label, final PatchSet.Id id) {
-    final Anchor anchor = new Anchor(label);
-    anchor.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(ClickEvent event) {
-        if (side == Side.A) {
-          idSideA = id;
-        } else {
-          idSideB = id;
-        }
-
-        Patch.Key keySideB = new Patch.Key(idSideB, patchKey.get());
-        Gerrit.display(Dispatcher.toUnified(idSideA, keySideB));
-      }
-    });
-    return anchor;
-  }
-
-  public boolean isFileOrCommitMessage() {
-    return !((side == Side.A && 0 >= script.getA().size()) || //
-    (side == Side.B && 0 >= script.getB().size()));
-  }
-
-  private Anchor createDownloadLink() {
-    boolean isCommitMessage = Patch.COMMIT_MSG.equals(script.getNewName());
-    if (isCommitMessage || //
-        (side == Side.A && 0 >= script.getA().size()) || //
-        (side == Side.B && 0 >= script.getB().size())) {
-      return null;
-    }
-
-    Patch.Key key = (idActive == null) ? //
-        patchKey : (new Patch.Key(idActive, patchKey.get()));
-
-    String sideURL = (idActive == null) ? "1" : "0";
-    final String base = GWT.getHostPageBaseURL() + "cat/";
-
-    Image image = new Image(Gerrit.RESOURCES.downloadIcon());
-
-    final Anchor anchor = new Anchor();
-    anchor.setHref(base + KeyUtil.encode(key.toString()) + "^" + sideURL);
-    anchor.setTitle(PatchUtil.C.download());
-    DOM.insertBefore(anchor.getElement(), image.getElement(),
-        DOM.getFirstChild(anchor.getElement()));
-
-    return anchor;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.ui.xml
deleted file mode 100644
index 8977876..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.ui.xml
+++ /dev/null
@@ -1,67 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-
-<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
-    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-  <ui:with field='res' type='com.google.gerrit.client.GerritResources'/>
-  <ui:style gss='false' type='com.google.gerrit.client.patches.PatchSetSelectBox.BoxStyle'>
-    @eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
-    @eval backgroundColor com.google.gerrit.client.Gerrit.getTheme().backgroundColor;
-
-    .linkPanel {
-      font-size: 12px;
-      white-space: normal;
-    }
-
-    .linkPanel > div {
-      padding-left: 3px;
-      padding-right: 3px;
-      vertical-align: middle;
-      display: inline-block;
-    }
-
-    .patchSetLabel {
-      font-weight: bold;
-    }
-
-    .sideMarker {
-      font-family: monospace;
-    }
-
-    .linkPanel > a {
-      padding-left: 3px;
-      padding-right: 3px;
-      text-decoration: none;
-      vertical-align: middle;
-      display: inline-block;
-    }
-
-    .selected {
-      font-weight: bold;
-      background-color: selectionColor;
-    }
-
-    .hidden {
-      visibility: hidden;
-    }
-  </ui:style>
-
-  <g:HTMLPanel>
-    <g:HTMLPanel styleName='{style.linkPanel}' ui:field='linkPanel'/>
-  </g:HTMLPanel>
-</ui:UiBinder>
-
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTable.java
deleted file mode 100644
index 4f2466f..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTable.java
+++ /dev/null
@@ -1,901 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.patches;
-
-import com.google.gerrit.client.Dispatcher;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.Util;
-import com.google.gerrit.client.ui.InlineHyperlink;
-import com.google.gerrit.client.ui.ListenableAccountDiffPreference;
-import com.google.gerrit.client.ui.NavigationTable;
-import com.google.gerrit.client.ui.PatchLink;
-import com.google.gerrit.common.data.PatchSetDetail;
-import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DiffView;
-import com.google.gerrit.reviewdb.client.Patch;
-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.gwt.core.client.Scheduler;
-import com.google.gwt.core.client.Scheduler.RepeatingCommand;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.user.client.Command;
-import com.google.gwt.user.client.Window;
-import com.google.gwt.user.client.ui.Anchor;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.HTMLTable.Cell;
-import com.google.gwt.user.client.ui.Image;
-import com.google.gwt.user.client.ui.Label;
-import com.google.gwt.user.client.ui.Widget;
-import com.google.gwtexpui.globalkey.client.KeyCommand;
-import com.google.gwtexpui.progress.client.ProgressBar;
-import com.google.gwtexpui.safehtml.client.SafeHtml;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-class PatchTable extends Composite {
-  interface PatchValidator {
-    /**
-     * @param patch
-     * @return true if patch is valid.
-     */
-    boolean isValid(Patch patch);
-  }
-
-  final PatchValidator PREFERENCE_VALIDATOR =
-      new PatchValidator() {
-        @Override
-        public boolean isValid(Patch patch) {
-          return !((listenablePrefs.get().skipDeleted
-              && patch.getChangeType().equals(ChangeType.DELETED))
-              || (listenablePrefs.get().skipUncommented
-              && patch.getCommentCount() == 0));
-        }
-
-      };
-
-  private final FlowPanel myBody;
-  private PatchSetDetail detail;
-  private Command onLoadCommand;
-  private MyTable myTable;
-  private String savePointerId;
-  private PatchSet.Id base;
-  private List<Patch> patchList;
-  private Map<Patch.Key, Integer> patchMap;
-  private ListenableAccountDiffPreference listenablePrefs;
-
-  private List<ClickHandler> clickHandlers;
-  private boolean active;
-  private boolean registerKeys;
-
-  PatchTable(ListenableAccountDiffPreference prefs) {
-    listenablePrefs = prefs;
-    myBody = new FlowPanel();
-    initWidget(myBody);
-  }
-
-  PatchTable() {
-    this(new ListenableAccountDiffPreference());
-  }
-
-  int indexOf(Patch.Key patch) {
-    Integer i = patchMap().get(patch);
-    return i != null ? i : -1;
-  }
-
-  int size() {
-    return patchMap.size();
-  }
-
-  private Map<Patch.Key, Integer> patchMap() {
-    if (patchMap == null) {
-      patchMap = new HashMap<>();
-      for (int i = 0; i < patchList.size(); i++) {
-        patchMap.put(patchList.get(i).getKey(), i);
-      }
-    }
-    return patchMap;
-  }
-
-  void display(PatchSet.Id base, PatchSetDetail detail) {
-    this.base = base;
-    this.detail = detail;
-    this.patchList = detail.getPatches();
-    this.patchMap = null;
-    myTable = null;
-
-    final DisplayCommand cmd = new DisplayCommand(patchList, base);
-    if (cmd.execute()) {
-      cmd.initMeter();
-      Scheduler.get().scheduleIncremental(cmd);
-    } else {
-      cmd.showTable();
-    }
-  }
-
-  PatchSet.Id getBase() {
-    return base;
-  }
-
-  void setSavePointerId(final String id) {
-    savePointerId = id;
-  }
-
-  boolean isLoaded() {
-    return myTable != null;
-  }
-
-  void onTableLoaded(final Command cmd) {
-    if (myTable != null) {
-      cmd.execute();
-    } else {
-      onLoadCommand = cmd;
-    }
-  }
-
-  void addClickHandler(final ClickHandler clickHandler) {
-    if (myTable != null) {
-      myTable.addClickHandler(clickHandler);
-    } else {
-      if (clickHandlers == null) {
-        clickHandlers = new ArrayList<>(2);
-      }
-      clickHandlers.add(clickHandler);
-    }
-  }
-
-  void setRegisterKeys(final boolean on) {
-    registerKeys = on;
-    if (myTable != null) {
-      myTable.setRegisterKeys(on);
-    }
-  }
-
-  void movePointerTo(final Patch.Key k) {
-    if (myTable != null) {
-      myTable.movePointerTo(k);
-    }
-  }
-
-  void setActive(boolean active) {
-    this.active = active;
-    if (myTable != null) {
-      myTable.setActive(active);
-    }
-  }
-
-  void notifyDraftDelta(final Patch.Key k, final int delta) {
-    if (myTable != null) {
-      myTable.notifyDraftDelta(k, delta);
-    }
-  }
-
-  private void setMyTable(MyTable table) {
-    myBody.clear();
-    myBody.add(table);
-    myTable = table;
-
-    if (clickHandlers != null) {
-      for (ClickHandler ch : clickHandlers) {
-        myTable.addClickHandler(ch);
-      }
-      clickHandlers = null;
-    }
-
-    if (active) {
-      myTable.setActive(true);
-      active = false;
-    }
-
-    if (registerKeys) {
-      myTable.setRegisterKeys(registerKeys);
-      registerKeys = false;
-    }
-
-    myTable.finishDisplay();
-  }
-
-  /**
-   * @return a link to the previous file in this patch set, or null.
-   */
-  InlineHyperlink getPreviousPatchLink(int index) {
-    int previousPatchIndex = getPreviousPatch(index, PREFERENCE_VALIDATOR);
-    if (previousPatchIndex < 0) {
-      return null;
-    }
-    return createLink(previousPatchIndex,
-        SafeHtml.asis(Util.C.prevPatchLinkIcon()), null);
-  }
-
-  /**
-   * @return a link to the next file in this patch set, or null.
-   */
-  InlineHyperlink getNextPatchLink(int index) {
-    int nextPatchIndex = getNextPatch(index, false, PREFERENCE_VALIDATOR);
-    if (nextPatchIndex < 0) {
-      return null;
-    }
-    return createLink(nextPatchIndex, null,
-        SafeHtml.asis(Util.C.nextPatchLinkIcon()));
-  }
-
-  /**
-   * @return a link to the the given patch.
-   * @param index The patch to link to
-   * @param before A string to display at the beginning of the href text
-   * @param after A string to display at the end of the href text
-   */
-  PatchLink createLink(int index, SafeHtml before, SafeHtml after) {
-    Patch patch = patchList.get(index);
-    Patch.Key thisKey = patch.getKey();
-    PatchLink link;
-
-    if (isUnifiedPatchLink(patch)) {
-      link = new PatchLink.Unified("", base, thisKey);
-    } else {
-      link = new PatchLink.SideBySide("", base, thisKey);
-    }
-
-    SafeHtmlBuilder text = new SafeHtmlBuilder();
-    text.append(before);
-    text.append(getFileNameOnly(patch));
-    text.append(after);
-    SafeHtml.set(link, text);
-    return link;
-  }
-
-  private static boolean isUnifiedPatchLink(final Patch patch) {
-    return (patch.getPatchType().equals(PatchType.BINARY)
-        || (Gerrit.isSignedIn()
-            && Gerrit.getUserPreferences().diffView()
-                .equals(DiffView.UNIFIED_DIFF)));
-  }
-
-  private static String getFileNameOnly(Patch patch) {
-    // Note: use '/' here and not File.pathSeparator since git paths
-    // are always separated by /
-    //
-    String fileName = getDisplayFileName(patch);
-    int s = fileName.lastIndexOf('/');
-    if (s >= 0) {
-      fileName = fileName.substring(s + 1);
-    }
-    return fileName;
-  }
-
-  static String getDisplayFileName(Patch patch) {
-    return getDisplayFileName(patch.getKey());
-  }
-
-  static String getDisplayFileName(Patch.Key patchKey) {
-    if (Patch.COMMIT_MSG.equals(patchKey.get())) {
-      return Util.C.commitMessage();
-    }
-    return patchKey.get();
-  }
-
-  /**
-   * Update the reviewed status for the given patch.
-   */
-  void updateReviewedStatus(Patch.Key patchKey, boolean reviewed) {
-    if (myTable != null) {
-      myTable.updateReviewedStatus(patchKey, reviewed);
-    }
-  }
-
-  ListenableAccountDiffPreference getPreferences() {
-    return listenablePrefs;
-  }
-
-  private class MyTable extends NavigationTable<Patch> {
-    private static final int C_PATH = 2;
-    private static final int C_DRAFT = 3;
-    private static final int C_SIZE = 4;
-    private static final int C_SIDEBYSIDE = 5;
-    private int activeRow = -1;
-
-    MyTable() {
-      keysNavigation.add(new PrevKeyCommand(0, 'k', Util.C.patchTablePrev()));
-      keysNavigation.add(new NextKeyCommand(0, 'j', Util.C.patchTableNext()));
-      keysNavigation.add(new OpenKeyCommand(0, 'o', Util.C.patchTableOpenDiff()));
-      keysNavigation.add(new OpenKeyCommand(0, KeyCodes.KEY_ENTER, Util.C
-          .patchTableOpenDiff()));
-      keysNavigation.add(new OpenUnifiedDiffKeyCommand(0, 'O', Util.C
-          .patchTableOpenUnifiedDiff()));
-
-      table.addClickHandler(new ClickHandler() {
-        @Override
-        public void onClick(final ClickEvent event) {
-          final Cell cell = table.getCellForEvent(event);
-          if (cell != null && cell.getRowIndex() > 0) {
-            movePointerTo(cell.getRowIndex());
-          }
-        }
-      });
-      setSavePointerId(PatchTable.this.savePointerId);
-    }
-
-    public void addClickHandler(final ClickHandler clickHandler) {
-      table.addClickHandler(clickHandler);
-    }
-
-    void updateReviewedStatus(final Patch.Key patchKey, boolean reviewed) {
-      int idx = patchMap().get(patchKey);
-      if (0 <= idx) {
-        Patch patch = patchList.get(idx);
-        if (patch.isReviewedByCurrentUser() != reviewed) {
-          int row = idx + 1;
-          int col = C_SIDEBYSIDE + 2;
-          if (patch.getPatchType() == Patch.PatchType.BINARY) {
-            col = C_SIDEBYSIDE + 3;
-          }
-          if (reviewed) {
-            table.setWidget(row, col, new Image(Gerrit.RESOURCES.greenCheck()));
-          } else {
-            table.clearCell(row, col);
-          }
-          patch.setReviewedByCurrentUser(reviewed);
-        }
-      }
-    }
-
-    void notifyDraftDelta(final Patch.Key key, final int delta) {
-      int idx = patchMap().get(key);
-      if (0 <= idx) {
-        Patch p = patchList.get(idx);
-        p.setDraftCount(p.getDraftCount() + delta);
-        SafeHtmlBuilder m = new SafeHtmlBuilder();
-        appendCommentCount(m, p);
-        SafeHtml.set(table, idx + 1, C_DRAFT, m);
-      }
-    }
-
-    @Override
-    public void resetHtml(final SafeHtml html) {
-      super.resetHtml(html);
-    }
-
-    @Override
-    public void movePointerTo(Object oldId) {
-      super.movePointerTo(oldId);
-    }
-
-    /** Activates / Deactivates the key navigation and the highlighting of the current row for this table */
-    void setActive(boolean active) {
-      if (active) {
-        if(activeRow > 0 && getCurrentRow() != activeRow) {
-          super.movePointerTo(activeRow);
-          activeRow = -1;
-        }
-      } else {
-        if(getCurrentRow() > 0) {
-          activeRow = getCurrentRow();
-          super.movePointerTo(-1);
-        }
-      }
-      setRegisterKeys(active);
-    }
-
-    void initializeRow(int row) {
-      Patch patch = PatchTable.this.patchList.get(row - 1);
-      setRowItem(row, patch);
-
-      Widget nameCol = new PatchLink.SideBySide(getDisplayFileName(patch), base,
-          patch.getKey());
-
-      if (patch.getSourceFileName() != null) {
-        final String text;
-        if (patch.getChangeType() == Patch.ChangeType.RENAMED) {
-          text = Util.M.renamedFrom(patch.getSourceFileName());
-        } else if (patch.getChangeType() == Patch.ChangeType.COPIED) {
-          text = Util.M.copiedFrom(patch.getSourceFileName());
-        } else {
-          text = Util.M.otherFrom(patch.getSourceFileName());
-        }
-        final Label line = new Label(text);
-        line.setStyleName(Gerrit.RESOURCES.css().sourceFilePath());
-        final FlowPanel cell = new FlowPanel();
-        cell.add(nameCol);
-        cell.add(line);
-        nameCol = cell;
-      }
-      table.setWidget(row, C_PATH, nameCol);
-
-      int C_UNIFIED = C_SIDEBYSIDE + 1;
-
-      PatchLink sideBySide = new PatchLink.SideBySide(
-          Util.C.patchTableDiffSideBySide(), base, patch.getKey());
-      sideBySide.setStyleName("gwt-Anchor");
-
-      PatchLink unified = new PatchLink.Unified(Util.C.patchTableDiffUnified(),
-          base, patch.getKey());
-      unified.setStyleName("gwt-Anchor");
-
-      table.setWidget(row, C_SIDEBYSIDE, sideBySide);
-      table.setWidget(row, C_UNIFIED, unified);
-    }
-
-    void initializeLastRow(int row) {
-      Anchor sideBySide = new Anchor(Util.C.diffAllSideBySide());
-      sideBySide.addClickHandler(new ClickHandler() {
-        @Override
-        public void onClick(ClickEvent event) {
-          for (Patch p : detail.getPatches()) {
-            openWindow(Dispatcher.toSideBySide(base, p.getKey()));
-          }
-        }
-      });
-      table.setWidget(row, C_SIDEBYSIDE - 2, sideBySide);
-
-      int C_UNIFIED = C_SIDEBYSIDE - 2 + 1;
-      Anchor unified = new Anchor(Util.C.diffAllUnified());
-      unified.addClickHandler(new ClickHandler() {
-        @Override
-        public void onClick(ClickEvent event) {
-          for (Patch p : detail.getPatches()) {
-            openWindow(Dispatcher.toUnified(base, p.getKey()));
-          }
-        }
-      });
-      table.setWidget(row, C_UNIFIED, unified);
-    }
-
-    private void openWindow(String token) {
-      String url = Window.Location.getPath() + "#" + token;
-      Window.open(url, "_blank", null);
-    }
-
-    void appendHeader(final SafeHtmlBuilder m) {
-      m.openTr();
-
-      // Cursor
-      m.openTd();
-      m.addStyleName(Gerrit.RESOURCES.css().iconHeader());
-      m.addStyleName(Gerrit.RESOURCES.css().leftMostCell());
-      m.nbsp();
-      m.closeTd();
-
-      // Mode
-      m.openTd();
-      m.setStyleName(Gerrit.RESOURCES.css().iconHeader());
-      m.nbsp();
-      m.closeTd();
-
-      // "File path"
-      m.openTd();
-      m.setStyleName(Gerrit.RESOURCES.css().dataHeader());
-      m.append(Util.C.patchTableColumnName());
-      m.closeTd();
-
-      // "Comments"
-      m.openTd();
-      m.setStyleName(Gerrit.RESOURCES.css().dataHeader());
-      m.append(Util.C.patchTableColumnComments());
-      m.closeTd();
-
-      // "Size"
-      m.openTd();
-      m.setStyleName(Gerrit.RESOURCES.css().dataHeader());
-      m.append(Util.C.patchTableColumnSize());
-      m.closeTd();
-
-      // "Diff"
-      m.openTd();
-      m.setStyleName(Gerrit.RESOURCES.css().dataHeader());
-      m.setAttribute("colspan", 3);
-      m.append(Util.C.patchTableColumnDiff());
-      m.closeTd();
-
-      // "Reviewed"
-      if (Gerrit.isSignedIn()) {
-        m.openTd();
-        m.setStyleName(Gerrit.RESOURCES.css().iconHeader());
-        m.addStyleName(Gerrit.RESOURCES.css().dataHeader());
-        m.append(Util.C.reviewed());
-        m.closeTd();
-      }
-
-      m.closeTr();
-    }
-
-    void appendRow(final SafeHtmlBuilder m, final Patch p,
-        final boolean isReverseDiff) {
-      m.openTr();
-
-      m.openTd();
-      m.addStyleName(Gerrit.RESOURCES.css().iconCell());
-      m.addStyleName(Gerrit.RESOURCES.css().leftMostCell());
-      m.nbsp();
-      m.closeTd();
-
-      m.openTd();
-      m.setStyleName(Gerrit.RESOURCES.css().changeTypeCell());
-      if (isReverseDiff) {
-        m.addStyleName(Gerrit.RESOURCES.css().patchCellReverseDiff());
-      }
-
-      if (Patch.COMMIT_MSG.equals(p.getFileName())) {
-        m.nbsp();
-      } else {
-        m.append(p.getChangeType().getCode());
-      }
-      m.closeTd();
-
-      m.openTd();
-      m.addStyleName(Gerrit.RESOURCES.css().dataCell());
-      m.addStyleName(Gerrit.RESOURCES.css().filePathCell());
-      m.closeTd();
-
-      m.openTd();
-      m.addStyleName(Gerrit.RESOURCES.css().dataCell());
-      m.addStyleName(Gerrit.RESOURCES.css().commentCell());
-      appendCommentCount(m, p);
-      m.closeTd();
-
-      m.openTd();
-      m.addStyleName(Gerrit.RESOURCES.css().dataCell());
-
-      m.addStyleName(Gerrit.RESOURCES.css().patchSizeCell());
-      if (isReverseDiff) {
-        m.addStyleName(Gerrit.RESOURCES.css().patchCellReverseDiff());
-      }
-
-      appendSize(m, p);
-      m.closeTd();
-
-      // Diff
-      openlink(m, 2);
-      m.closeTd();
-      openlink(m, 1);
-      m.closeTd();
-
-      // Green check mark if the user is logged in and they reviewed that file
-      if (Gerrit.isSignedIn()) {
-        m.openTd();
-        m.setStyleName(Gerrit.RESOURCES.css().dataCell());
-        if (p.isReviewedByCurrentUser()) {
-          m.openDiv();
-          m.setStyleName(Gerrit.RESOURCES.css().greenCheckClass());
-          m.closeSelf();
-        }
-        m.closeTd();
-      }
-
-      m.closeTr();
-    }
-
-    void appendLastRow(final SafeHtmlBuilder m, int ins, int dels,
-        final boolean isReverseDiff) {
-      m.openTr();
-
-      m.openTd();
-      m.addStyleName(Gerrit.RESOURCES.css().iconCell());
-      m.addStyleName(Gerrit.RESOURCES.css().noborder());
-      m.nbsp();
-      m.closeTd();
-
-      m.openTd();
-      m.setAttribute("colspan", C_SIZE - 1);
-      m.closeTd();
-
-      m.openTd();
-      m.addStyleName(Gerrit.RESOURCES.css().dataCell());
-      m.addStyleName(Gerrit.RESOURCES.css().patchSizeCell());
-      m.addStyleName(Gerrit.RESOURCES.css().leftMostCell());
-
-      if (isReverseDiff) {
-        m.addStyleName(Gerrit.RESOURCES.css().patchCellReverseDiff());
-      }
-
-      m.append(Util.M.patchTableSize_Modify(ins, dels));
-      m.closeTd();
-
-      openlink(m, 2);
-      m.closeTd();
-
-      openlink(m, 1);
-      m.closeTd();
-
-      m.closeTr();
-    }
-
-    void appendCommentCount(final SafeHtmlBuilder m, final Patch p) {
-      if (p.getCommentCount() > 0) {
-        m.append(Util.M.patchTableComments(p.getCommentCount()));
-      }
-      if (p.getDraftCount() > 0) {
-        if (p.getCommentCount() > 0) {
-          m.append(", ");
-        }
-        m.openSpan();
-        m.setStyleName(Gerrit.RESOURCES.css().drafts());
-        m.append(Util.M.patchTableDrafts(p.getDraftCount()));
-        m.closeSpan();
-      }
-    }
-
-    void appendSize(final SafeHtmlBuilder m, final Patch p) {
-      if (Patch.COMMIT_MSG.equals(p.getFileName())) {
-        m.nbsp();
-        return;
-      }
-
-      if (p.getPatchType() == PatchType.UNIFIED) {
-        int ins = p.getInsertions();
-        int dels = p.getDeletions();
-
-        switch (p.getChangeType()) {
-          case ADDED:
-            m.append(Util.M.patchTableSize_Lines(ins));
-            break;
-
-          case DELETED:
-            m.nbsp();
-            break;
-
-          case MODIFIED:
-          case COPIED:
-          case RENAMED:
-            m.append(Util.M.patchTableSize_Modify(ins, dels));
-            break;
-
-          case REWRITE:
-            break;
-        }
-      } else {
-        m.nbsp();
-      }
-    }
-
-    private void openlink(final SafeHtmlBuilder m, final int colspan) {
-      m.openTd();
-      m.addStyleName(Gerrit.RESOURCES.css().dataCell());
-      m.addStyleName(Gerrit.RESOURCES.css().diffLinkCell());
-      m.setAttribute("colspan", colspan);
-    }
-
-    @Override
-    protected Object getRowItemKey(final Patch item) {
-      return item.getKey();
-    }
-
-    @Override
-    protected void onOpenRow(final int row) {
-      Widget link = table.getWidget(row, C_PATH);
-      if (link instanceof FlowPanel) {
-        link = ((FlowPanel) link).getWidget(0);
-      }
-      if (link instanceof InlineHyperlink) {
-        ((InlineHyperlink) link).go();
-      }
-    }
-
-    private final class OpenUnifiedDiffKeyCommand extends KeyCommand {
-
-      public OpenUnifiedDiffKeyCommand(int mask, char key, String help) {
-        super(mask, key, help);
-      }
-
-      @Override
-      public void onKeyPress(KeyPressEvent event) {
-        Widget link = table.getWidget(getCurrentRow(), C_PATH);
-        if (link instanceof FlowPanel) {
-          link = ((FlowPanel) link).getWidget(0);
-        }
-        if (link instanceof PatchLink.Unified) {
-          ((InlineHyperlink) link).go();
-        } else {
-          link = table.getWidget(getCurrentRow(), C_SIDEBYSIDE + 1);
-          if (link instanceof PatchLink.Unified) {
-            ((InlineHyperlink) link).go();
-          }
-        }
-      }
-    }
-  }
-
-  private final class DisplayCommand implements RepeatingCommand {
-    private final MyTable table;
-    private final List<Patch> list;
-    private boolean attached;
-    private SafeHtmlBuilder nc = new SafeHtmlBuilder();
-    private int stage = 0;
-    private int row;
-    private double start;
-    private ProgressBar meter;
-
-    private int insertions;
-    private int deletions;
-
-    private final PatchSet.Id psIdToCompareWith;
-
-    private DisplayCommand(final List<Patch> list, final PatchSet.Id psIdToCompareWith) {
-      this.table = new MyTable();
-      this.list = list;
-      this.psIdToCompareWith = psIdToCompareWith;
-    }
-
-    /**
-     * Add the files contained in the list of patches to the table, one per row.
-     */
-    @Override
-    @SuppressWarnings("fallthrough")
-    public boolean execute() {
-      final boolean attachedNow = isAttached();
-      if (!attached && attachedNow) {
-        // Remember that we have been attached at least once. If
-        // later we find we aren't attached we should stop running.
-        //
-        attached = true;
-      } else if (attached && !attachedNow) {
-        // If the user navigated away, we aren't in the DOM anymore.
-        // Don't continue to render.
-        //
-        return false;
-      }
-
-      boolean isReverseDiff = false;
-
-      if (psIdToCompareWith != null
-          && list.get(0).getKey().getParentKey().get() < psIdToCompareWith.get()) {
-        isReverseDiff = true;
-      }
-
-      start = System.currentTimeMillis();
-      switch (stage) {
-        case 0:
-          if (row == 0) {
-            table.appendHeader(nc);
-            table.appendRow(nc, list.get(row++), isReverseDiff);
-          }
-          while (row < list.size()) {
-            Patch p = list.get(row);
-            insertions += p.getInsertions();
-            deletions += p.getDeletions();
-            table.appendRow(nc, p, isReverseDiff);
-            if ((++row % 10) == 0 && longRunning()) {
-              updateMeter();
-              return true;
-            }
-          }
-          table.appendLastRow(nc, insertions, deletions, isReverseDiff);
-          table.resetHtml(nc);
-          table.initializeLastRow(row + 1);
-          nc = null;
-          stage = 1;
-          row = 0;
-
-        case 1:
-          while (row < list.size()) {
-            table.initializeRow(row + 1);
-            if ((++row % 50) == 0 && longRunning()) {
-              updateMeter();
-              return true;
-            }
-          }
-          updateMeter();
-          showTable();
-      }
-      return false;
-    }
-
-    void showTable() {
-      setMyTable(table);
-
-      if (PatchTable.this.onLoadCommand != null) {
-        PatchTable.this.onLoadCommand.execute();
-        PatchTable.this.onLoadCommand = null;
-      }
-    }
-
-    void initMeter() {
-      if (meter == null) {
-        meter = new ProgressBar(Util.M.loadingPatchSet(detail.getPatchSet().getId().get()));
-        PatchTable.this.myBody.clear();
-        PatchTable.this.myBody.add(meter);
-      }
-      updateMeter();
-    }
-
-    void updateMeter() {
-      if (meter != null) {
-        final int n = list.size();
-        meter.setValue(((100 * (stage * n + row)) / (2 * n)));
-      }
-    }
-
-    private boolean longRunning() {
-      return System.currentTimeMillis() - start > 200;
-    }
-  }
-
-
-  /**
-   * Gets the next patch
-   *
-   * @param currentIndex
-   * @param validators
-   * @param loopAround loops back around to the front and traverses if this is
-   *        true
-   * @return index of next valid patch, or -1 if no valid patches
-   */
-  int getNextPatch(int currentIndex, boolean loopAround,
-      PatchValidator... validators) {
-    return getNextPatchHelper(currentIndex, loopAround, detail.getPatches()
-        .size(), validators);
-  }
-
-  /**
-   * Helper function for getNextPatch
-   *
-   * @param currentIndex
-   * @param validators
-   * @param loopAround
-   * @param maxIndex will only traverse up to this index
-   * @return index of next valid patch, or -1 if no valid patches
-   */
-  private int getNextPatchHelper(int currentIndex, boolean loopAround,
-      int maxIndex, PatchValidator... validators) {
-    for (int i = currentIndex + 1; i < maxIndex; i++) {
-      Patch patch = detail.getPatches().get(i);
-      if (patch != null && patchIsValid(patch, validators)) {
-        return i;
-      }
-    }
-
-    if (loopAround) {
-      return getNextPatchHelper(-1, false, currentIndex, validators);
-    }
-
-    return -1;
-  }
-
-  /**
-   * @return the index to the previous patch
-   */
-  int getPreviousPatch(int currentIndex, PatchValidator... validators) {
-    for (int i = currentIndex - 1; i >= 0; i--) {
-      Patch patch = detail.getPatches().get(i);
-      if (patch != null && patchIsValid(patch, validators)) {
-        return i;
-      }
-    }
-
-    return -1;
-  }
-
-  /**
-   * Helper function that returns whether a patch is valid or not
-   *
-   * @param patch
-   * @param validators
-   * @return whether the patch is valid based on the validators
-   */
-  private boolean patchIsValid(Patch patch, PatchValidator... validators) {
-    for (PatchValidator v : validators) {
-      if (!v.isValid(patch)) {
-        return false;
-      }
-    }
-    return true;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchUtil.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchUtil.java
index e949194..d599756 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchUtil.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchUtil.java
@@ -14,22 +14,9 @@
 
 package com.google.gerrit.client.patches;
 
-import com.google.gerrit.common.data.ChangeDetailService;
-import com.google.gerrit.common.data.PatchDetailService;
 import com.google.gwt.core.client.GWT;
-import com.google.gwtjsonrpc.client.JsonUtil;
 
 public class PatchUtil {
   public static final PatchConstants C = GWT.create(PatchConstants.class);
   public static final PatchMessages M = GWT.create(PatchMessages.class);
-  public static final ChangeDetailService CHANGE_SVC;
-  public static final PatchDetailService PATCH_SVC;
-
-  static {
-    CHANGE_SVC = GWT.create(ChangeDetailService.class);
-    JsonUtil.bind(CHANGE_SVC, "rpc/ChangeDetailService");
-
-    PATCH_SVC = GWT.create(PatchDetailService.class);
-    JsonUtil.bind(PATCH_SVC, "rpc/PatchDetailService");
-  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/ReviewedPanels.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/ReviewedPanels.java
deleted file mode 100644
index d889c79..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/ReviewedPanels.java
+++ /dev/null
@@ -1,176 +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.client.patches;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.VoidResult;
-import com.google.gerrit.client.changes.Util;
-import com.google.gerrit.client.patches.PatchTable.PatchValidator;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.client.ui.ChangeLink;
-import com.google.gerrit.client.ui.InlineHyperlink;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.logical.shared.ValueChangeEvent;
-import com.google.gwt.event.logical.shared.ValueChangeHandler;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwt.user.client.ui.Anchor;
-import com.google.gwt.user.client.ui.CheckBox;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwtexpui.safehtml.client.SafeHtml;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-
-class ReviewedPanels {
-  final FlowPanel top;
-  final FlowPanel bottom;
-
-  private Patch.Key patchKey;
-  private PatchTable fileList;
-  private InlineHyperlink reviewedLink;
-  private CheckBox checkBoxTop;
-  private CheckBox checkBoxBottom;
-
-  ReviewedPanels() {
-    this.top = new FlowPanel();
-    this.bottom = new FlowPanel();
-    this.bottom.setStyleName(Gerrit.RESOURCES.css().reviewedPanelBottom());
-  }
-
-  void populate(Patch.Key pk, PatchTable pt, int patchIndex) {
-    patchKey = pk;
-    fileList = pt;
-    reviewedLink = createReviewedLink(patchIndex);
-
-    top.clear();
-    checkBoxTop = createReviewedCheckbox();
-    top.add(checkBoxTop);
-    top.add(createReviewedAnchor());
-
-    bottom.clear();
-    checkBoxBottom = createReviewedCheckbox();
-    bottom.add(checkBoxBottom);
-    bottom.add(createReviewedAnchor());
-  }
-
-  private CheckBox createReviewedCheckbox() {
-    final CheckBox checkBox = new CheckBox(PatchUtil.C.reviewedAnd() + " ");
-    checkBox.addValueChangeHandler(new ValueChangeHandler<Boolean>() {
-      @Override
-      public void onValueChange(ValueChangeEvent<Boolean> event) {
-        final boolean value = event.getValue();
-        setReviewedByCurrentUser(value);
-        if (checkBoxTop.getValue() != value) {
-          checkBoxTop.setValue(value);
-        }
-        if (checkBoxBottom.getValue() != value) {
-          checkBoxBottom.setValue(value);
-        }
-      }
-    });
-    return checkBox;
-  }
-
-  boolean getValue() {
-    return checkBoxTop.getValue();
-  }
-
-  void setValue(final boolean value) {
-    checkBoxTop.setValue(value);
-    checkBoxBottom.setValue(value);
-  }
-
-  void setReviewedByCurrentUser(boolean reviewed) {
-    PatchSet.Id ps = patchKey.getParentKey();
-    if (ps.get() != 0) {
-      if (fileList != null) {
-        fileList.updateReviewedStatus(patchKey, reviewed);
-      }
-
-      RestApi api = new RestApi("/changes/").id(ps.getParentKey().get())
-          .view("revisions").id(ps.get())
-          .view("files").id(patchKey.getFileName())
-          .view("reviewed");
-
-      AsyncCallback<VoidResult> cb = new AsyncCallback<VoidResult>() {
-        @Override
-        public void onFailure(Throwable arg0) {
-          // nop
-        }
-
-        @Override
-        public void onSuccess(VoidResult result) {
-          // nop
-        }
-      };
-      if (reviewed) {
-        api.put(cb);
-      } else {
-        api.delete(cb);
-      }
-    }
-  }
-
-  void go() {
-    if (reviewedLink != null) {
-      setReviewedByCurrentUser(true);
-      reviewedLink.go();
-    }
-  }
-
-  private InlineHyperlink createReviewedLink(final int patchIndex) {
-    final PatchValidator unreviewedValidator = new PatchValidator() {
-      @Override
-      public boolean isValid(Patch patch) {
-        return !patch.isReviewedByCurrentUser();
-      }
-    };
-
-    InlineHyperlink reviewedLink = new ChangeLink("", patchKey.getParentKey());
-    if (fileList != null) {
-      int nextUnreviewedPatchIndex =
-          fileList.getNextPatch(patchIndex, true, unreviewedValidator,
-              fileList.PREFERENCE_VALIDATOR);
-
-      if (nextUnreviewedPatchIndex > -1) {
-        // Create invisible patch link to change page
-        reviewedLink =
-            fileList.createLink(nextUnreviewedPatchIndex, null, null);
-        reviewedLink.setText("");
-      }
-    }
-    return reviewedLink;
-  }
-
-  private Anchor createReviewedAnchor() {
-    SafeHtmlBuilder text = new SafeHtmlBuilder();
-    text.append(PatchUtil.C.next());
-    text.append(SafeHtml.asis(Util.C.nextPatchLinkIcon()));
-
-    Anchor reviewedAnchor = new Anchor("");
-    SafeHtml.set(reviewedAnchor, text);
-
-    reviewedAnchor.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(ClickEvent event) {
-        setReviewedByCurrentUser(true);
-        reviewedLink.go();
-      }
-    });
-
-    return reviewedAnchor;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedDiffTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedDiffTable.java
deleted file mode 100644
index 156545a..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedDiffTable.java
+++ /dev/null
@@ -1,643 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.patches;
-
-import static com.google.gerrit.client.patches.PatchLine.Type.CONTEXT;
-import static com.google.gerrit.client.patches.PatchLine.Type.DELETE;
-import static com.google.gerrit.client.patches.PatchLine.Type.INSERT;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.common.data.CommentDetail;
-import com.google.gerrit.common.data.PatchScript;
-import com.google.gerrit.common.data.PatchScript.DisplayMethod;
-import com.google.gerrit.common.data.PatchSetDetail;
-import com.google.gerrit.prettify.client.SparseHtmlFile;
-import com.google.gerrit.prettify.common.EditList;
-import com.google.gerrit.prettify.common.EditList.Hunk;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.user.client.DOM;
-import com.google.gwt.user.client.Event;
-import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
-import com.google.gwt.user.client.ui.UIObject;
-import com.google.gwtexpui.safehtml.client.SafeHtml;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.Iterator;
-import java.util.List;
-
-public class UnifiedDiffTable extends AbstractPatchContentTable {
-  private static final int PC = 3;
-  private static final Comparator<PatchLineComment> BY_DATE =
-      new Comparator<PatchLineComment>() {
-        @Override
-        public int compare(final PatchLineComment o1, final PatchLineComment o2) {
-          return o1.getWrittenOn().compareTo(o2.getWrittenOn());
-        }
-      };
-
-  protected boolean isFileCommentBorderRowExist;
-  // Cursors.
-  protected int rowOfTableHeaderB;
-  protected int borderRowOfFileComment;
-
-  @Override
-  protected void onCellDoubleClick(final int row, final int column) {
-    if (column > C_ARROW && getRowItem(row) instanceof PatchLine) {
-      final PatchLine pl = (PatchLine) getRowItem(row);
-      switch (pl.getType()) {
-        case DELETE:
-        case CONTEXT:
-          createCommentEditor(row + 1, PC, pl.getLineA(), (short) 0);
-          break;
-        case INSERT:
-          createCommentEditor(row + 1, PC, pl.getLineB(), (short) 1);
-          break;
-        case REPLACE:
-          break;
-      }
-    }
-  }
-
-  @Override
-  protected void updateCursor(final PatchLineComment newComment) {
-    if (newComment.getLine() == R_HEAD) {
-      final PatchSet.Id psId =
-          newComment.getKey().getParentKey().getParentKey();
-      switch (newComment.getSide()) {
-        case FILE_SIDE_A:
-          if (idSideA == null && idSideB.equals(psId)) {
-            rowOfTableHeaderB++;
-            borderRowOfFileComment++;
-            return;
-          }
-          break;
-        case FILE_SIDE_B:
-          if (idSideA != null && idSideA.equals(psId)) {
-            rowOfTableHeaderB++;
-            borderRowOfFileComment++;
-          } else if (idSideB.equals(psId)) {
-            borderRowOfFileComment++;
-          }
-      }
-    }
-  }
-
-  @Override
-  protected void onCellSingleClick(Event event, int row, int column) {
-    super.onCellSingleClick(event, row, column);
-    if (column == 1 || column == 2) {
-      if (!"".equals(table.getText(row, column))) {
-        onCellDoubleClick(row, column);
-      }
-    }
-  }
-
-  @Override
-  protected void destroyCommentRow(final int row) {
-    super.destroyCommentRow(row);
-    if (this.rowOfTableHeaderB + 1 == row && row + 1 == borderRowOfFileComment) {
-      table.removeRow(row);
-      isFileCommentBorderRowExist = false;
-    }
-  }
-
-  @Override
-  public void remove(CommentEditorPanel panel) {
-    super.remove(panel);
-    if (panel.getComment().getLine() == AbstractPatchContentTable.R_HEAD) {
-      final PatchSet.Id psId =
-          panel.getComment().getKey().getParentKey().getParentKey();
-      switch (panel.getComment().getSide()) {
-        case FILE_SIDE_A:
-          if (idSideA == null && idSideB.equals(psId)) {
-            rowOfTableHeaderB--;
-            borderRowOfFileComment--;
-            return;
-          }
-          break;
-        case FILE_SIDE_B:
-          if (idSideA != null && idSideA.equals(psId)) {
-            rowOfTableHeaderB--;
-            borderRowOfFileComment--;
-          } else if (idSideB.equals(psId)) {
-            borderRowOfFileComment--;
-          }
-      }
-    }
-  }
-
-  @Override
-  protected void onInsertComment(final PatchLine pl) {
-    final int row = getCurrentRow();
-    switch (pl.getType()) {
-      case DELETE:
-      case CONTEXT:
-        createCommentEditor(row + 1, PC, pl.getLineA(), (short) 0);
-        break;
-      case INSERT:
-        createCommentEditor(row + 1, PC, pl.getLineB(), (short) 1);
-        break;
-      case REPLACE:
-        break;
-    }
-  }
-
-  private void appendImgTag(SafeHtmlBuilder nc, String url) {
-    nc.openElement("img");
-    nc.setAttribute("src", url);
-    nc.closeElement("img");
-  }
-
-  @Override
-  protected void createFileCommentEditorOnSideA() {
-    createCommentEditor(R_HEAD + 1, PC, R_HEAD, FILE_SIDE_A);
-  }
-
-  @Override
-  protected void createFileCommentEditorOnSideB() {
-    createCommentEditor(rowOfTableHeaderB + 1, PC, R_HEAD, FILE_SIDE_B);
-    createFileCommentBorderRow();
-  }
-
-  private void populateTableHeader(final PatchScript script,
-      final PatchSetDetail detail) {
-    initHeaders(script, detail);
-    table.setWidget(R_HEAD, PC, headerSideA);
-    table.setWidget(rowOfTableHeaderB, PC, headerSideB);
-    table.getFlexCellFormatter().addStyleName(R_HEAD, PC,
-        Gerrit.RESOURCES.css().unifiedTableHeader());
-    table.getFlexCellFormatter().addStyleName(rowOfTableHeaderB, PC,
-        Gerrit.RESOURCES.css().unifiedTableHeader());
-
-    // Add icons to lineNumber column header
-    if (headerSideA.isFileOrCommitMessage()) {
-      table.setWidget(R_HEAD, 1, iconA);
-    }
-    if (headerSideB.isFileOrCommitMessage()) {
-      table.setWidget(rowOfTableHeaderB, 2, iconB);
-    }
-  }
-
-  private void allocateTableHeader(SafeHtmlBuilder nc) {
-    rowOfTableHeaderB = 1;
-    borderRowOfFileComment = 2;
-    for (int i = R_HEAD; i < borderRowOfFileComment; i++) {
-      openTableHeaderLine(nc);
-      padLineNumberOnTableHeaderForSideA(nc);
-      padLineNumberOnTableHeaderForSideB(nc);
-      nc.openTd();
-      nc.setStyleName(Gerrit.RESOURCES.css().fileLine());
-      nc.addStyleName(Gerrit.RESOURCES.css().fileColumnHeader());
-      nc.closeTd();
-      closeLine(nc);
-    }
-  }
-
-  @Override
-  protected void render(final PatchScript script, final PatchSetDetail detail) {
-    final SafeHtmlBuilder nc = new SafeHtmlBuilder();
-    allocateTableHeader(nc);
-
-    // Display the patch header
-    for (final String line : script.getPatchHeader()) {
-      appendFileHeader(nc, line);
-    }
-    final ArrayList<PatchLine> lines = new ArrayList<>();
-
-    if (hasDifferences(script)) {
-      if (script.getDisplayMethodA() == DisplayMethod.IMG
-          || script.getDisplayMethodB() == DisplayMethod.IMG) {
-        appendImageDifferences(script, nc);
-      } else if (!isDisplayBinary) {
-        appendTextDifferences(script, nc, lines);
-      }
-    } else {
-      appendNoDifferences(nc);
-    }
-
-    resetHtml(nc);
-    populateTableHeader(script, detail);
-    if (hasDifferences(script)) {
-      initScript(script);
-      if (!isDisplayBinary) {
-        int row = script.getPatchHeader().size();
-        final CellFormatter fmt = table.getCellFormatter();
-        final Iterator<PatchLine> iLine = lines.iterator();
-        while (iLine.hasNext()) {
-          final PatchLine l = iLine.next();
-          final String n;
-          switch (l.getType()) {
-            case CONTEXT:
-              n = Gerrit.RESOURCES.css().diffTextCONTEXT();
-              break;
-            case DELETE:
-              n = Gerrit.RESOURCES.css().diffTextDELETE();
-              break;
-            case INSERT:
-              n = Gerrit.RESOURCES.css().diffTextINSERT();
-              break;
-            case REPLACE:
-            default:
-              continue;
-          }
-          while (!fmt.getStyleName(row, PC).contains(n)) {
-            row++;
-          }
-          setRowItem(row++, l);
-        }
-      }
-    }
-  }
-
-  private void appendImageLine(final SafeHtmlBuilder nc, final String url,
-      final boolean syntaxHighlighting, final boolean isInsert) {
-    nc.openTr();
-    nc.setAttribute("valign", "center");
-    nc.setAttribute("align", "center");
-
-    nc.openTd();
-    nc.setStyleName(Gerrit.RESOURCES.css().iconCell());
-    nc.closeTd();
-
-    padLineNumberForSideA(nc);
-    padLineNumberForSideB(nc);
-
-    nc.openTd();
-    nc.setStyleName(Gerrit.RESOURCES.css().fileLine());
-    if (isInsert) {
-      setStyleInsert(nc, syntaxHighlighting);
-    } else {
-      setStyleDelete(nc, syntaxHighlighting);
-    }
-    appendImgTag(nc, url);
-    nc.closeTd();
-
-    nc.closeTr();
-  }
-
-  private void appendImageDifferences(final PatchScript script,
-      final SafeHtmlBuilder nc) {
-    final boolean syntaxHighlighting =
-        script.getDiffPrefs().syntaxHighlighting;
-    if (script.getDisplayMethodA() == DisplayMethod.IMG) {
-      final String url = getUrlA();
-      appendImageLine(nc, url, syntaxHighlighting, false);
-    }
-    if (script.getDisplayMethodB() == DisplayMethod.IMG) {
-      final String url = getUrlB();
-      appendImageLine(nc, url, syntaxHighlighting, true);
-    }
-  }
-
-  private void appendTextDifferences(final PatchScript script,
-      final SafeHtmlBuilder nc, final ArrayList<PatchLine> lines) {
-    final SparseHtmlFile a = getSparseHtmlFileA(script);
-    final SparseHtmlFile b = getSparseHtmlFileB(script);
-    final boolean syntaxHighlighting =
-        script.getDiffPrefs().syntaxHighlighting;
-    for (final EditList.Hunk hunk : script.getHunks()) {
-      appendHunkHeader(nc, hunk);
-      while (hunk.next()) {
-        if (hunk.isContextLine()) {
-          openLine(nc);
-          appendLineNumberForSideA(nc, hunk.getCurA());
-          appendLineNumberForSideB(nc, hunk.getCurB());
-          appendLineText(nc, false, CONTEXT, a, hunk.getCurA());
-          closeLine(nc);
-          hunk.incBoth();
-          lines.add(new PatchLine(CONTEXT, hunk.getCurA(), hunk.getCurB()));
-
-        } else if (hunk.isDeletedA()) {
-          openLine(nc);
-          appendLineNumberForSideA(nc, hunk.getCurA());
-          padLineNumberForSideB(nc);
-          appendLineText(nc, syntaxHighlighting, DELETE, a, hunk.getCurA());
-          closeLine(nc);
-          hunk.incA();
-          lines.add(new PatchLine(DELETE, hunk.getCurA(), -1));
-          if (a.size() == hunk.getCurA()
-              && script.getA().isMissingNewlineAtEnd()) {
-            appendNoLF(nc);
-          }
-
-        } else if (hunk.isInsertedB()) {
-          openLine(nc);
-          padLineNumberForSideA(nc);
-          appendLineNumberForSideB(nc, hunk.getCurB());
-          appendLineText(nc, syntaxHighlighting, INSERT, b, hunk.getCurB());
-          closeLine(nc);
-          hunk.incB();
-          lines.add(new PatchLine(INSERT, -1, hunk.getCurB()));
-          if (b.size() == hunk.getCurB()
-              && script.getB().isMissingNewlineAtEnd()) {
-            appendNoLF(nc);
-          }
-        }
-      }
-    }
-  }
-
-  @Override
-  public void display(final CommentDetail cd, boolean expandComments) {
-    if (cd == null || cd.isEmpty()) {
-      return;
-    }
-    setAccountInfoCache(cd.getAccounts());
-
-    final ArrayList<PatchLineComment> all = new ArrayList<>();
-    for (int row = 0; row < table.getRowCount();) {
-      final List<PatchLineComment> fora;
-      final List<PatchLineComment> forb;
-      if (row == R_HEAD) {
-        fora = cd.getForA(R_HEAD);
-        forb = cd.getForB(R_HEAD);
-        row++;
-
-        if (!fora.isEmpty()) {
-          row = insert(fora, row);
-        }
-        rowOfTableHeaderB = row;
-        borderRowOfFileComment = row + 1;
-        if (!forb.isEmpty()) {
-          row++;// Skip the Header of sideB.
-          row = insert(forb, row);
-          borderRowOfFileComment = row;
-          createFileCommentBorderRow();
-        }
-      } else if (getRowItem(row) instanceof PatchLine) {
-        final PatchLine pLine = (PatchLine) getRowItem(row);
-        fora = cd.getForA(pLine.getLineA());
-        forb = cd.getForB(pLine.getLineB());
-        row++;
-
-        if (!fora.isEmpty() && !forb.isEmpty()) {
-          all.clear();
-          all.addAll(fora);
-          all.addAll(forb);
-          Collections.sort(all, BY_DATE);
-          row = insert(all, row);
-
-        } else if (!fora.isEmpty()) {
-          row = insert(fora, row);
-
-        } else if (!forb.isEmpty()) {
-          row = insert(forb, row);
-        }
-      } else {
-        row++;
-        continue;
-      }
-    }
-  }
-
-  private void defaultStyle(final int row, final CellFormatter fmt) {
-    fmt.addStyleName(row, PC - 2, Gerrit.RESOURCES.css().lineNumber());
-    fmt.addStyleName(row, PC - 2, Gerrit.RESOURCES.css().rightBorder());
-    fmt.addStyleName(row, PC - 1, Gerrit.RESOURCES.css().lineNumber());
-    fmt.addStyleName(row, PC, Gerrit.RESOURCES.css().diffText());
-  }
-
-  @Override
-  protected void insertRow(final int row) {
-    super.insertRow(row);
-    final CellFormatter fmt = table.getCellFormatter();
-    defaultStyle(row, fmt);
-  }
-
-  private int insert(final List<PatchLineComment> in, int row) {
-    for (Iterator<PatchLineComment> ci = in.iterator(); ci.hasNext();) {
-      final PatchLineComment c = ci.next();
-      if (c.getLine() == R_HEAD) {
-        insertFileCommentRow(row);
-      } else {
-        insertRow(row);
-      }
-      bindComment(row, PC, c, !ci.hasNext());
-      row++;
-    }
-    return row;
-  }
-
-  @Override
-  protected void insertFileCommentRow(final int row) {
-    table.insertRow(row);
-    final CellFormatter fmt = table.getCellFormatter();
-
-    fmt.addStyleName(row, C_ARROW, //
-        Gerrit.RESOURCES.css().iconCellOfFileCommentRow());
-    defaultStyle(row, fmt);
-
-    fmt.addStyleName(row, C_ARROW, //
-        Gerrit.RESOURCES.css().cellsNextToFileComment());
-    fmt.addStyleName(row, PC - 2, //
-        Gerrit.RESOURCES.css().cellsNextToFileComment());
-    fmt.addStyleName(row, PC - 1, //
-        Gerrit.RESOURCES.css().cellsNextToFileComment());
-  }
-
-  private void createFileCommentBorderRow() {
-    if (!isFileCommentBorderRowExist) {
-      isFileCommentBorderRowExist = true;
-      table.insertRow(borderRowOfFileComment);
-      final CellFormatter fmt = table.getCellFormatter();
-      fmt.addStyleName(borderRowOfFileComment, C_ARROW, //
-          Gerrit.RESOURCES.css().iconCellOfFileCommentRow());
-      defaultStyle(borderRowOfFileComment, fmt);
-
-      final Element iconCell =
-          fmt.getElement(borderRowOfFileComment, C_ARROW);
-      UIObject.setStyleName(DOM.getParent(iconCell), //
-          Gerrit.RESOURCES.css().fileCommentBorder(), true);
-    }
-  }
-
-  private void appendFileHeader(final SafeHtmlBuilder m, final String line) {
-    openLine(m);
-    padLineNumberForSideA(m);
-    padLineNumberForSideB(m);
-
-    m.openTd();
-    m.setStyleName(Gerrit.RESOURCES.css().fileLine());
-    m.addStyleName(Gerrit.RESOURCES.css().diffText());
-    m.addStyleName(Gerrit.RESOURCES.css().diffTextFileHeader());
-    m.append(line);
-    m.closeTd();
-    closeLine(m);
-  }
-
-  private void appendHunkHeader(final SafeHtmlBuilder m, final Hunk hunk) {
-    openLine(m);
-    padLineNumberForSideA(m);
-    padLineNumberForSideB(m);
-
-    m.openTd();
-    m.setStyleName(Gerrit.RESOURCES.css().fileLine());
-    m.addStyleName(Gerrit.RESOURCES.css().diffText());
-    m.addStyleName(Gerrit.RESOURCES.css().diffTextHunkHeader());
-    m.append("@@ -");
-    appendRange(m, hunk.getCurA() + 1, hunk.getEndA() - hunk.getCurA());
-    m.append(" +");
-    appendRange(m, hunk.getCurB() + 1, hunk.getEndB() - hunk.getCurB());
-    m.append(" @@");
-    m.closeTd();
-
-    closeLine(m);
-  }
-
-  private void appendRange(final SafeHtmlBuilder m, final int begin,
-      final int cnt) {
-    switch (cnt) {
-      case 0:
-        m.append(begin - 1);
-        m.append(",0");
-        break;
-
-      case 1:
-        m.append(begin);
-        break;
-
-      default:
-        m.append(begin);
-        m.append(',');
-        m.append(cnt);
-        break;
-    }
-  }
-
-  private void setStyleDelete(final SafeHtmlBuilder m,
-      boolean syntaxHighlighting) {
-    m.addStyleName(Gerrit.RESOURCES.css().diffTextDELETE());
-    if (syntaxHighlighting) {
-      m.addStyleName(Gerrit.RESOURCES.css().fileLineDELETE());
-    }
-  }
-
-  private void setStyleInsert(final SafeHtmlBuilder m,
-      boolean syntaxHighlighting) {
-    m.addStyleName(Gerrit.RESOURCES.css().diffTextINSERT());
-    if (syntaxHighlighting) {
-      m.addStyleName(Gerrit.RESOURCES.css().fileLineINSERT());
-    }
-  }
-
-  private void appendLineText(final SafeHtmlBuilder m,
-      boolean syntaxHighlighting, final PatchLine.Type type,
-      final SparseHtmlFile src, final int i) {
-    final SafeHtml text = src.getSafeHtmlLine(i);
-    m.openTd();
-    m.setStyleName(Gerrit.RESOURCES.css().fileLine());
-    m.addStyleName(Gerrit.RESOURCES.css().diffText());
-    switch (type) {
-      case CONTEXT:
-        m.addStyleName(Gerrit.RESOURCES.css().diffTextCONTEXT());
-        m.nbsp();
-        m.append(text);
-        break;
-      case DELETE:
-        setStyleDelete(m, syntaxHighlighting);
-        m.append("-");
-        m.append(text);
-        break;
-      case INSERT:
-        setStyleInsert(m, syntaxHighlighting);
-        m.append("+");
-        m.append(text);
-        break;
-      case REPLACE:
-        break;
-    }
-    m.closeTd();
-  }
-
-  private void appendNoLF(final SafeHtmlBuilder m) {
-    openLine(m);
-    padLineNumberForSideA(m);
-    padLineNumberForSideB(m);
-    m.openTd();
-    m.addStyleName(Gerrit.RESOURCES.css().diffText());
-    m.addStyleName(Gerrit.RESOURCES.css().diffTextNoLF());
-    m.append("\\ No newline at end of file");
-    m.closeTd();
-    closeLine(m);
-  }
-
-  private void openLine(final SafeHtmlBuilder m) {
-    m.openTr();
-    m.setAttribute("valign", "top");
-    m.openTd();
-    m.setStyleName(Gerrit.RESOURCES.css().iconCell());
-    m.closeTd();
-  }
-
-  private void openTableHeaderLine(final SafeHtmlBuilder m) {
-    m.openTr();
-    m.openTd();
-    m.setStyleName(Gerrit.RESOURCES.css().iconCell());
-    m.addStyleName(Gerrit.RESOURCES.css().fileColumnHeader());
-    m.closeTd();
-  }
-
-  private void closeLine(final SafeHtmlBuilder m) {
-    m.closeTr();
-  }
-
-  private void padLineNumberForSideB(final SafeHtmlBuilder m) {
-    m.openTd();
-    m.setStyleName(Gerrit.RESOURCES.css().lineNumber());
-    m.closeTd();
-  }
-
-  private void padLineNumberForSideA(final SafeHtmlBuilder m) {
-    m.openTd();
-    m.setStyleName(Gerrit.RESOURCES.css().lineNumber());
-    m.addStyleName(Gerrit.RESOURCES.css().rightBorder());
-    m.closeTd();
-  }
-
-  private void appendLineNumberForSideB(final SafeHtmlBuilder m, final int idx) {
-    m.openTd();
-    m.setStyleName(Gerrit.RESOURCES.css().lineNumber());
-    m.append(SafeHtml.asis("<a href=\"javascript:void(0)\">"+ (idx + 1) + "</a>"));
-    m.closeTd();
-  }
-
-  private void appendLineNumberForSideA(final SafeHtmlBuilder m, final int idx) {
-    m.openTd();
-    m.setStyleName(Gerrit.RESOURCES.css().lineNumber());
-    m.addStyleName(Gerrit.RESOURCES.css().rightBorder());
-    m.append(SafeHtml.asis("<a href=\"javascript:void(0)\">"+ (idx + 1) + "</a>"));
-    m.closeTd();
-  }
-
-  private void padLineNumberOnTableHeaderForSideB(final SafeHtmlBuilder m) {
-    m.openTd();
-    m.setStyleName(Gerrit.RESOURCES.css().lineNumber());
-    m.addStyleName(Gerrit.RESOURCES.css().fileColumnHeader());
-    m.closeTd();
-  }
-
-  private void padLineNumberOnTableHeaderForSideA(final SafeHtmlBuilder m) {
-    m.openTd();
-    m.setStyleName(Gerrit.RESOURCES.css().lineNumber());
-    m.addStyleName(Gerrit.RESOURCES.css().fileColumnHeader());
-    m.addStyleName(Gerrit.RESOURCES.css().rightBorder());
-    m.closeTd();
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedPatchScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedPatchScreen.java
deleted file mode 100644
index e587aac..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedPatchScreen.java
+++ /dev/null
@@ -1,564 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.patches;
-
-import com.google.gerrit.client.Dispatcher;
-import com.google.gerrit.client.ErrorDialog;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.RpcStatus;
-import com.google.gerrit.client.diff.DiffApi;
-import com.google.gerrit.client.diff.DiffInfo;
-import com.google.gerrit.client.info.WebLinkInfo;
-import com.google.gerrit.client.projects.ConfigInfoCache;
-import com.google.gerrit.client.rpc.CallbackGroup;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.ScreenLoadCallback;
-import com.google.gerrit.client.ui.CommentLinkProcessor;
-import com.google.gerrit.client.ui.InlineHyperlink;
-import com.google.gerrit.client.ui.ListenableAccountDiffPreference;
-import com.google.gerrit.client.ui.Screen;
-import com.google.gerrit.common.data.PatchScript;
-import com.google.gerrit.common.data.PatchSetDetail;
-import com.google.gerrit.extensions.client.DiffPreferencesInfo;
-import com.google.gerrit.prettify.client.ClientSideFormatter;
-import com.google.gerrit.prettify.client.PrettyFactory;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gwt.core.client.Scheduler;
-import com.google.gwt.core.client.Scheduler.ScheduledCommand;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.logical.shared.ValueChangeEvent;
-import com.google.gwt.event.logical.shared.ValueChangeHandler;
-import com.google.gwt.event.shared.HandlerRegistration;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.ImageResourceRenderer;
-import com.google.gwtexpui.globalkey.client.GlobalKey;
-import com.google.gwtexpui.globalkey.client.KeyCommand;
-import com.google.gwtexpui.globalkey.client.KeyCommandSet;
-
-import java.util.Collections;
-import java.util.List;
-
-public class UnifiedPatchScreen extends Screen implements
-    CommentEditorContainer {
-  static final PrettyFactory PRETTY = ClientSideFormatter.FACTORY;
-  static final short LARGE_FILE_CONTEXT = 100;
-
-  /**
-   * What should be displayed in the top of the screen
-   */
-  public static enum TopView {
-    MAIN, COMMIT, PREFERENCES, PATCH_SETS, FILES
-  }
-
-  protected final Patch.Key patchKey;
-  protected PatchSetDetail patchSetDetail;
-  protected PatchTable fileList;
-  protected PatchSet.Id idSideA;
-  protected PatchSet.Id idSideB;
-  protected PatchScriptSettingsPanel settingsPanel;
-  protected TopView topView;
-  protected CommentLinkProcessor commentLinkProcessor;
-
-  private ReviewedPanels reviewedPanels;
-  private HistoryTable historyTable;
-  private FlowPanel topPanel;
-  private FlowPanel contentPanel;
-  private UnifiedDiffTable contentTable;
-  private CommitMessageBlock commitMessageBlock;
-  private NavLinks topNav;
-  private NavLinks bottomNav;
-
-  private int rpcSequence;
-  private PatchScript lastScript;
-
-  /** The index of the file we are currently looking at among the fileList */
-  private int patchIndex;
-  private ListenableAccountDiffPreference prefs;
-  private HandlerRegistration prefsHandler;
-
-  /** Keys that cause an action on this screen */
-  private KeyCommandSet keysNavigation;
-  private KeyCommandSet keysAction;
-  private HandlerRegistration regNavigation;
-  private HandlerRegistration regAction;
-  private boolean intralineFailure;
-  private boolean intralineTimeout;
-
-  public UnifiedPatchScreen(Patch.Key id, TopView top, PatchSet.Id baseId) {
-    patchKey = id;
-    topView = top;
-
-    idSideA = baseId; // null here means we're diff'ing from the Base
-    idSideB = id.getParentKey();
-
-    prefs = fileList != null
-        ? fileList.getPreferences()
-        : new ListenableAccountDiffPreference();
-    if (Gerrit.isSignedIn()) {
-      prefs.reset();
-    }
-    reviewedPanels = new ReviewedPanels();
-    settingsPanel = new PatchScriptSettingsPanel(prefs);
-  }
-
-  @Override
-  public void notifyDraftDelta(int delta) {
-    lastScript = null;
-  }
-
-  @Override
-  public void remove(CommentEditorPanel panel) {
-    lastScript = null;
-  }
-
-  private void update(DiffPreferencesInfo dp) {
-    // Did the user just turn on auto-review?
-    if (!reviewedPanels.getValue() && prefs.getOld().manualReview
-        && !dp.manualReview) {
-      reviewedPanels.setValue(true);
-      reviewedPanels.setReviewedByCurrentUser(true);
-    }
-
-    if (lastScript != null && canReuse(dp, lastScript)) {
-      lastScript.setDiffPrefs(dp);
-      RpcStatus.INSTANCE.onRpcStart(null);
-      settingsPanel.setEnabled(false);
-      Scheduler.get().scheduleDeferred(new ScheduledCommand() {
-        @Override
-        public void execute() {
-          try {
-            onResult(lastScript, false /* not the first time */);
-          } finally {
-            RpcStatus.INSTANCE.onRpcComplete(null);
-          }
-        }
-      });
-    } else {
-      refresh(false);
-    }
-  }
-
-  private boolean canReuse(DiffPreferencesInfo dp, PatchScript last) {
-    if (last.getDiffPrefs().ignoreWhitespace != dp.ignoreWhitespace) {
-      // Whitespace ignore setting requires server computation.
-      return false;
-    }
-
-    final int ctx = dp.context;
-    if (ctx == DiffPreferencesInfo.WHOLE_FILE_CONTEXT
-        && !last.getA().isWholeFile()) {
-      // We don't have the entire file here, so we can't render it.
-      return false;
-    }
-
-    if (last.getDiffPrefs().context < ctx && !last.getA().isWholeFile()) {
-      // We don't have sufficient context.
-      return false;
-    }
-
-    if (dp.syntaxHighlighting && !last.getA().isWholeFile()) {
-      // We need the whole file to syntax highlight accurately.
-      return false;
-    }
-
-    return true;
-  }
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-
-    if (Gerrit.isSignedIn()) {
-      setTitleFarEast(reviewedPanels.top);
-    }
-
-    keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation());
-    keysNavigation.add(new UpToChangeCommand(patchKey.getParentKey(), 0, 'u'));
-    keysNavigation.add(new FileListCmd(0, 'f', PatchUtil.C.fileList()));
-
-    if (Gerrit.isSignedIn()) {
-      keysAction = new KeyCommandSet(Gerrit.C.sectionActions());
-      keysAction
-          .add(new ToggleReviewedCmd(0, 'm', PatchUtil.C.toggleReviewed()));
-      keysAction.add(new MarkAsReviewedAndGoToNextCmd(0, 'M', PatchUtil.C
-          .markAsReviewedAndGoToNext()));
-    }
-
-    historyTable = new HistoryTable(this);
-
-    commitMessageBlock = new CommitMessageBlock();
-
-    topPanel = new FlowPanel();
-    add(topPanel);
-
-    contentTable = new UnifiedDiffTable();
-    contentTable.fileList = fileList;
-
-    topNav = new NavLinks(keysNavigation, patchKey.getParentKey());
-    bottomNav = new NavLinks(null, patchKey.getParentKey());
-
-    add(topNav);
-    contentPanel = new FlowPanel();
-    contentPanel.setStyleName(Gerrit.RESOURCES.css().unifiedTable());
-
-    contentPanel.add(contentTable);
-    add(contentPanel);
-    add(bottomNav);
-    if (Gerrit.isSignedIn()) {
-      add(reviewedPanels.bottom);
-    }
-
-    if (fileList != null) {
-      displayNav();
-    }
-  }
-
-  private void displayNav() {
-    DiffApi.diff(idSideB, patchKey.getFileName())
-      .base(idSideA)
-      .webLinksOnly()
-      .get(new GerritCallback<DiffInfo>() {
-        @Override
-        public void onSuccess(DiffInfo diffInfo) {
-          topNav.display(patchIndex, fileList,
-              getLinks(), getWebLinks(diffInfo));
-          bottomNav.display(patchIndex, fileList,
-              getLinks(), getWebLinks(diffInfo));
-        }
-      });
-  }
-
-  private List<InlineHyperlink> getLinks() {
-    InlineHyperlink toSideBySideDiffLink = new InlineHyperlink();
-    toSideBySideDiffLink.setHTML(new ImageResourceRenderer().render(Gerrit.RESOURCES.sideBySideDiff()));
-    toSideBySideDiffLink.setTargetHistoryToken(getSideBySideDiffUrl());
-    toSideBySideDiffLink.setTitle(PatchUtil.C.sideBySideDiff());
-    return Collections.singletonList(toSideBySideDiffLink);
-  }
-
-  private List<WebLinkInfo> getWebLinks(DiffInfo diffInfo) {
-    return diffInfo.unifiedWebLinks();
-  }
-
-  private String getSideBySideDiffUrl() {
-    return Dispatcher.toPatch("sidebyside", idSideA,
-        new Patch.Key(idSideB, patchKey.getFileName()));
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-
-    if (patchSetDetail == null) {
-      PatchUtil.CHANGE_SVC.patchSetDetail(idSideB,
-          new GerritCallback<PatchSetDetail>() {
-            @Override
-            public void onSuccess(PatchSetDetail result) {
-              patchSetDetail = result;
-              if (fileList == null) {
-                fileList = new PatchTable(prefs);
-                fileList.display(idSideA, result);
-                patchIndex = fileList.indexOf(patchKey);
-              }
-              refresh(true);
-            }
-          });
-    } else {
-      refresh(true);
-    }
-  }
-
-  @Override
-  protected void onUnload() {
-    if (prefsHandler != null) {
-      prefsHandler.removeHandler();
-      prefsHandler = null;
-    }
-    if (regNavigation != null) {
-      regNavigation.removeHandler();
-      regNavigation = null;
-    }
-    if (regAction != null) {
-      regAction.removeHandler();
-      regAction = null;
-    }
-    super.onUnload();
-  }
-
-  @Override
-  public void registerKeys() {
-    super.registerKeys();
-    contentTable.setRegisterKeys(contentTable.isVisible());
-    if (regNavigation != null) {
-      regNavigation.removeHandler();
-      regNavigation = null;
-    }
-    regNavigation = GlobalKey.add(this, keysNavigation);
-    if (regAction != null) {
-      regAction.removeHandler();
-      regAction = null;
-    }
-    if (keysAction != null) {
-      regAction = GlobalKey.add(this, keysAction);
-    }
-  }
-
-  public PatchSet.Id getSideA() {
-    return idSideA;
-  }
-
-  public Patch.Key getPatchKey() {
-    return patchKey;
-  }
-
-  public int getPatchIndex() {
-    return patchIndex;
-  }
-
-  public PatchSetDetail getPatchSetDetail() {
-    return patchSetDetail;
-  }
-
-  public PatchTable getFileList() {
-    return fileList;
-  }
-
-  public TopView getTopView() {
-    return topView;
-  }
-
-  protected void refresh(final boolean isFirst) {
-    final int rpcseq = ++rpcSequence;
-    lastScript = null;
-    settingsPanel.setEnabled(false);
-    reviewedPanels.populate(patchKey, fileList, patchIndex);
-    if (isFirst && fileList != null && fileList.isLoaded()) {
-      fileList.movePointerTo(patchKey);
-    }
-
-    CallbackGroup cb = new CallbackGroup();
-    ConfigInfoCache.get(patchSetDetail.getProject(),
-        cb.add(new AsyncCallback<ConfigInfoCache.Entry>() {
-          @Override
-          public void onSuccess(ConfigInfoCache.Entry result) {
-            commentLinkProcessor = result.getCommentLinkProcessor();
-            contentTable.setCommentLinkProcessor(commentLinkProcessor);
-            setTheme(result.getTheme());
-          }
-
-          @Override
-          public void onFailure(Throwable caught) {
-            // Handled by ScreenLoadCallback.onFailure.
-          }
-        }));
-    PatchUtil.PATCH_SVC.patchScript(patchKey, idSideA, idSideB,
-        settingsPanel.getValue(), cb.addFinal(
-            new ScreenLoadCallback<PatchScript>(this) {
-              @Override
-              protected void preDisplay(final PatchScript result) {
-                if (rpcSequence == rpcseq) {
-                  onResult(result, isFirst);
-                }
-              }
-
-              @Override
-              public void onFailure(final Throwable caught) {
-                if (rpcSequence == rpcseq) {
-                  settingsPanel.setEnabled(true);
-                  super.onFailure(caught);
-                }
-              }
-        }));
-  }
-
-  private void onResult(final PatchScript script, final boolean isFirst) {
-    final String path = PatchTable.getDisplayFileName(patchKey);
-    String fileName = path;
-    final int last = fileName.lastIndexOf('/');
-    if (last >= 0) {
-      fileName = fileName.substring(last + 1);
-    }
-
-    setWindowTitle(fileName);
-    setPageTitle(path);
-
-    if (idSideB.equals(patchSetDetail.getPatchSet().getId())) {
-      commitMessageBlock.setVisible(true);
-      commitMessageBlock.display(patchSetDetail.getInfo().getMessage(),
-          commentLinkProcessor);
-    } else {
-      commitMessageBlock.setVisible(false);
-      PatchUtil.CHANGE_SVC.patchSetDetail(idSideB,
-          new GerritCallback<PatchSetDetail>() {
-            @Override
-            public void onSuccess(PatchSetDetail result) {
-              commitMessageBlock.setVisible(true);
-              commitMessageBlock.display(result.getInfo().getMessage(),
-                  commentLinkProcessor);
-            }
-          });
-    }
-
-    historyTable.display(script.getHistory());
-
-    for (Patch p : patchSetDetail.getPatches()) {
-      if (p.getKey().equals(patchKey)) {
-        if (p.getPatchType().equals(Patch.PatchType.BINARY)) {
-          contentTable.isDisplayBinary = true;
-        }
-        break;
-      }
-    }
-
-    if (script.isHugeFile()) {
-      DiffPreferencesInfo dp = script.getDiffPrefs();
-      int context = dp.context;
-      if (context == DiffPreferencesInfo.WHOLE_FILE_CONTEXT) {
-        context = Short.MAX_VALUE;
-      } else if (context > Short.MAX_VALUE) {
-        context = Short.MAX_VALUE;
-      }
-      dp.context = Math.min(context, LARGE_FILE_CONTEXT);
-      dp.syntaxHighlighting = false;
-      script.setDiffPrefs(dp);
-    }
-
-    contentTable.display(patchKey, idSideA, idSideB, script, patchSetDetail);
-    contentTable.display(script.getCommentDetail(), script.isExpandAllComments());
-    contentTable.finishDisplay();
-    contentTable.setRegisterKeys(isCurrentView());
-
-    settingsPanel.setEnableSmallFileFeatures(!script.isHugeFile());
-    settingsPanel.setEnableIntralineDifference(script.hasIntralineDifference());
-    settingsPanel.setEnabled(true);
-    lastScript = script;
-
-    if (fileList != null) {
-      displayNav();
-    }
-
-    if (Gerrit.isSignedIn()) {
-      boolean isReviewed = false;
-      if (isFirst && !prefs.get().manualReview) {
-        isReviewed = true;
-        reviewedPanels.setReviewedByCurrentUser(isReviewed);
-      } else {
-        for (Patch p : patchSetDetail.getPatches()) {
-          if (p.getKey().equals(patchKey)) {
-            isReviewed = p.isReviewedByCurrentUser();
-            break;
-          }
-        }
-      }
-      reviewedPanels.setValue(isReviewed);
-    }
-
-    intralineFailure = isFirst && script.hasIntralineFailure();
-    intralineTimeout = isFirst && script.hasIntralineTimeout();
-  }
-
-  @Override
-  public void onShowView() {
-    super.onShowView();
-    if (prefsHandler == null) {
-      prefsHandler = prefs.addValueChangeHandler(
-          new ValueChangeHandler<DiffPreferencesInfo>() {
-            @Override
-            public void onValueChange(ValueChangeEvent<DiffPreferencesInfo> event) {
-              update(event.getValue());
-            }
-          });
-    }
-    if (intralineFailure) {
-      intralineFailure = false;
-      new ErrorDialog(PatchUtil.C.intralineFailure()).show();
-    } else if (intralineTimeout) {
-      intralineTimeout = false;
-      new ErrorDialog(PatchUtil.C.intralineTimeout()).setText(
-          Gerrit.C.warnTitle()).show();
-    }
-    if (topView != null && prefs.get().retainHeader) {
-      setTopView(topView);
-    }
-  }
-
-  public void setTopView(TopView tv) {
-    topView = tv;
-    topPanel.clear();
-    switch(tv) {
-      case COMMIT:      topPanel.add(commitMessageBlock);
-        break;
-      case PREFERENCES: topPanel.add(settingsPanel);
-        break;
-      case PATCH_SETS:  topPanel.add(historyTable);
-        break;
-      case FILES:       topPanel.add(fileList);
-        break;
-      case MAIN:
-        break;
-    }
-  }
-
-  public class FileListCmd extends KeyCommand {
-    public FileListCmd(int mask, int key, String help) {
-      super(mask, key, help);
-    }
-
-    @Override
-    public void onKeyPress(final KeyPressEvent event) {
-      if (fileList == null || fileList.isAttached()) {
-        final PatchSet.Id psid = patchKey.getParentKey();
-        fileList = new PatchTable(prefs);
-        fileList.setSavePointerId("PatchTable " + psid);
-        PatchUtil.CHANGE_SVC.patchSetDetail(psid,
-            new GerritCallback<PatchSetDetail>() {
-              @Override
-              public void onSuccess(final PatchSetDetail result) {
-                fileList.display(idSideA, result);
-              }
-            });
-      }
-
-      final PatchBrowserPopup p = new PatchBrowserPopup(patchKey, fileList);
-      p.open();
-    }
-  }
-
-  public class ToggleReviewedCmd extends KeyCommand {
-    public ToggleReviewedCmd(int mask, int key, String help) {
-      super(mask, key, help);
-    }
-
-    @Override
-    public void onKeyPress(final KeyPressEvent event) {
-      final boolean isReviewed = !reviewedPanels.getValue();
-      reviewedPanels.setValue(isReviewed);
-      reviewedPanels.setReviewedByCurrentUser(isReviewed);
-    }
-  }
-
-  public class MarkAsReviewedAndGoToNextCmd extends KeyCommand {
-    public MarkAsReviewedAndGoToNextCmd(int mask, int key, String help) {
-      super(mask, key, help);
-    }
-
-    @Override
-    public void onKeyPress(final KeyPressEvent event) {
-      reviewedPanels.go();
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UpToChangeCommand.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UpToChangeCommand.java
deleted file mode 100644
index 970e33d3..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UpToChangeCommand.java
+++ /dev/null
@@ -1,35 +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.client.patches;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwtexpui.globalkey.client.KeyCommand;
-
-class UpToChangeCommand extends KeyCommand {
-  private final PatchSet.Id patchSetId;
-
-  UpToChangeCommand(PatchSet.Id patchSetId, int mask, int key) {
-    super(mask, key, PatchUtil.C.upToChange());
-    this.patchSetId = patchSetId;
-  }
-
-  @Override
-  public void onKeyPress(final KeyPressEvent event) {
-    Gerrit.display(PageLinks.toChange(patchSetId));
-  }
-}
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 322a354..96b25c5 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
@@ -69,7 +69,7 @@
   public final native NativeMap<ActionInfo> actions()
   /*-{ return this.actions; }-*/;
 
-  private final native String submitTypeRaw()
+  private native String submitTypeRaw()
   /*-{ return this.submit_type }-*/;
 
   public final ProjectState state() {
@@ -78,13 +78,13 @@
     }
     return ProjectState.valueOf(stateRaw());
   }
-  private final native String stateRaw()
+  private native String stateRaw()
   /*-{ return this.state }-*/;
 
   public final native MaxObjectSizeLimitInfo maxObjectSizeLimit()
   /*-{ return this.max_object_size_limit; }-*/;
 
-  private final native NativeMap<CommentLinkInfo> commentlinks0()
+  private native NativeMap<CommentLinkInfo> commentlinks0()
   /*-{ return this.commentlinks; }-*/;
   final List<FindReplace> commentlinks() {
     JsArray<CommentLinkInfo> cls = commentlinks0().values();
@@ -143,7 +143,7 @@
     public final InheritableBoolean configuredValue() {
       return InheritableBoolean.valueOf(configuredValueRaw());
     }
-    private final native String configuredValueRaw()
+    private native String configuredValueRaw()
     /*-{ return this.configured_value }-*/;
 
     public final void setConfiguredValue(InheritableBoolean v) {
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 fffdd3f..8e29e4c 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
@@ -228,43 +228,43 @@
     final void setUseContributorAgreements(InheritableBoolean v) {
       setUseContributorAgreementsRaw(v.name());
     }
-    private final native void setUseContributorAgreementsRaw(String v)
+    private native void setUseContributorAgreementsRaw(String v)
     /*-{ if(v)this.use_contributor_agreements=v; }-*/;
 
     final void setUseContentMerge(InheritableBoolean v) {
       setUseContentMergeRaw(v.name());
     }
-    private final native void setUseContentMergeRaw(String v)
+    private native void setUseContentMergeRaw(String v)
     /*-{ if(v)this.use_content_merge=v; }-*/;
 
     final void setUseSignedOffBy(InheritableBoolean v) {
       setUseSignedOffByRaw(v.name());
     }
-    private final native void setUseSignedOffByRaw(String v)
+    private native void setUseSignedOffByRaw(String v)
     /*-{ if(v)this.use_signed_off_by=v; }-*/;
 
     final void setRequireChangeId(InheritableBoolean v) {
       setRequireChangeIdRaw(v.name());
     }
-    private final native void setRequireChangeIdRaw(String v)
+    private native void setRequireChangeIdRaw(String v)
     /*-{ if(v)this.require_change_id=v; }-*/;
 
     final void setCreateNewChangeForAllNotInTarget(InheritableBoolean v) {
       setCreateNewChangeForAllNotInTargetRaw(v.name());
     }
-    private final native void setCreateNewChangeForAllNotInTargetRaw(String v)
+    private native void setCreateNewChangeForAllNotInTargetRaw(String v)
     /*-{ if(v)this.create_new_change_for_all_not_in_target=v; }-*/;
 
     final void setEnableSignedPush(InheritableBoolean v) {
       setEnableSignedPushRaw(v.name());
     }
-    private final native void setEnableSignedPushRaw(String v)
+    private native void setEnableSignedPushRaw(String v)
     /*-{ if(v)this.enable_signed_push=v; }-*/;
 
     final void setRequireSignedPush(InheritableBoolean v) {
       setRequireSignedPushRaw(v.name());
     }
-    private final native void setRequireSignedPushRaw(String v)
+    private native void setRequireSignedPushRaw(String v)
     /*-{ if(v)this.require_signed_push=v; }-*/;
 
     final native void setMaxObjectSizeLimit(String l)
@@ -273,13 +273,13 @@
     final void setSubmitType(SubmitType t) {
       setSubmitTypeRaw(t.name());
     }
-    private final native void setSubmitTypeRaw(String t)
+    private native void setSubmitTypeRaw(String t)
     /*-{ if(t)this.submit_type=t; }-*/;
 
     final void setState(ProjectState s) {
       setStateRaw(s.name());
     }
-    private final native void setStateRaw(String s)
+    private native void setStateRaw(String s)
     /*-{ if(s)this.state=s; }-*/;
 
     final void setPluginConfigValues(Map<String, Map<String, ConfigParameterValue>> pluginConfigValues) {
@@ -295,7 +295,7 @@
         setPluginConfigValuesRaw(configValues);
       }
     }
-    private final native void setPluginConfigValuesRaw(NativeMap<ConfigParameterValueMap> v)
+    private native void setPluginConfigValuesRaw(NativeMap<ConfigParameterValueMap> v)
     /*-{ this.plugin_config_values=v; }-*/;
   }
 
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 00a4034..eed9d1d 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
@@ -36,7 +36,7 @@
     return ProjectState.valueOf(getStringState());
   }
 
-  private final native String getStringState() /*-{ return this.state; }-*/;
+  private native String getStringState() /*-{ return this.state; }-*/;
 
   @Override
   public final String getDisplayString() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/RefInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/RefInfo.java
index 053dbd3..9801d60 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/RefInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/RefInfo.java
@@ -27,4 +27,4 @@
 
   protected RefInfo() {
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/CallbackGroup.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/CallbackGroup.java
index 071ca72..009deaf 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/CallbackGroup.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/CallbackGroup.java
@@ -78,23 +78,7 @@
 
   public <T> HttpCallback<T> add(HttpCallback<T> cb) {
     checkFinalAdded();
-    if (failed) {
-      cb.onFailure(failedThrowable);
-      return new HttpCallback<T>() {
-        @Override
-        public void onSuccess(HttpResponse<T> result) {
-        }
-
-        @Override
-        public void onFailure(Throwable caught) {
-        }
-      };
-    }
-
-    HttpCallbackImpl<T> w = new HttpCallbackImpl<>(cb);
-    callbacks.add(w);
-    remaining.add(w);
-    return w;
+    return handleAdd(cb);
   }
 
   public <T> Callback<T> addFinal(final AsyncCallback<T> cb) {
@@ -103,6 +87,12 @@
     return handleAdd(cb);
   }
 
+  public <T> HttpCallback<T> addFinal(final HttpCallback<T> cb) {
+    checkFinalAdded();
+    finalAdded = true;
+    return handleAdd(cb);
+  }
+
   public void done() {
     finalAdded = true;
     apply();
@@ -161,6 +151,26 @@
     return wrapper;
   }
 
+  private <T> HttpCallback<T> handleAdd(HttpCallback<T> cb) {
+    if (failed) {
+      cb.onFailure(failedThrowable);
+      return new HttpCallback<T>() {
+        @Override
+        public void onSuccess(HttpResponse<T> result) {
+        }
+
+        @Override
+        public void onFailure(Throwable caught) {
+        }
+      };
+    }
+
+    HttpCallbackImpl<T> w = new HttpCallbackImpl<>(cb);
+    callbacks.add(w);
+    remaining.add(w);
+    return w;
+  }
+
   private void checkFinalAdded() {
     if (finalAdded) {
       throw new IllegalStateException("final callback already added");
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/GerritCallback.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/GerritCallback.java
index daac7cf..cd44fac 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/GerritCallback.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/GerritCallback.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.client.ErrorDialog;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.NotSignedInDialog;
-import com.google.gerrit.common.errors.InactiveAccountException;
 import com.google.gerrit.common.errors.NameAlreadyUsedException;
 import com.google.gerrit.common.errors.NoSuchAccountException;
 import com.google.gerrit.common.errors.NoSuchEntityException;
@@ -42,9 +41,6 @@
       new NotSignedInDialog().center();
     } else if (isNoSuchEntity(caught)) {
       new ErrorDialog(Gerrit.C.notFoundBody()).center();
-    } else if (isInactiveAccount(caught)) {
-      new ErrorDialog(Gerrit.C.inactiveAccountBody()).center();
-
     } else if (isNoSuchAccount(caught)) {
       final String msg = caught.getMessage();
       final String who = msg.substring(NoSuchAccountException.MESSAGE.length());
@@ -97,11 +93,6 @@
             && caught.getMessage().equals(NoSuchEntityException.MESSAGE));
   }
 
-  protected static boolean isInactiveAccount(final Throwable caught) {
-    return caught instanceof RemoteJsonException
-        && caught.getMessage().startsWith(InactiveAccountException.MESSAGE);
-  }
-
   protected static boolean isNoSuchAccount(final Throwable caught) {
     return caught instanceof RemoteJsonException
         && caught.getMessage().startsWith(NoSuchAccountException.MESSAGE);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RpcConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RpcConstants.java
index 89e9367..620133d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RpcConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RpcConstants.java
@@ -18,7 +18,7 @@
 import com.google.gwt.i18n.client.Constants;
 
 public interface RpcConstants extends Constants {
-  public static final RpcConstants C = GWT.create(RpcConstants.class);
+  RpcConstants C = GWT.create(RpcConstants.class);
 
   String errorServerUnavailable();
   String errorRemoteJsonException();
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 8c80e55..eb3b1ff 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
@@ -20,19 +20,10 @@
 import com.google.gerrit.client.info.AccountInfo;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.UserIdentity;
 import com.google.gwt.user.client.ui.FlowPanel;
 
 /** Link to any user's account dashboard. */
 public class AccountLinkPanel extends FlowPanel {
-  public AccountLinkPanel(UserIdentity ident) {
-    this(AccountInfo.create(
-        ident.getAccount().get(),
-        ident.getName(),
-        ident.getEmail(),
-        ident.getUsername()));
-  }
-
   public AccountLinkPanel(AccountInfo info) {
     this(info, Change.Status.NEW);
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ChangeLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ChangeLink.java
index 241b354..1ae4489 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ChangeLink.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ChangeLink.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.core.client.GWT;
 
 public class ChangeLink extends InlineHyperlink {
@@ -26,19 +25,11 @@
   }
 
   protected Change.Id cid;
-  protected PatchSet.Id psid;
 
   public ChangeLink(final String text, final Change.Id c) {
     super(text, PageLinks.toChange(c));
     getElement().setPropertyString("href", permalink(c));
     cid = c;
-    psid = null;
-  }
-
-  public ChangeLink(final String text, final PatchSet.Id ps) {
-    super(text, PageLinks.toChange(ps));
-    cid = ps.getParentKey();
-    psid = ps;
   }
 
   @Override
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 f69e042..4e6f500 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
@@ -85,7 +85,7 @@
   static class BranchSuggestion implements Suggestion {
     private BranchInfo branch;
 
-    public BranchSuggestion(BranchInfo branch) {
+    BranchSuggestion(BranchInfo branch) {
       this.branch = branch;
     }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java
deleted file mode 100644
index 77f40df..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java
+++ /dev/null
@@ -1,265 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.ui;
-
-import com.google.gerrit.client.AvatarImage;
-import com.google.gerrit.client.FormatUtil;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.info.AccountInfo;
-import com.google.gwt.event.dom.client.BlurEvent;
-import com.google.gwt.event.dom.client.BlurHandler;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.DoubleClickEvent;
-import com.google.gwt.event.dom.client.DoubleClickHandler;
-import com.google.gwt.event.dom.client.FocusEvent;
-import com.google.gwt.event.dom.client.FocusHandler;
-import com.google.gwt.event.dom.client.HasBlurHandlers;
-import com.google.gwt.event.dom.client.HasDoubleClickHandlers;
-import com.google.gwt.event.dom.client.HasFocusHandlers;
-import com.google.gwt.event.shared.HandlerManager;
-import com.google.gwt.event.shared.HandlerRegistration;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.FlexTable;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.HTML;
-import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
-import com.google.gwt.user.client.ui.HasHorizontalAlignment;
-import com.google.gwt.user.client.ui.InlineLabel;
-import com.google.gwt.user.client.ui.Panel;
-import com.google.gwt.user.client.ui.Widget;
-import com.google.gwtexpui.safehtml.client.SafeHtml;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-
-import java.util.Date;
-
-public class CommentPanel extends Composite implements HasDoubleClickHandlers,
-    HasFocusHandlers, FocusHandler, HasBlurHandlers, BlurHandler {
-  private static final int SUMMARY_LENGTH = 75;
-  private final HandlerManager handlerManager = new HandlerManager(this);
-  private final FlowPanel body;
-  private final FlexTable header;
-  private final InlineLabel messageSummary;
-  private final FlowPanel content;
-  private final DoubleClickHTML messageText;
-  private CommentLinkProcessor commentLinkProcessor;
-  private FlowPanel buttons;
-  private boolean recent;
-
-  public CommentPanel(final AccountInfo author, final Date when, String message,
-      CommentLinkProcessor commentLinkProcessor) {
-    this(commentLinkProcessor);
-
-    setMessageText(message);
-    setAuthorNameText(author, FormatUtil.name(author));
-    setDateText(FormatUtil.shortFormatDayTime(when));
-
-    final CellFormatter fmt = header.getCellFormatter();
-    fmt.getElement(0, 1).setTitle(FormatUtil.nameEmail(author));
-    fmt.getElement(0, 3).setTitle(FormatUtil.mediumFormat(when));
-  }
-
-  protected CommentPanel(CommentLinkProcessor commentLinkProcessor) {
-    this.commentLinkProcessor = commentLinkProcessor;
-    body = new FlowPanel();
-    initWidget(body);
-    setStyleName(Gerrit.RESOURCES.css().commentPanel());
-
-    messageSummary = new InlineLabel();
-    messageSummary.setStyleName(Gerrit.RESOURCES.css().commentPanelSummary());
-
-    header = new FlexTable();
-    header.setStyleName(Gerrit.RESOURCES.css().commentPanelHeader());
-    header.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(ClickEvent event) {
-        setOpen(!isOpen());
-      }
-    });
-    header.setText(0, 1, "");
-    header.setWidget(0, 2, messageSummary);
-    header.setText(0, 3, "");
-    final CellFormatter fmt = header.getCellFormatter();
-    fmt.setStyleName(0, 1, Gerrit.RESOURCES.css().commentPanelAuthorCell());
-    fmt.setStyleName(0, 2, Gerrit.RESOURCES.css().commentPanelSummaryCell());
-    fmt.setStyleName(0, 3, Gerrit.RESOURCES.css().commentPanelDateCell());
-    fmt.setHorizontalAlignment(0, 3, HasHorizontalAlignment.ALIGN_RIGHT);
-    body.add(header);
-
-    content = new FlowPanel();
-    content.setStyleName(Gerrit.RESOURCES.css().commentPanelContent());
-    content.setVisible(false);
-    body.add(content);
-
-    messageText = new DoubleClickHTML();
-    messageText.setStyleName(Gerrit.RESOURCES.css().commentPanelMessage());
-    content.add(messageText);
-  }
-
-  @Override
-  public HandlerRegistration addDoubleClickHandler(DoubleClickHandler handler) {
-    return messageText.addDoubleClickHandler(handler);
-  }
-
-  protected void setMessageText(String message) {
-    if (message == null) {
-      message = "";
-    } else {
-      message = message.trim();
-    }
-
-    messageSummary.setText(summarize(message));
-    SafeHtml buf = new SafeHtmlBuilder().append(message).wikify();
-    buf = commentLinkProcessor.apply(buf);
-    SafeHtml.set(messageText, buf);
-  }
-
-  public void setAuthorNameText(final AccountInfo author, final String nameText) {
-    header.setWidget(0, 0, new AvatarImage(author));
-    header.setText(0, 1, nameText);
-    body.getElement().setAttribute("email", author.email());
-    body.getElement().setAttribute("name", author.name());
-  }
-
-  protected void setDateText(final String dateText) {
-    header.setText(0, 3, dateText);
-  }
-
-  protected void setMessageTextVisible(final boolean show) {
-    messageText.setVisible(show);
-  }
-
-  protected void addContent(final Widget w) {
-    if (buttons != null) {
-      content.insert(w, content.getWidgetIndex(buttons));
-    } else {
-      content.add(w);
-    }
-  }
-
-  /**
-   * Registers a {@link FocusHandler} for this comment panel.
-   * The comment panel is considered as being focused whenever any button in the
-   * comment panel gets focused.
-   *
-   * @param handler the focus handler to be registered
-   */
-  @Override
-  public HandlerRegistration addFocusHandler(final FocusHandler handler) {
-    return handlerManager.addHandler(FocusEvent.getType(), handler);
-  }
-
-  /**
-   * Registers a {@link BlurHandler} for this comment panel.
-   * The comment panel is considered as being blurred whenever any button in the
-   * comment panel gets blurred.
-   *
-   * @param handler the blur handler to be registered
-   */
-  @Override
-  public HandlerRegistration addBlurHandler(final BlurHandler handler) {
-    return handlerManager.addHandler(BlurEvent.getType(), handler);
-  }
-
-  protected void addButton(final Button button) {
-    // register focus and blur handler for each button, so that we can fire
-    // focus and blur events for the comment panel
-    button.addFocusHandler(this);
-    button.addBlurHandler(this);
-    getButtonPanel().add(button);
-  }
-
-  private Panel getButtonPanel() {
-    if (buttons == null) {
-      buttons = new FlowPanel();
-      buttons.setStyleName(Gerrit.RESOURCES.css().commentPanelButtons());
-      content.add(buttons);
-    }
-    return buttons;
-  }
-
-  @Override
-  public void onFocus(final FocusEvent event) {
-    // a button was focused -> fire focus event for the comment panel
-    handlerManager.fireEvent(event);
-  }
-
-  @Override
-  public void onBlur(final BlurEvent event) {
-    // a button was blurred -> fire blur event for the comment panel
-    handlerManager.fireEvent(event);
-  }
-
-  public void enableButtons(final boolean on) {
-    for (Widget w : getButtonPanel()) {
-      if (w instanceof Button) {
-        ((Button) w).setEnabled(on);
-      }
-    }
-  }
-
-  private static String summarize(final String message) {
-    if (message.length() < SUMMARY_LENGTH) {
-      return message;
-    }
-
-    int p = 0;
-    final StringBuilder r = new StringBuilder();
-    while (r.length() < SUMMARY_LENGTH) {
-      final int e = message.indexOf(' ', p);
-      if (e < 0) {
-        break;
-      }
-
-      final String word = message.substring(p, e).trim();
-      if (SUMMARY_LENGTH <= r.length() + word.length() + 1) {
-        break;
-      }
-      if (r.length() > 0) {
-        r.append(' ');
-      }
-      r.append(word);
-      p = e + 1;
-    }
-    r.append(" \u2026");
-    return r.toString();
-  }
-
-  public boolean isOpen() {
-    return content.isVisible();
-  }
-
-  public void setOpen(final boolean open) {
-    messageSummary.setVisible(!open);
-    content.setVisible(open);
-  }
-
-  public boolean isRecent() {
-    return recent;
-  }
-
-  public void setRecent(final boolean r) {
-    recent = r;
-  }
-
-  private static class DoubleClickHTML extends HTML implements
-      HasDoubleClickHandlers {
-    @Override
-    public HandlerRegistration addDoubleClickHandler(DoubleClickHandler handler) {
-      return addDomHandler(handler, DoubleClickEvent.getType());
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentedActionDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentedActionDialog.java
index 60b5f93..c21d5dc 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentedActionDialog.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentedActionDialog.java
@@ -35,7 +35,7 @@
   protected final FlowPanel contentPanel;
   protected FocusWidget focusOn;
 
-  protected boolean sent = false;
+  protected boolean sent;
 
   public CommentedActionDialog(final String title, final String heading) {
     super(/* auto hide */false, /* modal */true);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CreateChangeDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CreateChangeDialog.java
index e398e78..7cda8a3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CreateChangeDialog.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CreateChangeDialog.java
@@ -98,7 +98,7 @@
   static class BranchSuggestion implements Suggestion {
     private BranchInfo branch;
 
-    public BranchSuggestion(BranchInfo branch) {
+    BranchSuggestion(BranchInfo branch) {
       this.branch = branch;
     }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ExpandAllCommand.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ExpandAllCommand.java
deleted file mode 100644
index 9abf135..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ExpandAllCommand.java
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.ui;
-
-import com.google.gwt.user.client.Command;
-import com.google.gwt.user.client.ui.Panel;
-import com.google.gwt.user.client.ui.Widget;
-
-/** Expands all {@link CommentPanel} in a parent panel. */
-public class ExpandAllCommand implements Command {
-  private final Panel panel;
-  protected final boolean open;
-
-  public ExpandAllCommand(final Panel p, final boolean isOpen) {
-    panel = p;
-    open = isOpen;
-  }
-
-  @Override
-  public void execute() {
-    for (final Widget w : panel) {
-      if (w instanceof CommentPanel) {
-        expand((CommentPanel) w);
-      }
-    }
-  }
-
-  protected void expand(final CommentPanel w) {
-    w.setOpen(open);
-  }
-}
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 88ee293..e77bc10 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
@@ -222,9 +222,9 @@
   protected static class MyFlexTable extends FlexTable {
   }
 
-  private static final native <ItemType> void setRowItem(Element td, ItemType c)
+  private static native <ItemType> void setRowItem(Element td, ItemType c)
   /*-{ td['__gerritRowItem'] = c; }-*/;
 
-  private static final native <ItemType> ItemType getRowItem(Element td)
+  private static native <ItemType> ItemType getRowItem(Element td)
   /*-{ return td['__gerritRowItem']; }-*/;
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableAccountDiffPreference.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableAccountDiffPreference.java
deleted file mode 100644
index 0c31f41..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableAccountDiffPreference.java
+++ /dev/null
@@ -1,55 +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.client.ui;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.account.Util;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.extensions.client.DiffPreferencesInfo;
-import com.google.gwtjsonrpc.common.VoidResult;
-
-public class ListenableAccountDiffPreference
-    extends ListenableOldValue<DiffPreferencesInfo> {
-
-  public ListenableAccountDiffPreference() {
-    reset();
-  }
-
-  public void save(final GerritCallback<VoidResult> cb) {
-    if (Gerrit.isSignedIn()) {
-      Util.ACCOUNT_SVC.changeDiffPreferences(get(),
-          new GerritCallback<VoidResult>() {
-        @Override
-        public void onSuccess(VoidResult result) {
-          Gerrit.setDiffPreferences(get());
-          cb.onSuccess(result);
-        }
-
-        @Override
-        public void onFailure(Throwable caught) {
-          cb.onFailure(caught);
-        }
-      });
-    }
-  }
-
-  public void reset() {
-    if (Gerrit.isSignedIn() && Gerrit.getDiffPreferences() != null) {
-      set(Gerrit.getDiffPreferences());
-    } else {
-      set(DiffPreferencesInfo.defaults());
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableOldValue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableOldValue.java
deleted file mode 100644
index 758dff4..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableOldValue.java
+++ /dev/null
@@ -1,34 +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.client.ui;
-
-public class ListenableOldValue<T> extends ListenableValue<T> {
-
-  private T oldValue;
-
-  public T getOld() {
-    return oldValue;
-  }
-
-  @Override
-  public void set(final T value) {
-    try {
-      oldValue = get();
-      super.set(value);
-    } finally {
-      oldValue = null; // allow it to be gced before the next event
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableValue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableValue.java
deleted file mode 100644
index c8bb12e..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableValue.java
+++ /dev/null
@@ -1,59 +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.client.ui;
-
-import com.google.gwt.event.logical.shared.HasValueChangeHandlers;
-import com.google.gwt.event.logical.shared.ValueChangeEvent;
-import com.google.gwt.event.logical.shared.ValueChangeHandler;
-import com.google.gwt.event.shared.GwtEvent;
-import com.google.gwt.event.shared.HandlerManager;
-import com.google.gwt.event.shared.HandlerRegistration;
-
-
-public class ListenableValue<T> implements HasValueChangeHandlers<T> {
-
-  private HandlerManager manager = new HandlerManager(this);
-
-  private T value;
-
-  public T get() {
-    return value;
-  }
-
-  public void set(final T value) {
-    this.value = value;
-    fireEvent(new ValueChangeEvent<T>(value) {});
-  }
-
-  @Override
-  public void fireEvent(GwtEvent<?> event) {
-    manager.fireEvent(event);
-  }
-
-  @Override
-  public HandlerRegistration addValueChangeHandler(
-      ValueChangeHandler<T> handler) {
-    return manager.addHandler(ValueChangeEvent.getType(), handler);
-  }
-
-  public int getHandlerCount() {
-    return manager.getHandlerCount(ValueChangeEvent.getType());
-  }
-
-  public ValueChangeHandler<?> getHandler(int index) {
-    return manager.getHandler(ValueChangeEvent.getType(), index);
-  }
-
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/MorphingTabPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/MorphingTabPanel.java
index db7ad3c..1b99707 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/MorphingTabPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/MorphingTabPanel.java
@@ -81,7 +81,7 @@
 
         /* Re-insert the widget right after the first visible widget found
            when scanning backwards from the current widget */
-        for (int pos = origPos -1; pos >=0 ; pos--) {
+        for (int pos = origPos - 1; pos >= 0 ; pos--) {
           int visiblePos = visibles.indexOf(widgets.get(pos));
           if (visiblePos != -1) {
             visibles.add(visiblePos + 1, w);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NpIntTextBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NpIntTextBox.java
index 0933153..3994381 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NpIntTextBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NpIntTextBox.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.client.ui;
 
-import com.google.gwt.dom.client.Element;
 import com.google.gwt.event.dom.client.KeyCodes;
 import com.google.gwt.event.dom.client.KeyDownEvent;
 import com.google.gwt.event.dom.client.KeyDownHandler;
@@ -31,11 +30,6 @@
     init();
   }
 
-  public NpIntTextBox(Element element) {
-    super(element);
-    init();
-  }
-
   private void init() {
     addKeyDownHandler(new KeyDownHandler() {
       @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/PatchLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/PatchLink.java
deleted file mode 100644
index 09244c9..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/PatchLink.java
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.ui;
-
-import com.google.gerrit.client.Dispatcher;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchSet;
-
-public class PatchLink extends InlineHyperlink {
-  private PatchLink(String text, String historyToken) {
-    super(text, historyToken);
-  }
-
-  public static class SideBySide extends PatchLink {
-    public SideBySide(String text, PatchSet.Id base, Patch.Key id) {
-      super(text, Dispatcher.toSideBySide(base, id));
-    }
-  }
-
-  public static class Unified extends PatchLink {
-    public Unified(String text, PatchSet.Id base, Patch.Key id) {
-      super(text, Dispatcher.toUnified(base, id));
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RebaseDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RebaseDialog.java
index 1a9fc4b..77c4e56 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RebaseDialog.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RebaseDialog.java
@@ -147,7 +147,7 @@
   private static class ChangeSuggestion implements Suggestion {
     private ChangeInfo change;
 
-    public ChangeSuggestion(ChangeInfo change) {
+    ChangeSuggestion(ChangeInfo change) {
       this.change = change;
     }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RemoteSuggestBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RemoteSuggestBox.java
index 50f991c..62b8f2e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RemoteSuggestBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RemoteSuggestBox.java
@@ -50,9 +50,9 @@
       public void onKeyDown(KeyDownEvent e) {
         submitOnSelection = false;
 
-        if (e.getNativeEvent().getKeyCode() == KeyCodes.KEY_ESCAPE) {
+        if (e.getNativeKeyCode() == KeyCodes.KEY_ESCAPE) {
           CloseEvent.fire(RemoteSuggestBox.this, RemoteSuggestBox.this);
-        } else if (e.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
+        } else if (e.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
           if (display.isSuggestionListShowing()) {
             if (textBox.getValue().equals(remoteSuggestOracle.getLast())) {
               submitOnSelection = true;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java
index 5192d6d..1068b3e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java
@@ -70,7 +70,7 @@
   public void registerKeys() {
   }
 
-  private static enum Cols {
+  private enum Cols {
     West, Title, East, FarEast
   }
 
@@ -184,7 +184,6 @@
     if (windowTitle != null) {
       Gerrit.setWindowTitle(this, windowTitle);
     }
-    Gerrit.updateMenus(this);
     Gerrit.EVENT_BUS.fireEvent(new ScreenLoadEvent(this));
     Gerrit.setQueryString(null);
     registerKeys();
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 45ba808..debd9a6 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
@@ -36,7 +36,7 @@
     return TYPE;
   }
 
-  public Screen getScreen(){
+  public Screen getScreen() {
     return screen;
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/TextBoxChangeListener.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/TextBoxChangeListener.java
deleted file mode 100644
index ca8ea1d..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/TextBoxChangeListener.java
+++ /dev/null
@@ -1,96 +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.client.ui;
-
-import com.google.gwt.core.client.Scheduler;
-import com.google.gwt.core.client.Scheduler.ScheduledCommand;
-import com.google.gwt.event.dom.client.FocusEvent;
-import com.google.gwt.event.dom.client.FocusHandler;
-import com.google.gwt.event.dom.client.KeyDownEvent;
-import com.google.gwt.event.dom.client.KeyDownHandler;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.dom.client.KeyPressHandler;
-import com.google.gwt.event.dom.client.MouseUpEvent;
-import com.google.gwt.event.dom.client.MouseUpHandler;
-import com.google.gwt.event.shared.GwtEvent;
-import com.google.gwt.user.client.ui.TextBoxBase;
-
-public abstract class TextBoxChangeListener implements KeyPressHandler, KeyDownHandler, MouseUpHandler {
-
-  private String oldText;
-
-  public TextBoxChangeListener(final TextBoxBase tb) {
-    oldText = tb.getText();
-
-    tb.addKeyPressHandler(this);
-
-    // Is there another way to capture middle button X11 pastes in browsers
-    // which do not yet support ONPASTE events (Firefox)?
-    tb.addMouseUpHandler(this);
-
-    // Resetting the "original text" on focus ensures that we are
-    // up to date with non-user updates of the text (calls to
-    // setText()...) and also up to date with user changes which
-    // occurred after enabling "widget".
-    tb.addFocusHandler(new FocusHandler() {
-        @Override
-        public void onFocus(FocusEvent event) {
-          oldText = tb.getText();
-        }
-      });
-
-    // CTRL-V Pastes in Chrome seem only detectable via BrowserEvents or
-    // KeyDownEvents, the latter is better.
-    tb.addKeyDownHandler(this);
-  }
-
-
-  @Override
-  public void onKeyPress(final KeyPressEvent e) {
-    on(e);
-  }
-
-  @Override
-  public void onKeyDown(final KeyDownEvent e) {
-    on(e);
-  }
-
-  @Override
-  public void onMouseUp(final MouseUpEvent e) {
-    on(e);
-  }
-
-  private void on(final GwtEvent<?> e) {
-    final TextBoxBase tb = (TextBoxBase) e.getSource();
-
-    if (!tb.getText().equals(oldText)) {
-      onTextChanged(tb.getText());
-      oldText = tb.getText();
-    } else {
-      // The text appears to not always get updated until the handlers complete.
-      Scheduler.get().scheduleDeferred(new ScheduledCommand() {
-        @Override
-        public void execute() {
-          if (!tb.getText().equals(oldText)) {
-            onTextChanged(tb.getText());
-            oldText = tb.getText();
-          }
-        }
-      });
-    }
-  }
-
-  public abstract void onTextChanged(String newText);
-}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/addon/AddonInjector.java b/gerrit-gwtui/src/main/java/net/codemirror/addon/AddonInjector.java
new file mode 100644
index 0000000..efed451
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/addon/AddonInjector.java
@@ -0,0 +1,95 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package net.codemirror.addon;
+
+import com.google.gwt.safehtml.shared.SafeUri;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+import net.codemirror.lib.Loader;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class AddonInjector {
+  private static final Map<String, SafeUri> addonUris = new HashMap<>();
+  static {
+    addonUris.put(Addons.I.merge_bundled().getName(),
+        Addons.I.merge_bundled().getSafeUri());
+  }
+
+  public static SafeUri getAddonScriptUri(String addon) {
+    return addonUris.get(addon);
+  }
+
+  private static boolean canLoad(String addon) {
+    return getAddonScriptUri(addon) != null;
+  }
+
+  private final Set<String> loading = new HashSet<>();
+  private int pending;
+  private AsyncCallback<Void> appCallback;
+
+  public AddonInjector add(String name) {
+    if (name == null) {
+      return this;
+    }
+
+    if (!canLoad(name)) {
+      Logger.getLogger("net.codemirror").log(
+        Level.WARNING,
+        "CodeMirror addon " + name + " not configured.");
+      return this;
+    }
+
+    loading.add(name);
+    return this;
+  }
+
+  public void inject(AsyncCallback<Void> appCallback) {
+    this.appCallback = appCallback;
+    for (String addon : loading) {
+      beginLoading(addon);
+    }
+    if (pending == 0) {
+      appCallback.onSuccess(null);
+    }
+  }
+
+  private void beginLoading(final String addon) {
+    pending++;
+    Loader.injectScript(
+      getAddonScriptUri(addon),
+      new AsyncCallback<Void>() {
+        @Override
+        public void onSuccess(Void result) {
+          pending--;
+          if (pending == 0) {
+            appCallback.onSuccess(null);
+          }
+        }
+
+        @Override
+        public void onFailure(Throwable caught) {
+          if (--pending == 0) {
+            appCallback.onFailure(caught);
+          }
+        }
+      });
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/addon/Addons.java b/gerrit-gwtui/src/main/java/net/codemirror/addon/Addons.java
new file mode 100644
index 0000000..7c8b362
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/addon/Addons.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package net.codemirror.addon;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.resources.client.ClientBundle;
+import com.google.gwt.resources.client.DataResource;
+import com.google.gwt.resources.client.DataResource.DoNotEmbed;
+
+public interface Addons extends ClientBundle {
+  Addons I = GWT.create(Addons.class);
+
+  @Source("merge_bundled.js") @DoNotEmbed DataResource merge_bundled();
+}
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFactory.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/BlameConfig.java
similarity index 62%
copy from gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFactory.java
copy to gerrit-gwtui/src/main/java/net/codemirror/lib/BlameConfig.java
index f68b629..7418795 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFactory.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/BlameConfig.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 The Android Open Source Project
+// Copyright (C) 2016 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,9 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.prettify.client;
+package net.codemirror.lib;
 
-/** Creates a new PrettyFormatter instance for one formatting run. */
-public interface PrettyFactory {
-  PrettyFormatter get();
+import com.google.gwt.i18n.client.Messages;
+
+public interface BlameConfig extends Messages {
+  String shortBlameMsg(String commitId, String date, String author);
+  String detailedBlameMsg(String commitId, String author, String time,
+      String msg);
 }
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/BlameConfig.properties b/gerrit-gwtui/src/main/java/net/codemirror/lib/BlameConfig.properties
new file mode 100644
index 0000000..658b50f
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/BlameConfig.properties
@@ -0,0 +1,2 @@
+shortBlameMsg={0} {1} {2}
+detailedBlameMsg=commit {0}\nAuthor: {1}\nDate: {2}\n\n{3}
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 ee4c050..be914f3 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java
@@ -138,7 +138,7 @@
     addLineClassNative(line, where.value(), className);
   }
 
-  private final native void addLineClassNative(int line, String where,
+  private native void addLineClassNative(int line, String where,
       String lineClass) /*-{
     this.addLineClass(line, where, lineClass)
   }-*/;
@@ -148,7 +148,7 @@
     addLineClassNative(line, where.value(), className);
   }
 
-  private final native void addLineClassNative(LineHandle line, String where,
+  private native void addLineClassNative(LineHandle line, String where,
       String lineClass) /*-{
     this.addLineClass(line, where, lineClass)
   }-*/;
@@ -158,7 +158,7 @@
     removeLineClassNative(line, where.value(), className);
   }
 
-  private final native void removeLineClassNative(int line, String where,
+  private native void removeLineClassNative(int line, String where,
       String lineClass) /*-{
     this.removeLineClass(line, where, lineClass)
   }-*/;
@@ -168,7 +168,7 @@
     removeLineClassNative(line, where.value(), className);
   }
 
-  private final native void removeLineClassNative(LineHandle line, String where,
+  private native void removeLineClassNative(LineHandle line, String where,
       String lineClass) /*-{
     this.removeLineClass(line, where, lineClass)
   }-*/;
@@ -395,6 +395,10 @@
     return this.setGutterMarker(line, gutterId, value);
   }-*/;
 
+  public final native boolean hasSearchHighlight() /*-{
+    return this.state.search && !!this.state.search.query;
+  }-*/;
+
   protected CodeMirror() {
   }
 
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/Extras.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/Extras.java
index 13186d1..d727e24 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/Extras.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/Extras.java
@@ -19,18 +19,31 @@
 import static net.codemirror.lib.CodeMirror.style;
 import static net.codemirror.lib.CodeMirror.LineClassWhere.WRAP;
 
+import com.google.gerrit.client.FormatUtil;
+import com.google.gerrit.client.RangeInfo;
+import com.google.gerrit.client.blame.BlameInfo;
 import com.google.gerrit.client.diff.DisplaySide;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gwt.core.client.GWT;
+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.i18n.client.DateTimeFormat;
 import com.google.gwt.user.client.DOM;
 
 import net.codemirror.lib.CodeMirror.LineHandle;
 
+import java.util.Date;
 import java.util.Objects;
 
 /** Additional features added to CodeMirror by Gerrit Code Review. */
 public class Extras {
+  private static final String ANNOTATION_GUTTER_ID = "CodeMirror-lint-markers";
+  private static final BlameConfig C = GWT.create(BlameConfig.class);
+
   static final native Extras get(CodeMirror c) /*-{ return c.gerritExtras }-*/;
-  private static final native void set(CodeMirror c, Extras e)
+  private static native void set(CodeMirror c, Extras e)
   /*-{ c.gerritExtras = e }-*/;
 
   static void attach(CodeMirror c) {
@@ -43,6 +56,7 @@
   private double charWidthPx;
   private double lineHeightPx;
   private LineHandle activeLine;
+  private boolean annotated;
 
   private Extras(CodeMirror cm) {
     this.cm = cm;
@@ -140,4 +154,68 @@
       activeLine = null;
     }
   }
+
+  public boolean isAnnotated() {
+    return annotated;
+  }
+
+  public final void clearAnnotations() {
+    JsArrayString gutters = ((JsArrayString) JsArrayString.createArray());
+    cm.setOption("gutters", gutters);
+    annotated = false;
+  }
+
+  public final void setAnnotations(JsArray<BlameInfo> blameInfos) {
+    if (blameInfos.length() > 0) {
+      setBlameInfo(blameInfos);
+      JsArrayString gutters = ((JsArrayString) JsArrayString.createArray());
+      gutters.push(ANNOTATION_GUTTER_ID);
+      cm.setOption("gutters", gutters);
+      annotated = true;
+      DateTimeFormat format = DateTimeFormat.getFormat(
+          DateTimeFormat.PredefinedFormat.DATE_SHORT);
+      JsArray<LintLine> annotations = JsArray.createArray().cast();
+      for (BlameInfo blameInfo : Natives.asList(blameInfos)) {
+        for (RangeInfo range : Natives.asList(blameInfo.ranges())) {
+          Date commitTime = new Date(blameInfo.time() * 1000L);
+          String shortId = blameInfo.id().substring(0, 8);
+          String shortBlame = C.shortBlameMsg(
+              shortId, format.format(commitTime), blameInfo.author());
+          String detailedBlame = C.detailedBlameMsg(blameInfo.id(),
+              blameInfo.author(), FormatUtil.mediumFormat(commitTime),
+              blameInfo.commitMsg());
+
+          annotations.push(LintLine.create(shortBlame, detailedBlame, shortId,
+              Pos.create(range.start() - 1)));
+        }
+      }
+      cm.setOption("lint", getAnnotation(annotations));
+    }
+  }
+
+  private native JavaScriptObject getAnnotation(JsArray<LintLine> annotations) /*-{
+     return {
+        getAnnotations: function(text, options, cm) { return annotations; }
+     };
+  }-*/;
+
+  public final native JsArray<BlameInfo> getBlameInfo() /*-{
+    return this.blameInfos;
+  }-*/;
+
+  public final native void setBlameInfo(JsArray<BlameInfo> blameInfos) /*-{
+    this['blameInfos'] = blameInfos;
+  }-*/;
+
+  public final void toggleAnnotation() {
+    toggleAnnotation(getBlameInfo());
+  }
+
+  public final void toggleAnnotation(JsArray<BlameInfo> blameInfos) {
+    if (isAnnotated()) {
+      clearAnnotations();
+    } else {
+      setAnnotations(blameInfos);
+    }
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/Lib.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/Lib.java
index a3b1cf4..f205ef9 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/Lib.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/Lib.java
@@ -21,7 +21,7 @@
 import com.google.gwt.resources.client.ExternalTextResource;
 
 interface Lib extends ClientBundle {
-  static final Lib I = GWT.create(Lib.class);
+  Lib I = GWT.create(Lib.class);
 
   @Source("cm.css")
   ExternalTextResource css();
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/LintLine.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/LintLine.java
new file mode 100644
index 0000000..b1e20c1
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/LintLine.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package net.codemirror.lib;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.dom.client.StyleInjector;
+
+public class LintLine extends JavaScriptObject {
+  public static LintLine create(String shortMsg, String msg, String sev,
+      Pos line) {
+    StyleInjector.inject(".CodeMirror-lint-marker-" + sev + " {\n"
+        + "  visibility: hidden;\n"
+        + "  text-overflow: ellipsis;\n"
+        + "  white-space: nowrap;\n"
+        + "  overflow: hidden;\n"
+        + "  position: relative;\n"
+        + "}\n"
+        + ".CodeMirror-lint-marker-" + sev + ":after {\n"
+        + "  content:'" + shortMsg + "';\n"
+        + "  visibility: visible;\n"
+        + "}");
+    return create(msg, sev, line, null);
+  }
+
+  public static native LintLine create(String msg, String sev, Pos f, Pos t) /*-{
+    return {
+      message : msg,
+      severity : sev,
+      from : f,
+      to : t
+    };
+  }-*/;
+
+  public final native String message() /*-{ return this.message; }-*/;
+  public final native String detailedMessage() /*-{ return this.message; }-*/;
+  public final native String severity() /*-{ return this.severity; }-*/;
+  public final native Pos from() /*-{ return this.from; }-*/;
+  public final native Pos to() /*-{ return this.to; }-*/;
+
+  protected LintLine() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/MergeView.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/MergeView.java
new file mode 100644
index 0000000..d7e1430
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/MergeView.java
@@ -0,0 +1,51 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package net.codemirror.lib;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.dom.client.Element;
+
+/** Object that represents a text marker within CodeMirror */
+public class MergeView extends JavaScriptObject {
+  public static MergeView create(Element p, Configuration cfg) {
+    MergeView mv = newMergeView(p, cfg);
+    Extras.attach(mv.leftOriginal());
+    Extras.attach(mv.editor());
+    return mv;
+  }
+
+  private static native MergeView newMergeView(Element p, Configuration cfg) /*-{
+    return $wnd.CodeMirror.MergeView(p, cfg);
+  }-*/;
+
+  public final native CodeMirror leftOriginal() /*-{
+    return this.leftOriginal();
+  }-*/;
+
+  public final native CodeMirror editor() /*-{
+    return this.editor();
+  }-*/;
+
+  public final native void setShowDifferences(boolean b) /*-{
+    this.setShowDifferences(b);
+  }-*/;
+
+  public final native Element getGapElement() /*-{
+    return $doc.getElementsByClassName("CodeMirror-merge-gap")[0];
+  }-*/;
+
+  protected MergeView() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/Vim.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/Vim.java
index f1dee69..84b4a6a 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/Vim.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/Vim.java
@@ -32,7 +32,7 @@
     }
     for (String key : new String[] {
       "Ctrl-C", "Ctrl-O", "Ctrl-P", "Ctrl-S",
-      "Ctrl-F", "Ctrl-B", "Ctrl-R"}) {
+      "Ctrl-F", "Ctrl-B", "Ctrl-R",}) {
       km.propagate(key);
     }
     for (int i = 0; i <= 9; i++) {
@@ -57,6 +57,10 @@
     $wnd.CodeMirror.Vim.handleKey(this, key)
   }-*/;
 
+  public final native void handleEx(String exCommand) /*-{
+    $wnd.CodeMirror.Vim.handleEx(this, exCommand);
+  }-*/;
+
   public final native boolean hasSearchHighlight() /*-{
     var v = this.state.vim;
     return v && v.searchState_ && !!v.searchState_.getOverlay();
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/style.css b/gerrit-gwtui/src/main/java/net/codemirror/lib/style.css
index aa4c0021d..022a800 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/style.css
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/style.css
@@ -16,6 +16,8 @@
 @external .CodeMirror;
 @external .CodeMirror-lines;
 @external .CodeMirror-linenumber;
+@external .CodeMirror-lint-markers;
+@external .CodeMirror-lint-tooltip;
 @external .CodeMirror-overlayscroll-horizontal;
 @external .CodeMirror-overlayscroll-vertical;
 @external .CodeMirror-scrollbar-filler;
@@ -94,3 +96,29 @@
   z-index: 2;
   cursor: text;
 }
+
+.CodeMirror-lint-markers {
+  width: 250px;
+}
+
+.CodeMirror-lint-tooltip {
+  background-color: infobackground;
+  border: 1px solid black;
+  border-radius: 4px 4px 4px 4px;
+  color: infotext;
+  font-family: monospace;
+  font-size: 10pt;
+  overflow: hidden;
+  padding: 2px 5px;
+  position: fixed;
+  white-space: pre;
+  white-space: pre-wrap;
+  z-index: 100;
+  max-width: 600px;
+  opacity: 0;
+  transition: opacity .4s;
+  -moz-transition: opacity .4s;
+  -webkit-transition: opacity .4s;
+  -o-transition: opacity .4s;
+  -ms-transition: opacity .4s;
+}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInfo.java b/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInfo.java
index 9c82c06..943be7e 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInfo.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInfo.java
@@ -91,6 +91,7 @@
       Modes.I.lua(),
       Modes.I.markdown(),
       Modes.I.mathematica(),
+      Modes.I.mbox(),
       Modes.I.mirc(),
       Modes.I.mllike(),
       Modes.I.modelica(),
@@ -106,6 +107,7 @@
       Modes.I.perl(),
       Modes.I.php(),
       Modes.I.pig(),
+      Modes.I.powershell(),
       Modes.I.properties(),
       Modes.I.protobuf(),
       Modes.I.puppet(),
@@ -116,6 +118,8 @@
       Modes.I.rst(),
       Modes.I.ruby(),
       Modes.I.rust(),
+      Modes.I.sas(),
+      Modes.I.sass(),
       Modes.I.scheme(),
       Modes.I.shell(),
       Modes.I.smalltalk(),
@@ -145,8 +149,10 @@
       Modes.I.verilog(),
       Modes.I.vhdl(),
       Modes.I.vue(),
+      Modes.I.webidl(),
       Modes.I.xml(),
       Modes.I.xquery(),
+      Modes.I.yacas(),
       Modes.I.yaml_frontmatter(),
       Modes.I.yaml(),
       Modes.I.z80(),
@@ -258,7 +264,7 @@
   public final native JsArrayString mimes()
   /*-{ return this.mimes || [this.mime] }-*/;
 
-  private final native JsArrayString ext()
+  private native JsArrayString ext()
   /*-{ return this.ext || [] }-*/;
 
   protected ModeInfo() {
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 593dc29..668a57f 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java
@@ -20,7 +20,7 @@
 import com.google.gwt.resources.client.DataResource.DoNotEmbed;
 
 public interface Modes extends ClientBundle {
-  public static final Modes I = GWT.create(Modes.class);
+  Modes I = GWT.create(Modes.class);
 
   @Source("apl.js") @DoNotEmbed DataResource apl();
   @Source("asciiarmor.js") @DoNotEmbed DataResource asciiarmor();
@@ -76,6 +76,7 @@
   @Source("lua.js") @DoNotEmbed DataResource lua();
   @Source("markdown.js") @DoNotEmbed DataResource markdown();
   @Source("mathematica.js") @DoNotEmbed DataResource mathematica();
+  @Source("mbox.js") @DoNotEmbed DataResource mbox();
   @Source("mirc.js") @DoNotEmbed DataResource mirc();
   @Source("mllike.js") @DoNotEmbed DataResource mllike();
   @Source("modelica.js") @DoNotEmbed DataResource modelica();
@@ -91,6 +92,7 @@
   @Source("perl.js") @DoNotEmbed DataResource perl();
   @Source("php.js") @DoNotEmbed DataResource php();
   @Source("pig.js") @DoNotEmbed DataResource pig();
+  @Source("powershell.js") @DoNotEmbed DataResource powershell();
   @Source("properties.js") @DoNotEmbed DataResource properties();
   @Source("protobuf.js") @DoNotEmbed DataResource protobuf();
   @Source("puppet.js") @DoNotEmbed DataResource puppet();
@@ -101,6 +103,7 @@
   @Source("rst.js") @DoNotEmbed DataResource rst();
   @Source("ruby.js") @DoNotEmbed DataResource ruby();
   @Source("rust.js") @DoNotEmbed DataResource rust();
+  @Source("sas.js") @DoNotEmbed DataResource sas();
   @Source("sass.js") @DoNotEmbed DataResource sass();
   @Source("scheme.js") @DoNotEmbed DataResource scheme();
   @Source("shell.js") @DoNotEmbed DataResource shell();
@@ -133,8 +136,10 @@
   @Source("verilog.js") @DoNotEmbed DataResource verilog();
   @Source("vhdl.js") @DoNotEmbed DataResource vhdl();
   @Source("vue.js") @DoNotEmbed DataResource vue();
+  @Source("webidl.js") @DoNotEmbed DataResource webidl();
   @Source("xml.js") @DoNotEmbed DataResource xml();
   @Source("xquery.js") @DoNotEmbed DataResource xquery();
+  @Source("yacas.js") @DoNotEmbed DataResource yacas();
   @Source("yaml-frontmatter.js") @DoNotEmbed DataResource yaml_frontmatter();
   @Source("yaml.js") @DoNotEmbed DataResource yaml();
   @Source("z80.js") @DoNotEmbed DataResource z80();
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/theme/ThemeLoader.java b/gerrit-gwtui/src/main/java/net/codemirror/theme/ThemeLoader.java
index c563c0a..20dd8c7 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/theme/ThemeLoader.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/theme/ThemeLoader.java
@@ -27,12 +27,50 @@
 /** Dynamically loads a known CodeMirror theme's CSS */
 public class ThemeLoader {
   private static final ExternalTextResource[] THEMES = {
+      Themes.I.day_3024(),
+      Themes.I.night_3024(),
+      Themes.I.abcdef(),
+      Themes.I.ambiance(),
+      Themes.I.base16_dark(),
+      Themes.I.base16_light(),
+      Themes.I.bespin(),
+      Themes.I.blackboard(),
+      Themes.I.cobalt(),
+      Themes.I.colorforth(),
+      Themes.I.dracula(),
       Themes.I.eclipse(),
       Themes.I.elegant(),
+      Themes.I.erlang_dark(),
+      Themes.I.hopscotch(),
+      Themes.I.icecoder(),
+      Themes.I.isotope(),
+      Themes.I.lesser_dark(),
+      Themes.I.liquibyte(),
+      Themes.I.material(),
+      Themes.I.mbo(),
+      Themes.I.mdn_like(),
       Themes.I.midnight(),
+      Themes.I.monokai(),
       Themes.I.neat(),
+      Themes.I.neo(),
       Themes.I.night(),
+      Themes.I.paraiso_dark(),
+      Themes.I.paraiso_light(),
+      Themes.I.pastel_on_dark(),
+      Themes.I.railscasts(),
+      Themes.I.rubyblue(),
+      Themes.I.seti(),
+      Themes.I.solarized(),
+      Themes.I.the_matrix(),
+      Themes.I.tomorrow_night_bright(),
+      Themes.I.tomorrow_night_eighties(),
+      Themes.I.ttcn(),
       Themes.I.twilight(),
+      Themes.I.vibrant_ink(),
+      Themes.I.xq_dark(),
+      Themes.I.xq_light(),
+      Themes.I.yeti(),
+      Themes.I.zenburn(),
   };
 
   private static final EnumSet<Theme> loaded = EnumSet.of(Theme.DEFAULT);
@@ -69,7 +107,7 @@
     }
   }
 
-  private static final ExternalTextResource findTheme(Theme theme) {
+  private static ExternalTextResource findTheme(Theme theme) {
     for (ExternalTextResource r : THEMES) {
       if (theme.name().toLowerCase().equals(r.getName())) {
         return r;
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/theme/Themes.java b/gerrit-gwtui/src/main/java/net/codemirror/theme/Themes.java
index ed0ffca..80304a3 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/theme/Themes.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/theme/Themes.java
@@ -19,14 +19,52 @@
 import com.google.gwt.resources.client.ExternalTextResource;
 
 public interface Themes extends ClientBundle {
-  public static final Themes I = GWT.create(Themes.class);
+  Themes I = GWT.create(Themes.class);
 
+  @Source("3024-day.css") ExternalTextResource day_3024();
+  @Source("3024-night.css") ExternalTextResource night_3024();
+  @Source("abcdef.css") ExternalTextResource abcdef();
+  @Source("ambiance.css") ExternalTextResource ambiance();
+  @Source("base16-dark.css") ExternalTextResource base16_dark();
+  @Source("base16-light.css") ExternalTextResource base16_light();
+  @Source("bespin.css") ExternalTextResource bespin();
+  @Source("blackboard.css") ExternalTextResource blackboard();
+  @Source("cobalt.css") ExternalTextResource cobalt();
+  @Source("colorforth.css") ExternalTextResource colorforth();
+  @Source("dracula.css") ExternalTextResource dracula();
   @Source("eclipse.css") ExternalTextResource eclipse();
   @Source("elegant.css") ExternalTextResource elegant();
+  @Source("erlang-dark.css") ExternalTextResource erlang_dark();
+  @Source("hopscotch.css") ExternalTextResource hopscotch();
+  @Source("icecoder.css") ExternalTextResource icecoder();
+  @Source("isotope.css") ExternalTextResource isotope();
+  @Source("lesser-dark.css") ExternalTextResource lesser_dark();
+  @Source("liquibyte.css") ExternalTextResource liquibyte();
+  @Source("material.css") ExternalTextResource material();
+  @Source("mbo.css") ExternalTextResource mbo();
+  @Source("mdn-like.css") ExternalTextResource mdn_like();
   @Source("midnight.css") ExternalTextResource midnight();
+  @Source("monokai.css") ExternalTextResource monokai();
   @Source("neat.css") ExternalTextResource neat();
+  @Source("neo.css") ExternalTextResource neo();
   @Source("night.css") ExternalTextResource night();
+  @Source("paraiso-dark.css") ExternalTextResource paraiso_dark();
+  @Source("paraiso-light.css") ExternalTextResource paraiso_light();
+  @Source("pastel-on-dark.css") ExternalTextResource pastel_on_dark();
+  @Source("railscasts.css") ExternalTextResource railscasts();
+  @Source("rubyblue.css") ExternalTextResource rubyblue();
+  @Source("seti.css") ExternalTextResource seti();
+  @Source("solarized.css") ExternalTextResource solarized();
+  @Source("the-matrix.css") ExternalTextResource the_matrix();
+  @Source("tomorrow-night-bright.css") ExternalTextResource tomorrow_night_bright();
+  @Source("tomorrow-night-eighties.css") ExternalTextResource tomorrow_night_eighties();
+  @Source("ttcn.css") ExternalTextResource ttcn();
   @Source("twilight.css") ExternalTextResource twilight();
+  @Source("vibrant-ink.css") ExternalTextResource vibrant_ink();
+  @Source("xq-dark.css") ExternalTextResource xq_dark();
+  @Source("xq-light.css") ExternalTextResource xq_light();
+  @Source("yeti.css") ExternalTextResource yeti();
+  @Source("zenburn.css") ExternalTextResource zenburn();
 
   // When adding a resource, update:
   // - static initializer in ThemeLoader
diff --git a/gerrit-httpd/BUCK b/gerrit-httpd/BUCK
index e17caed..d52963a 100644
--- a/gerrit-httpd/BUCK
+++ b/gerrit-httpd/BUCK
@@ -33,10 +33,10 @@
     '//lib/guice:guice',
     '//lib/guice:guice-assistedinject',
     '//lib/guice:guice-servlet',
+    '//lib/jgit/org.eclipse.jgit:jgit',
+    '//lib/jgit/org.eclipse.jgit.http.server:jgit-servlet',
     '//lib/log:api',
     '//lib/lucene:lucene-core-and-backward-codecs',
-    '@jgit//org.eclipse.jgit:jgit',
-    '@jgit//org.eclipse.jgit.http.server:jgit-servlet',
   ],
   provided_deps = ['//lib:servlet-api-3_1'],
   visibility = ['PUBLIC'],
@@ -69,9 +69,9 @@
     '//lib/easymock:easymock',
     '//lib/guice:guice',
     '//lib/guice:guice-servlet',
+    '//lib/jgit/org.eclipse.jgit:jgit',
+    '//lib/jgit/org.eclipse.jgit.junit:junit',
     '//lib/joda:joda-time',
-    '@jgit//org.eclipse.jgit:jgit',
-    '@jgit//org.eclipse.jgit.junit:junit',
   ],
   source_under_test = [':httpd'],
   # TODO(sop) Remove after Buck supports Eclipse
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 ab6cc90..5146b31 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
@@ -60,7 +60,7 @@
     filter("/a/*").through(authFilter);
   }
 
-  private boolean isHttpEnabled(){
+  private boolean isHttpEnabled() {
     return downloadConfig.getDownloadSchemes().contains(CoreDownloadSchemes.ANON_HTTP)
         || downloadConfig.getDownloadSchemes().contains(CoreDownloadSchemes.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 13b1a48..063f22b 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
@@ -98,7 +98,7 @@
 
     private final boolean enableReceive;
 
-    public Module(boolean enableReceive) {
+    Module(boolean enableReceive) {
       this.enableReceive = enableReceive;
     }
 
@@ -256,10 +256,8 @@
           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);
     }
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
index 177ff04..41acfa2 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/LoginUrlToken.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/LoginUrlToken.java
@@ -23,7 +23,7 @@
 public class LoginUrlToken {
   private static final String DEFAULT_TOKEN = '#' + PageLinks.MINE;
 
-  public static String getToken(final HttpServletRequest req){
+  public static String getToken(final HttpServletRequest req) {
     String token = req.getPathInfo();
     if (Strings.isNullOrEmpty(token)) {
       return DEFAULT_TOKEN;
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 38dd118..3ff060c 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
@@ -212,7 +212,7 @@
 
   private static final char[] LHEX =
       {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', //
-          'a', 'b', 'c', 'd', 'e', 'f'};
+          'a', 'b', 'c', 'd', 'e', 'f',};
 
   private static String LHEX(byte[] bin) {
     StringBuilder r = new StringBuilder(bin.length * 2);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectOAuthFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectOAuthFilter.java
index e80b5ab..7cadbae37 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectOAuthFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectOAuthFilter.java
@@ -221,7 +221,7 @@
       throw new ServletException("OAuth login provider configuration is"
           + " invalid: Must be of the form pluginName:providerName");
     }
-    defaultAuthPlugin= gitOAuthProvider.substring(0, splitPos);
+    defaultAuthPlugin = gitOAuthProvider.substring(0, splitPos);
     defaultAuthProvider = gitOAuthProvider.substring(splitPos + 1);
     OAuthLoginProvider provider = loginProviders.get(defaultAuthPlugin,
         defaultAuthProvider);
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 3083edd..45e5615 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
@@ -87,6 +87,9 @@
     serve("/watched").with(query("is:watched status:open"));
     serve("/starred").with(query("is:starred"));
 
+    // Forward PolyGerrit URLs to their respective GWT equivalents.
+    serveRegex("^/(c|q|x|admin|dashboard|settings)/(.*)").with(gerritUrl());
+
     serveRegex("^/settings/?$").with(screen(PageLinks.SETTINGS));
     serveRegex("^/register/?$").with(screen(PageLinks.REGISTER + "/"));
     serveRegex("^/([1-9][0-9]*)/?$").with(directChangeById());
@@ -117,6 +120,18 @@
     });
   }
 
+  private Key<HttpServlet> gerritUrl() {
+    return key(new HttpServlet() {
+      private static final long serialVersionUID = 1L;
+
+      @Override
+      protected void doGet(final HttpServletRequest req,
+          final HttpServletResponse rsp) throws IOException {
+        toGerrit(req.getRequestURI(), req, rsp);
+      }
+    });
+  }
+
   private Key<HttpServlet> screen(final String target) {
     return key(new HttpServlet() {
       private static final long serialVersionUID = 1L;
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 422a83d..3e3b7c4 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
@@ -16,8 +16,6 @@
 
 import static com.google.gerrit.extensions.registration.PrivateInternals_DynamicTypes.registerInParentInjectors;
 
-import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.extensions.webui.WebUiPlugin;
 import com.google.gerrit.httpd.auth.become.BecomeAnyAccountModule;
 import com.google.gerrit.httpd.auth.container.HttpAuthModule;
 import com.google.gerrit.httpd.auth.container.HttpsClientSslCertModule;
@@ -71,8 +69,6 @@
       install(new GitwebModule());
     }
 
-    DynamicSet.setOf(binder(), WebUiPlugin.class);
-
     install(new AsyncReceiveCommits.Module());
 
     bind(SocketAddress.class).annotatedWith(RemotePeer.class).toProvider(
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 88abe84..f144210 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
@@ -137,7 +137,8 @@
 
   String getRemoteUser(HttpServletRequest req) {
     String remoteUser = RemoteUserUtil.getRemoteUser(req, loginHeader);
-    return userNameToLowerCase ? remoteUser.toLowerCase(Locale.US) : remoteUser;
+    return (userNameToLowerCase && remoteUser != null) ?
+        remoteUser.toLowerCase(Locale.US) : remoteUser;
   }
 
   String getRemoteDisplayname(HttpServletRequest req) {
@@ -157,7 +158,7 @@
   }
 
   String getRemoteExternalIdToken(HttpServletRequest req) {
-    if(externalIdHeader != null) {
+    if (externalIdHeader != null) {
       return emptyToNull(req.getHeader(externalIdHeader));
     } else {
       return null;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ContextMapper.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ContextMapper.java
index 6afe52a..b51bfb9 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ContextMapper.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ContextMapper.java
@@ -25,7 +25,7 @@
   private final String base;
   private final String authorizedBase;
 
-  public ContextMapper(String contextPath) {
+  ContextMapper(String contextPath) {
     base = Strings.nullToEmpty(contextPath) + PLUGINS_PREFIX;
     authorizedBase = Strings.nullToEmpty(contextPath) + AUTHORIZED_PREFIX;
   }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpAutoRegisterModuleGenerator.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpAutoRegisterModuleGenerator.java
index 0e81a0d..8ae0e5c 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpAutoRegisterModuleGenerator.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpAutoRegisterModuleGenerator.java
@@ -14,33 +14,28 @@
 
 package com.google.gerrit.httpd.plugins;
 
-import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.server.plugins.AutoRegisterUtil.calculateBindAnnotation;
 
 import com.google.common.collect.LinkedListMultimap;
-import com.google.common.collect.Maps;
 import com.google.common.collect.Multimap;
 import com.google.gerrit.extensions.annotations.Export;
-import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.extensions.webui.JavaScriptPlugin;
-import com.google.gerrit.extensions.webui.WebUiPlugin;
-import com.google.gerrit.server.plugins.HttpModuleGenerator;
 import com.google.gerrit.server.plugins.InvalidPluginException;
+import com.google.gerrit.server.plugins.ModuleGenerator;
 import com.google.inject.Module;
 import com.google.inject.Scopes;
 import com.google.inject.TypeLiteral;
 import com.google.inject.servlet.ServletModule;
 
 import java.lang.annotation.Annotation;
+import java.util.HashMap;
 import java.util.Map;
 
 import javax.servlet.http.HttpServlet;
 
 class HttpAutoRegisterModuleGenerator extends ServletModule
-    implements HttpModuleGenerator {
-  private final Map<String, Class<HttpServlet>> serve = Maps.newHashMap();
+    implements ModuleGenerator {
+  private final Map<String, Class<HttpServlet>> serve = new HashMap<>();
   private final Multimap<TypeLiteral<?>, Class<?>> listeners = LinkedListMultimap.create();
-  private String javascript;
 
   @Override
   protected void configureServlets() {
@@ -58,10 +53,6 @@
       Annotation n = calculateBindAnnotation(impl);
       bind(type).annotatedWith(n).to(impl);
     }
-    if (javascript != null) {
-      DynamicSet.bind(binder(), WebUiPlugin.class).toInstance(
-          new JavaScriptPlugin(javascript));
-    }
   }
 
   @Override
@@ -89,14 +80,6 @@
   }
 
   @Override
-  public void export(String javascript) {
-    checkState(this.javascript == null,
-        "Multiple JavaScript plugins detected: %s, %s", this.javascript,
-        javascript);
-    this.javascript = javascript;
-  }
-
-  @Override
   public void listen(TypeLiteral<?> tl, Class<?> clazz) {
     listeners.put(tl, clazz);
   }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java
index 6b66bbd..3c72ec5 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java
@@ -18,7 +18,7 @@
 import com.google.gerrit.httpd.resources.ResourceKey;
 import com.google.gerrit.httpd.resources.ResourceWeigher;
 import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.plugins.HttpModuleGenerator;
+import com.google.gerrit.server.plugins.ModuleGenerator;
 import com.google.gerrit.server.plugins.ReloadPluginListener;
 import com.google.gerrit.server.plugins.StartPluginListener;
 import com.google.inject.internal.UniqueAnnotations;
@@ -51,7 +51,7 @@
       .annotatedWith(UniqueAnnotations.create())
       .to(LfsPluginServlet.class);
 
-    bind(HttpModuleGenerator.class)
+    bind(ModuleGenerator.class)
       .to(HttpAutoRegisterModuleGenerator.class);
 
     install(new CacheModule() {
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 4e635b2..2190fe0 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
@@ -66,8 +66,10 @@
 import java.nio.charset.Charset;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Enumeration;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentMap;
@@ -100,7 +102,7 @@
   private final int sshPort;
   private final RestApiServlet managerApi;
 
-  private List<Plugin> pending = Lists.newArrayList();
+  private List<Plugin> pending = new ArrayList<>();
   private ContextMapper wrapper;
   private final ConcurrentMap<String, PluginHolder> plugins
       = Maps.newConcurrentMap();
@@ -125,7 +127,7 @@
       int c = host.lastIndexOf(':');
       if (0 <= c) {
         sshHost = host.substring(0, c);
-        sshPort = Integer.parseInt(host.substring(c+1));
+        sshPort = Integer.parseInt(host.substring(c + 1));
       } else {
         sshHost = host;
         sshPort = 22;
@@ -346,7 +348,7 @@
       int nameOffset) throws IOException {
     if (!entries.isEmpty()) {
       md.append("## ").append(sectionTitle).append(" ##\n");
-      for(PluginEntry entry : entries) {
+      for (PluginEntry entry : entries) {
         String rsrc = entry.getName().substring(prefix.length());
         String entryTitle;
         if (rsrc.endsWith(".html")) {
@@ -370,10 +372,10 @@
       String prefix, String pluginName,
       PluginResourceKey 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();
+    List<PluginEntry> cmds = new ArrayList<>();
+    List<PluginEntry> servlets = new ArrayList<>();
+    List<PluginEntry> restApis = new ArrayList<>();
+    List<PluginEntry> docs = new ArrayList<>();
     PluginEntry about = null;
     Enumeration<PluginEntry> entries = scanner.entries();
     while (entries.hasMoreElements()) {
@@ -443,7 +445,7 @@
   private void sendMarkdownAsHtml(String md, String pluginName,
       PluginResourceKey cacheKey, HttpServletResponse res, long lastModifiedTime)
       throws UnsupportedEncodingException, IOException {
-    Map<String, String> macros = Maps.newHashMap();
+    Map<String, String> macros = new HashMap<>();
     macros.put("PLUGIN", pluginName);
     macros.put("SSH_HOST", sshHost);
     macros.put("SSH_PORT", "" + sshPort);
@@ -486,7 +488,6 @@
       String t = main.getValue(Attributes.Name.IMPLEMENTATION_TITLE);
       String n = main.getValue(Attributes.Name.IMPLEMENTATION_VENDOR);
       String v = main.getValue(Attributes.Name.IMPLEMENTATION_VERSION);
-      String u = main.getValue(Attributes.Name.IMPLEMENTATION_URL);
       String a = main.getValue("Gerrit-ApiVersion");
 
       html.append("<table class=\"plugin_info\">");
@@ -505,11 +506,6 @@
             .append(v)
             .append("</td></tr>\n");
       }
-      if (!Strings.isNullOrEmpty(u)) {
-        html.append("<tr><th>URL</th><td>")
-            .append(String.format("<a href=\"%s\">%s</a>", u, u))
-            .append("</td></tr>\n");
-      }
       if (!Strings.isNullOrEmpty(a)) {
         html.append("<tr><th>API Version</th><td>")
             .append(a)
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/LfsPluginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/LfsPluginServlet.java
index 6a9bf9f..af4776d 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/LfsPluginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/LfsPluginServlet.java
@@ -16,7 +16,6 @@
 
 import static javax.servlet.http.HttpServletResponse.SC_NOT_IMPLEMENTED;
 
-import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.httpd.resources.Resource;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -33,6 +32,7 @@
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.List;
 
 import javax.servlet.FilterChain;
@@ -55,7 +55,7 @@
   public static final String URL_REGEX =
       "^(?:/a)?(?:/p/|/)(.+)(?:/info/lfs/objects/batch)$";
 
-  private List<Plugin> pending = Lists.newArrayList();
+  private List<Plugin> pending = new ArrayList<>();
   private final String pluginName;
   private GuiceFilter filter;
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/PluginServletContext.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/PluginServletContext.java
index af34c5f..0889c85 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/PluginServletContext.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/PluginServletContext.java
@@ -208,7 +208,7 @@
     }
   }
 
-  static interface API {
+  interface API {
     String getContextPath();
     String getInitParameter(String name);
     @SuppressWarnings("rawtypes")
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 43c66db..e8efd72 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
@@ -18,7 +18,6 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.base.Strings;
-import com.google.common.collect.Lists;
 import com.google.common.hash.Hasher;
 import com.google.common.hash.Hashing;
 import com.google.common.primitives.Bytes;
@@ -115,7 +114,7 @@
     site = sp;
     refreshHeaderFooter = cfg.getBoolean("site", "refreshHeaderFooter", true);
     staticServlet = ss;
-    isNoteDbEnabled = migration.enabled();
+    isNoteDbEnabled = migration.readChanges();
     pluginsLoadTimeout = getPluginsLoadTimeout(cfg);
     getDiff = diffPref;
 
@@ -231,7 +230,7 @@
   }
 
   private void plugins(StringWriter w) {
-    List<String> urls = Lists.newArrayList();
+    List<String> urls = new ArrayList<>();
     for (WebUiPlugin u : plugins) {
       urls.add(String.format("plugins/%s/%s",
           u.getPluginName(),
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java
index d9aab24..05990e9 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java
@@ -205,6 +205,10 @@
   Resource getResource(String name) {
     try {
       Path p = getResourcePath(name);
+      if (p == null) {
+        log.warn(String.format("Path doesn't exist %s", name));
+        return null;
+      }
       return cache.get(p, newLoader(p));
     } catch (ExecutionException | IOException e) {
       log.warn(String.format("Cannot load static resource %s", name), e);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/SingleFileServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/SingleFileServlet.java
index d75a523..111145f 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/SingleFileServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/SingleFileServlet.java
@@ -19,7 +19,7 @@
 import java.nio.file.Path;
 
 /** Serve a single static file, regardless of path. */
-class SingleFileServlet extends ResourceServlet{
+class SingleFileServlet extends ResourceServlet {
   private static final long serialVersionUID = 1L;
 
   private final Path path;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java
index 46843fc..4dd21ae 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java
@@ -24,7 +24,6 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Multimap;
-import com.google.common.collect.Sets;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.Url;
@@ -40,6 +39,7 @@
 
 import java.io.IOException;
 import java.io.StringWriter;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.Map;
 import java.util.Set;
@@ -107,7 +107,7 @@
   }
 
   private static Set<String> query(HttpServletRequest req) {
-    Set<String> params = Sets.newHashSet();
+    Set<String> params = new HashSet<>();
     if (!Strings.isNullOrEmpty(req.getQueryString())) {
       for (String kvPair : Splitter.on('&').split(req.getQueryString())) {
         params.add(Iterables.getFirst(
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 c6ae883..4654655 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
@@ -32,6 +32,7 @@
 import static javax.servlet.http.HttpServletResponse.SC_OK;
 import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED;
 
+import com.google.common.base.CharMatcher;
 import com.google.common.base.Function;
 import com.google.common.base.Joiner;
 import com.google.common.base.Splitter;
@@ -40,9 +41,7 @@
 import com.google.common.collect.Iterables;
 import com.google.common.collect.LinkedHashMultimap;
 import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
 import com.google.common.collect.Multimap;
-import com.google.common.collect.Sets;
 import com.google.common.io.BaseEncoding;
 import com.google.common.io.CountingOutputStream;
 import com.google.common.math.IntMath;
@@ -123,12 +122,15 @@
 import java.lang.reflect.ParameterizedType;
 import java.lang.reflect.Type;
 import java.sql.Timestamp;
+import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.TreeMap;
 import java.util.concurrent.TimeUnit;
-import java.util.regex.Pattern;
 import java.util.zip.GZIPOutputStream;
 
 import javax.servlet.ServletException;
@@ -712,13 +714,13 @@
 
   private static void enablePartialGetFields(GsonBuilder gb,
       Multimap<String, String> config) {
-    final Set<String> want = Sets.newHashSet();
+    final Set<String> want = new HashSet<>();
     for (String p : config.get("fields")) {
       Iterables.addAll(want, OptionUtil.splitOptionValue(p));
     }
     if (!want.isEmpty()) {
       gb.addSerializationExclusionStrategy(new ExclusionStrategy() {
-        private final Map<String, String> names = Maps.newHashMap();
+        private final Map<String, String> names = new HashMap<>();
 
         @Override
         public boolean shouldSkipField(FieldAttributes field) {
@@ -796,7 +798,7 @@
       final BinaryResult src) throws IOException {
     TemporaryBuffer.Heap buf = heap(HEAP_EST_SIZE, Integer.MAX_VALUE);
     buf.write(JSON_MAGIC);
-    try(Writer w = new BufferedWriter(new OutputStreamWriter(buf, UTF_8));
+    try (Writer w = new BufferedWriter(new OutputStreamWriter(buf, UTF_8));
         JsonWriter json = new JsonWriter(w)) {
       json.setLenient(true);
       json.setHtmlSafe(true);
@@ -917,7 +919,7 @@
       }
     }
 
-    Map<String, RestView<RestResource>> r = Maps.newTreeMap();
+    Map<String, RestView<RestResource>> r = new TreeMap<>();
     for (String plugin : views.plugins()) {
       RestView<RestResource> action = views.get(plugin, name);
       if (action != null) {
@@ -950,7 +952,7 @@
     if (Strings.isNullOrEmpty(path)) {
       return Collections.emptyList();
     }
-    List<IdString> out = Lists.newArrayList();
+    List<IdString> out = new ArrayList<>();
     for (String p : Splitter.on('/').split(path)) {
       out.add(IdString.fromUrl(p));
     }
@@ -1037,9 +1039,8 @@
     }
   }
 
-  private static final Pattern IS_HTML = Pattern.compile("[<&]");
   private static boolean isMaybeHTML(String text) {
-    return IS_HTML.matcher(text).find();
+    return CharMatcher.anyOf("<&").matchesAnyOf(text);
   }
 
   private static boolean acceptsJson(HttpServletRequest req) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java
index 3c1c6e9..98fabd1 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.httpd.rpc;
 
-import com.google.gerrit.common.errors.CorruptEntityException;
 import com.google.gerrit.common.errors.InvalidQueryException;
 import com.google.gerrit.common.errors.NoSuchEntityException;
 import com.google.gerrit.common.errors.NoSuchGroupException;
@@ -102,8 +101,6 @@
       final AsyncCallback<T> callback, Exception e) {
     if (e.getCause() instanceof Failure) {
       callback.onFailure(e.getCause().getCause());
-    } else if (e.getCause() instanceof CorruptEntityException) {
-      callback.onFailure(e.getCause());
     } else if (e.getCause() instanceof NoSuchEntityException) {
       callback.onFailure(e.getCause());
     } else {
@@ -121,7 +118,7 @@
   }
 
   /** Arbitrary action to run with a database connection. */
-  public static interface Action<T> {
+  public interface Action<T> {
     /**
      * Perform this action, returning the onSuccess value.
      *
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/Handler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/Handler.java
index 1b2b990..9364764 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/Handler.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/Handler.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.httpd.rpc;
 
-import com.google.gerrit.common.errors.CorruptEntityException;
 import com.google.gerrit.common.errors.NoSuchEntityException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.NoSuchProjectException;
@@ -74,9 +73,6 @@
       if (e.getCause() instanceof BaseServiceImplementation.Failure) {
         callback.onFailure(e.getCause().getCause());
 
-      } else if (e.getCause() instanceof CorruptEntityException) {
-        callback.onFailure(e.getCause());
-
       } else if (e.getCause() instanceof NoSuchEntityException) {
         callback.onFailure(e.getCause());
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/UiRpcModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/UiRpcModule.java
index f3577b9..4398c78 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/UiRpcModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/UiRpcModule.java
@@ -15,8 +15,6 @@
 package com.google.gerrit.httpd.rpc;
 
 import com.google.gerrit.httpd.rpc.account.AccountModule;
-import com.google.gerrit.httpd.rpc.changedetail.ChangeModule;
-import com.google.gerrit.httpd.rpc.patch.PatchModule;
 import com.google.gerrit.httpd.rpc.project.ProjectModule;
 
 /** Registers servlets to answer RPCs from client UI. */
@@ -30,8 +28,6 @@
     rpc(SystemInfoServiceImpl.class);
 
     install(new AccountModule());
-    install(new ChangeModule());
-    install(new PatchModule());
     install(new ProjectModule());
   }
 }
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 a8c5210..ce47161 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
@@ -19,17 +19,12 @@
 import com.google.gerrit.common.data.AgreementInfo;
 import com.google.gerrit.common.errors.InvalidQueryException;
 import com.google.gerrit.common.errors.NoSuchEntityException;
-import com.google.gerrit.extensions.client.DiffPreferencesInfo;
-import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.httpd.rpc.BaseServiceImplementation;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountResource;
-import com.google.gerrit.server.account.SetDiffPreferences;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.query.QueryParseException;
@@ -41,9 +36,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-import org.eclipse.jgit.errors.ConfigInvalidException;
-
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
@@ -55,41 +47,17 @@
   private final ProjectControl.Factory projectControlFactory;
   private final AgreementInfoFactory.Factory agreementInfoFactory;
   private final Provider<ChangeQueryBuilder> queryBuilder;
-  private final SetDiffPreferences setDiff;
 
   @Inject
   AccountServiceImpl(final Provider<ReviewDb> schema,
       final Provider<IdentifiedUser> identifiedUser,
       final ProjectControl.Factory projectControlFactory,
       final AgreementInfoFactory.Factory agreementInfoFactory,
-      final Provider<ChangeQueryBuilder> queryBuilder,
-      SetDiffPreferences setDiff) {
+      final Provider<ChangeQueryBuilder> queryBuilder) {
     super(schema, identifiedUser);
     this.projectControlFactory = projectControlFactory;
     this.agreementInfoFactory = agreementInfoFactory;
     this.queryBuilder = queryBuilder;
-    this.setDiff = setDiff;
-  }
-
-  @Override
-  public void changeDiffPreferences(final DiffPreferencesInfo diffPref,
-      AsyncCallback<VoidResult> callback) {
-    run(callback, new Action<VoidResult>(){
-      @Override
-      public VoidResult run(ReviewDb db) throws OrmException {
-        if (!getUser().isIdentifiedUser()) {
-          throw new IllegalArgumentException("Not authenticated");
-        }
-        IdentifiedUser me = getUser().asIdentifiedUser();
-        try {
-          setDiff.apply(new AccountResource(me), diffPref);
-        } catch (AuthException | BadRequestException | ConfigInvalidException
-            | IOException e) {
-          throw new OrmException("Cannot save diff preferences", e);
-        }
-        return VoidResult.INSTANCE;
-      }
-    });
   }
 
   @Override
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java
index 0fff8ce..91afd97 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.httpd.rpc.account;
 
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
 import com.google.gerrit.common.data.AgreementInfo;
 import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.common.data.PermissionRule;
@@ -29,7 +27,9 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.util.ArrayList;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -54,14 +54,14 @@
 
   @Override
   public AgreementInfo call() throws Exception {
-    List<String> accepted = Lists.newArrayList();
-    Map<String, ContributorAgreement> agreements = Maps.newHashMap();
+    List<String> accepted = new ArrayList<>();
+    Map<String, ContributorAgreement> agreements = new HashMap<>();
     Collection<ContributorAgreement> cas =
         projectCache.getAllProjects().getConfig().getContributorAgreements();
     for (ContributorAgreement ca : cas) {
       agreements.put(ca.getName(), ca.forUi());
 
-      List<AccountGroup.UUID> groupIds = Lists.newArrayList();
+      List<AccountGroup.UUID> groupIds = new ArrayList<>();
       for (PermissionRule rule : ca.getAccepted()) {
         if ((rule.getAction() == Action.ALLOW) && (rule.getGroup() != null)) {
           if (rule.getGroup().getUUID() == null) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailServiceImpl.java
deleted file mode 100644
index 37ca524..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailServiceImpl.java
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd.rpc.changedetail;
-
-import com.google.gerrit.common.data.ChangeDetailService;
-import com.google.gerrit.common.data.PatchSetDetail;
-import com.google.gerrit.extensions.client.DiffPreferencesInfo;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gwtjsonrpc.common.AsyncCallback;
-import com.google.inject.Inject;
-
-class ChangeDetailServiceImpl implements ChangeDetailService {
-  private final PatchSetDetailFactory.Factory patchSetDetail;
-
-  @Inject
-  ChangeDetailServiceImpl(
-      final PatchSetDetailFactory.Factory patchSetDetail) {
-    this.patchSetDetail = patchSetDetail;
-  }
-
-  @Override
-  public void patchSetDetail(PatchSet.Id id,
-      AsyncCallback<PatchSetDetail> callback) {
-    patchSetDetail2(null, id, null, callback);
-  }
-
-  @Override
-  public void patchSetDetail2(PatchSet.Id baseId, PatchSet.Id id,
-      DiffPreferencesInfo diffPrefs, AsyncCallback<PatchSetDetail> callback) {
-    patchSetDetail.create(baseId, id, diffPrefs).to(callback);
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeModule.java
deleted file mode 100644
index d8adfe3..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeModule.java
+++ /dev/null
@@ -1,36 +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.httpd.rpc.changedetail;
-
-import com.google.gerrit.extensions.config.FactoryModule;
-import com.google.gerrit.httpd.rpc.RpcServletModule;
-import com.google.gerrit.httpd.rpc.UiRpcModule;
-
-public class ChangeModule extends RpcServletModule {
-  public ChangeModule() {
-    super(UiRpcModule.PREFIX);
-  }
-
-  @Override
-  protected void configureServlets() {
-    install(new FactoryModule() {
-      @Override
-      protected void configure() {
-        factory(PatchSetDetailFactory.Factory.class);
-      }
-    });
-    rpc(ChangeDetailServiceImpl.class);
-  }
-}
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
deleted file mode 100644
index 1f71b86..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java
+++ /dev/null
@@ -1,234 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd.rpc.changedetail;
-
-import static com.google.common.base.Preconditions.checkArgument;
-
-import com.google.common.base.Optional;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.data.PatchSetDetail;
-import com.google.gerrit.common.errors.NoSuchEntityException;
-import com.google.gerrit.extensions.client.DiffPreferencesInfo;
-import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
-import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountPatchReview;
-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.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.PatchLineCommentsUtil;
-import com.google.gerrit.server.PatchSetUtil;
-import com.google.gerrit.server.edit.ChangeEdit;
-import com.google.gerrit.server.edit.ChangeEditUtil;
-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;
-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.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.assistedinject.Assisted;
-
-import org.eclipse.jgit.lib.ObjectId;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/** Creates a {@link PatchSetDetail} from a {@link PatchSet}. */
-class PatchSetDetailFactory extends Handler<PatchSetDetail> {
-
-  private static final Logger log =
-    LoggerFactory.getLogger(PatchSetDetailFactory.class);
-
-  interface Factory {
-    PatchSetDetailFactory create(
-        @Assisted("psIdBase") @Nullable PatchSet.Id psIdBase,
-        @Assisted("psIdNew") PatchSet.Id psIdNew,
-        @Nullable DiffPreferencesInfo diffPrefs);
-  }
-
-  private final PatchSetInfoFactory infoFactory;
-  private final ReviewDb db;
-  private final PatchListCache patchListCache;
-  private final Provider<CurrentUser> userProvider;
-  private final ChangeControl.GenericFactory changeControlFactory;
-  private final PatchLineCommentsUtil plcUtil;
-  private final PatchSetUtil psUtil;
-  private final ChangeEditUtil editUtil;
-
-  private Project.NameKey project;
-  private final PatchSet.Id psIdBase;
-  private final PatchSet.Id psIdNew;
-  private final DiffPreferencesInfo diffPrefs;
-  private ObjectId oldId;
-  private ObjectId newId;
-
-  private PatchSetDetail detail;
-  ChangeControl control;
-  PatchSet patchSet;
-
-  @Inject
-  PatchSetDetailFactory(final PatchSetInfoFactory psif, final ReviewDb db,
-      final PatchListCache patchListCache,
-      final Provider<CurrentUser> userProvider,
-      final ChangeControl.GenericFactory changeControlFactory,
-      final PatchLineCommentsUtil plcUtil,
-      final PatchSetUtil psUtil,
-      ChangeEditUtil editUtil,
-      @Assisted("psIdBase") @Nullable final PatchSet.Id psIdBase,
-      @Assisted("psIdNew") final PatchSet.Id psIdNew,
-      @Assisted @Nullable final DiffPreferencesInfo diffPrefs) {
-    this.infoFactory = psif;
-    this.db = db;
-    this.patchListCache = patchListCache;
-    this.userProvider = userProvider;
-    this.changeControlFactory = changeControlFactory;
-    this.plcUtil = plcUtil;
-    this.psUtil = psUtil;
-    this.editUtil = editUtil;
-
-    this.psIdBase = psIdBase;
-    this.psIdNew = psIdNew;
-    if (psIdBase != null && psIdNew != null) {
-      checkArgument(psIdBase.getParentKey().equals(psIdNew.getParentKey()),
-          "cannot compare PatchSets from different changes: %s and %s",
-          psIdBase, psIdNew);
-    }
-    this.diffPrefs = diffPrefs;
-  }
-
-  @Override
-  public PatchSetDetail call() throws OrmException, NoSuchEntityException,
-      PatchSetInfoNotAvailableException, NoSuchChangeException, AuthException,
-      IOException {
-    Optional<ChangeEdit> edit = null;
-    ChangeNotes notes;
-    if (control == null || patchSet == null) {
-      control = changeControlFactory.validateFor(db, psIdNew.getParentKey(),
-          userProvider.get());
-      notes = control.getNotes();
-      if (psIdNew.get() == 0) {
-        edit = editUtil.byChange(control.getChange());
-        if (edit.isPresent()) {
-          patchSet = edit.get().getBasePatchSet();
-        }
-      } else {
-        patchSet = psUtil.get(db, notes, psIdNew);
-      }
-      if (patchSet == null) {
-        throw new NoSuchEntityException();
-      }
-    } else {
-      notes = control.getNotes();
-    }
-    project = control.getProject().getNameKey();
-    final PatchList list;
-
-    try {
-      if (psIdBase != null) {
-        oldId = toObjectId(psUtil.get(db, notes, psIdBase));
-        if (edit != null && edit.isPresent()) {
-          newId = edit.get().getEditCommit().toObjectId();
-        } else {
-          newId = toObjectId(patchSet);
-        }
-
-        list = listFor(keyFor(diffPrefs.ignoreWhitespace));
-      } else { // OK, means use base to compare
-        list = patchListCache.get(control.getChange(), patchSet);
-      }
-    } catch (PatchListNotAvailableException e) {
-      throw new NoSuchEntityException();
-    }
-
-    final List<Patch> patches = list.toPatchList(patchSet.getId());
-    final Map<Patch.Key, Patch> byKey = new HashMap<>();
-    for (final Patch p : patches) {
-      byKey.put(p.getKey(), p);
-    }
-
-    if (edit == null) {
-      for (PatchLineComment c : plcUtil.publishedByPatchSet(db, notes, psIdNew)) {
-        final Patch p = byKey.get(c.getKey().getParentKey());
-        if (p != null) {
-          p.setCommentCount(p.getCommentCount() + 1);
-        }
-      }
-    }
-
-    detail = new PatchSetDetail();
-    detail.setPatchSet(patchSet);
-    detail.setProject(project);
-
-    detail.setInfo(infoFactory.get(db, notes, patchSet.getId()));
-    detail.setPatches(patches);
-
-    final CurrentUser user = control.getUser();
-    if (user.isIdentifiedUser() && edit == null) {
-      // If we are signed in, compute the number of draft comments by the
-      // current user on each of these patch files. This way they can more
-      // quickly locate where they have pending drafts, and review them.
-      //
-      final Account.Id me = user.getAccountId();
-      for (PatchLineComment c
-          : plcUtil.draftByPatchSetAuthor(db, psIdNew, me, notes)) {
-        final Patch p = byKey.get(c.getKey().getParentKey());
-        if (p != null) {
-          p.setDraftCount(p.getDraftCount() + 1);
-        }
-      }
-
-      for (AccountPatchReview r : db.accountPatchReviews().byReviewer(me, psIdNew)) {
-        final Patch p = byKey.get(r.getKey().getPatchKey());
-        if (p != null) {
-          p.setReviewedByCurrentUser(true);
-        }
-      }
-    }
-
-    return detail;
-  }
-
-  private ObjectId toObjectId(PatchSet ps) throws NoSuchEntityException {
-    try {
-      return ObjectId.fromString(ps.getRevision().get());
-    } catch (IllegalArgumentException e) {
-      log.error("Patch set " + ps.getId() + " has invalid revision");
-      throw new NoSuchEntityException();
-    }
-  }
-
-  private PatchListKey keyFor(Whitespace whitespace) {
-    return new PatchListKey(oldId, newId, whitespace);
-  }
-
-  private PatchList listFor(PatchListKey key)
-      throws PatchListNotAvailableException {
-    return patchListCache.get(key, project);
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java
deleted file mode 100644
index d311ecc..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java
+++ /dev/null
@@ -1,69 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd.rpc.patch;
-
-import com.google.gerrit.common.data.PatchDetailService;
-import com.google.gerrit.common.data.PatchScript;
-import com.google.gerrit.common.errors.NoSuchEntityException;
-import com.google.gerrit.extensions.client.DiffPreferencesInfo;
-import com.google.gerrit.httpd.rpc.BaseServiceImplementation;
-import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.patch.PatchScriptFactory;
-import com.google.gerrit.server.project.ChangeControl;
-import com.google.gwtjsonrpc.common.AsyncCallback;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-class PatchDetailServiceImpl extends BaseServiceImplementation implements
-    PatchDetailService {
-  private final PatchScriptFactory.Factory patchScriptFactoryFactory;
-  private final ChangeControl.GenericFactory changeControlFactory;
-
-  @Inject
-  PatchDetailServiceImpl(final Provider<ReviewDb> schema,
-      final Provider<CurrentUser> currentUser,
-      final PatchScriptFactory.Factory patchScriptFactoryFactory,
-      final ChangeControl.GenericFactory changeControlFactory) {
-    super(schema, currentUser);
-
-    this.patchScriptFactoryFactory = patchScriptFactoryFactory;
-    this.changeControlFactory = changeControlFactory;
-  }
-
-  @Override
-  public void patchScript(final Patch.Key patchKey, final PatchSet.Id psa,
-      final PatchSet.Id psb, final DiffPreferencesInfo dp,
-      final AsyncCallback<PatchScript> callback) {
-    if (psb == null) {
-      callback.onFailure(new NoSuchEntityException());
-      return;
-    }
-
-    new Handler<PatchScript>() {
-      @Override
-      public PatchScript call() throws Exception {
-        ChangeControl control = changeControlFactory.validateFor(
-            getDb(), patchKey.getParentKey().getParentKey(),
-            getUser());
-        return patchScriptFactoryFactory.create(
-            control, patchKey.getFileName(), psa, psb, dp).call();
-      }
-    }.to(callback);
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchModule.java
deleted file mode 100644
index 83bfcdc..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchModule.java
+++ /dev/null
@@ -1,29 +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.httpd.rpc.patch;
-
-import com.google.gerrit.httpd.rpc.RpcServletModule;
-import com.google.gerrit.httpd.rpc.UiRpcModule;
-
-public class PatchModule extends RpcServletModule {
-  public PatchModule() {
-    super(UiRpcModule.PREFIX);
-  }
-
-  @Override
-  protected void configureServlets() {
-    rpc(PatchDetailServiceImpl.class);
-  }
-}
diff --git a/gerrit-lucene/BUCK b/gerrit-lucene/BUCK
index 163340b..771a021 100644
--- a/gerrit-lucene/BUCK
+++ b/gerrit-lucene/BUCK
@@ -31,11 +31,11 @@
     '//lib:gwtorm',
     '//lib/guice:guice',
     '//lib/guice:guice-assistedinject',
+    '//lib/jgit/org.eclipse.jgit:jgit',
     '//lib/log:api',
     '//lib/lucene:lucene-analyzers-common',
     '//lib/lucene:lucene-core-and-backward-codecs',
     '//lib/lucene:lucene-misc',
-    '@jgit//org.eclipse.jgit:jgit',
   ],
   visibility = ['PUBLIC'],
 )
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/AbstractLuceneIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
index 5acdc25..b40d46b 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
@@ -16,6 +16,7 @@
 
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 
+import com.google.common.base.Joiner;
 import com.google.common.collect.Sets;
 import com.google.common.util.concurrent.AbstractFuture;
 import com.google.common.util.concurrent.ListenableFuture;
@@ -47,7 +48,6 @@
 import org.apache.lucene.store.AlreadyClosedException;
 import org.apache.lucene.store.Directory;
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -69,13 +69,11 @@
     return f.getName() + "_SORT";
   }
 
-  public static void setReady(SitePaths sitePaths, int version, boolean ready)
-      throws IOException {
+  public static void setReady(SitePaths sitePaths, String name, int version,
+      boolean ready) throws IOException {
     try {
-      // TODO(dborowitz): Totally broken for non-change indexes.
-      FileBasedConfig cfg =
-          LuceneVersionManager.loadGerritIndexConfig(sitePaths);
-      LuceneVersionManager.setReady(cfg, version, ready);
+      GerritIndexStatus cfg = new GerritIndexStatus(sitePaths);
+      cfg.setReady(name, version, ready);
       cfg.save();
     } catch (ConfigInvalidException e) {
       throw new IOException(e);
@@ -85,6 +83,7 @@
   private final Schema<V> schema;
   private final SitePaths sitePaths;
   private final Directory dir;
+  private final String name;
   private final TrackingIndexWriter writer;
   private final ReferenceManager<IndexSearcher> searcherManager;
   private final ControlledRealTimeReopenThread<IndexSearcher> reopenThread;
@@ -94,12 +93,15 @@
       Schema<V> schema,
       SitePaths sitePaths,
       Directory dir,
-      final String name,
+      String name,
+      String subIndex,
       GerritIndexWriterConfig writerConfig,
       SearcherFactory searcherFactory) throws IOException {
     this.schema = schema;
     this.sitePaths = sitePaths;
     this.dir = dir;
+    this.name = name;
+    final String index = Joiner.on('_').skipNulls().join(name, subIndex);
     IndexWriter delegateWriter;
     long commitPeriod = writerConfig.getCommitWithinMs();
 
@@ -114,7 +116,7 @@
       delegateWriter = autoCommitWriter;
 
       new ScheduledThreadPoolExecutor(1, new ThreadFactoryBuilder()
-          .setNameFormat("Commit-%d " + name)
+          .setNameFormat("Commit-%d " + index)
           .setDaemon(true)
           .build())
           .scheduleAtFixedRate(new Runnable() {
@@ -126,13 +128,13 @@
                   autoCommitWriter.commit();
                 }
               } catch (IOException e) {
-                log.error("Error committing " + name + " Lucene index", e);
+                log.error("Error committing " + index + " Lucene index", e);
               } catch (OutOfMemoryError e) {
-                log.error("Error committing " + name + " Lucene index", e);
+                log.error("Error committing " + index + " Lucene index", e);
                 try {
                   autoCommitWriter.close();
                 } catch (IOException e2) {
-                  log.error("SEVERE: Error closing " + name
+                  log.error("SEVERE: Error closing " + index
                       + " Lucene index  after OOM; index may be corrupted.", e);
                 }
               }
@@ -181,7 +183,7 @@
 
   @Override
   public void markReady(boolean ready) throws IOException {
-    setReady(sitePaths, schema.getVersion(), ready);
+    setReady(sitePaths, name, schema.getVersion(), ready);
   }
 
   @Override
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/ChangeSubIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/ChangeSubIndex.java
index ad53493..6f0df0f 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/ChangeSubIndex.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/ChangeSubIndex.java
@@ -17,6 +17,7 @@
 import static com.google.common.collect.Iterables.getOnlyElement;
 import static com.google.gerrit.lucene.LuceneChangeIndex.ID_SORT_FIELD;
 import static com.google.gerrit.lucene.LuceneChangeIndex.UPDATED_SORT_FIELD;
+import static com.google.gerrit.server.index.change.ChangeSchemaDefinitions.NAME;
 
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.config.SitePaths;
@@ -57,10 +58,11 @@
       Schema<ChangeData> schema,
       SitePaths sitePaths,
       Directory dir,
-      String name,
+      String subIndex,
       GerritIndexWriterConfig writerConfig,
       SearcherFactory searcherFactory) throws IOException {
-    super(schema, sitePaths, dir, name, writerConfig, searcherFactory);
+    super(schema, sitePaths, dir, NAME, subIndex, writerConfig,
+        searcherFactory);
   }
 
   @Override
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/GerritIndexStatus.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/GerritIndexStatus.java
new file mode 100644
index 0000000..f43e385
--- /dev/null
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/GerritIndexStatus.java
@@ -0,0 +1,78 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.lucene;
+
+import com.google.common.primitives.Ints;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+
+import java.io.IOException;
+
+class GerritIndexStatus {
+  private static final String SECTION = "index";
+  private static final String KEY_READY = "ready";
+
+  private final FileBasedConfig cfg;
+
+  GerritIndexStatus(SitePaths sitePaths)
+      throws ConfigInvalidException, IOException {
+    cfg = new FileBasedConfig(
+        sitePaths.index_dir.resolve("gerrit_index.config").toFile(),
+        FS.detect());
+    cfg.load();
+    convertLegacyConfig();
+  }
+
+  void setReady(String indexName, int version, boolean ready) {
+    cfg.setBoolean(SECTION, indexDirName(indexName, version), KEY_READY, ready);
+  }
+
+  boolean getReady(String indexName, int version) {
+    return cfg.getBoolean(SECTION, indexDirName(indexName, version), KEY_READY,
+        false);
+  }
+
+  void save() throws IOException {
+    cfg.save();
+  }
+
+  private void convertLegacyConfig() throws IOException {
+    boolean dirty = false;
+    // Convert legacy [index "25"] to modern [index "changes_0025"].
+    for (String subsection : cfg.getSubsections(SECTION)) {
+      Integer v = Ints.tryParse(subsection);
+      if (v != null) {
+        String ready = cfg.getString(SECTION, subsection, KEY_READY);
+        if (ready != null) {
+          dirty = false;
+          cfg.unset(SECTION, subsection, KEY_READY);
+          cfg.setString(SECTION,
+              indexDirName(ChangeSchemaDefinitions.NAME, v), KEY_READY, ready);
+        }
+      }
+    }
+    if (dirty) {
+      cfg.save();
+    }
+  }
+
+  private static String indexDirName(String indexName, int version) {
+    return String.format("%s_%04d", indexName, version);
+  }
+}
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 8f7e899..bdc7189 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
@@ -24,8 +24,10 @@
 import static com.google.gerrit.server.index.change.IndexRewriter.CLOSED_STATUSES;
 import static com.google.gerrit.server.index.change.IndexRewriter.OPEN_STATUSES;
 
+import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
+import com.google.common.collect.Multimap;
 import com.google.common.collect.Sets;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
@@ -35,6 +37,7 @@
 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.StarredChangesUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.index.FieldDef.FillArgs;
@@ -114,6 +117,11 @@
   private static final String PATCH_SET_FIELD = ChangeField.PATCH_SET.getName();
   private static final String REVIEWEDBY_FIELD =
       ChangeField.REVIEWEDBY.getName();
+  private static final String HASHTAG_FIELD =
+      ChangeField.HASHTAG_CASE_AWARE.getName();
+  private static final String STAR_FIELD = ChangeField.STAR.getName();
+  @Deprecated
+  private static final String STARREDBY_FIELD = ChangeField.STARREDBY.getName();
 
   static Term idTerm(ChangeData cd) {
     return QueryBuilder.intTerm(LEGACY_ID.getName(), cd.getId().get());
@@ -123,7 +131,6 @@
     return QueryBuilder.intTerm(LEGACY_ID.getName(), id.get());
   }
 
-  private final SitePaths sitePaths;
   private final FillArgs fillArgs;
   private final ListeningExecutorService executor;
   private final Provider<ReviewDb> db;
@@ -142,7 +149,6 @@
       ChangeData.Factory changeDataFactory,
       FillArgs fillArgs,
       @Assisted Schema<ChangeData> schema) throws IOException {
-    this.sitePaths = sitePaths;
     this.fillArgs = fillArgs;
     this.executor = executor;
     this.db = db;
@@ -249,9 +255,9 @@
 
   @Override
   public void markReady(boolean ready) throws IOException {
-    // Do not delegate to ChangeSubIndex#markReady, since changes have an
-    // additional level of directory nesting.
-    AbstractLuceneIndex.setReady(sitePaths, schema.getVersion(), ready);
+    // Arbitrary done on open index, as ready bit is set
+    // per index and not sub index
+    openIndex.markReady(ready);
   }
 
   private Sort getSort() {
@@ -413,6 +419,15 @@
     if (fields.contains(REVIEWEDBY_FIELD)) {
       decodeReviewedBy(doc, cd);
     }
+    if (fields.contains(HASHTAG_FIELD)) {
+      decodeHashtags(doc, cd);
+    }
+    if (fields.contains(STARREDBY_FIELD)) {
+      decodeStarredBy(doc, cd);
+    }
+    if (fields.contains(STAR_FIELD)) {
+      decodeStar(doc, cd);
+    }
     return cd;
   }
 
@@ -466,6 +481,39 @@
     }
   }
 
+  private void decodeHashtags(Document doc, ChangeData cd) {
+    IndexableField[] hashtag = doc.getFields(HASHTAG_FIELD);
+    Set<String> hashtags = Sets.newHashSetWithExpectedSize(hashtag.length);
+    for (IndexableField r : hashtag) {
+      hashtags.add(r.binaryValue().utf8ToString());
+    }
+    cd.setHashtags(hashtags);
+  }
+
+  @Deprecated
+  private void decodeStarredBy(Document doc, ChangeData cd) {
+    IndexableField[] starredBy = doc.getFields(STARREDBY_FIELD);
+    Set<Account.Id> accounts =
+        Sets.newHashSetWithExpectedSize(starredBy.length);
+    for (IndexableField r : starredBy) {
+      accounts.add(new Account.Id(r.numericValue().intValue()));
+    }
+    cd.setStarredBy(accounts);
+  }
+
+  private void decodeStar(Document doc, ChangeData cd) {
+    IndexableField[] star = doc.getFields(STAR_FIELD);
+    Multimap<Account.Id, String> stars = ArrayListMultimap.create();
+    for (IndexableField r : star) {
+      StarredChangesUtil.StarField starField =
+          StarredChangesUtil.StarField.parse(r.stringValue());
+      if (starField != null) {
+        stars.put(starField.accountId(), starField.label());
+      }
+    }
+    cd.setStars(stars);
+  }
+
   private static <T> List<T> decodeProtos(Document doc, String fieldName,
       ProtobufCodec<T> codec) {
     BytesRef[] bytesRefs = doc.getBinaryValues(fieldName);
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
index 9dd4399..94408c6 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
@@ -20,23 +20,20 @@
 import com.google.common.collect.Maps;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.extensions.events.LifecycleListener;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.index.Index;
+import com.google.gerrit.server.index.IndexCollection;
+import com.google.gerrit.server.index.IndexDefinition;
+import com.google.gerrit.server.index.IndexDefinition.IndexFactory;
 import com.google.gerrit.server.index.OnlineReindexer;
 import com.google.gerrit.server.index.Schema;
-import com.google.gerrit.server.index.change.ChangeIndex;
-import com.google.gerrit.server.index.change.ChangeIndexCollection;
-import com.google.gerrit.server.index.change.ChangeIndexDefinition;
-import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
 import com.google.inject.ProvisionException;
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
-import org.eclipse.jgit.util.FS;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -46,6 +43,7 @@
 import java.nio.file.Path;
 import java.util.Collection;
 import java.util.List;
+import java.util.Map;
 import java.util.TreeMap;
 
 @Singleton
@@ -55,13 +53,13 @@
 
   static final String CHANGES_PREFIX = "changes_";
 
-  private static class Version {
-    private final Schema<ChangeData> schema;
+  private static class Version<V> {
+    private final Schema<V> schema;
     private final int version;
     private final boolean exists;
     private final boolean ready;
 
-    private Version(Schema<ChangeData> schema, int version, boolean exists,
+    private Version(Schema<V> schema, int version, boolean exists,
         boolean ready) {
       checkArgument(schema == null || schema.getVersion() == version);
       this.schema = schema;
@@ -76,71 +74,60 @@
         prefix, schema.getVersion()));
   }
 
-  static FileBasedConfig loadGerritIndexConfig(SitePaths sitePaths)
-      throws ConfigInvalidException, IOException {
-    FileBasedConfig cfg = new FileBasedConfig(
-        sitePaths.index_dir.resolve("gerrit_index.config").toFile(),
-        FS.detect());
-    cfg.load();
-    return cfg;
-  }
-
-  static void setReady(Config cfg, int version, boolean ready) {
-    cfg.setBoolean("index", Integer.toString(version), "ready", ready);
-  }
-
-  private static boolean getReady(Config cfg, int version) {
-    return cfg.getBoolean("index", Integer.toString(version), "ready", false);
-  }
-
   private final SitePaths sitePaths;
-  private final LuceneChangeIndex.Factory indexFactory;
-  private final ChangeIndexCollection indexes;
-  private final ChangeIndexDefinition changeDef;
+  private final Map<String, IndexDefinition<?, ?, ?>> defs;
+  private final Map<String, OnlineReindexer<?, ?, ?>> reindexers;
   private final boolean onlineUpgrade;
-  private OnlineReindexer<Change.Id, ChangeData, ChangeIndex> reindexer;
+  private final String runReindexMsg;
 
   @Inject
   LuceneVersionManager(
       @GerritServerConfig Config cfg,
       SitePaths sitePaths,
-      LuceneChangeIndex.Factory indexFactory,
-      ChangeIndexCollection indexes,
-      ChangeIndexDefinition changeDef) {
+      Collection<IndexDefinition<?, ?, ?>> defs) {
     this.sitePaths = sitePaths;
-    this.indexFactory = indexFactory;
-    this.indexes = indexes;
-    this.changeDef = changeDef;
-    this.onlineUpgrade = cfg.getBoolean("index", null, "onlineUpgrade", true);
+    this.defs = Maps.newHashMapWithExpectedSize(defs.size());
+    for (IndexDefinition<?, ?, ?> def : defs) {
+      this.defs.put(def.getName(), def);
+    }
+
+    reindexers = Maps.newHashMapWithExpectedSize(defs.size());
+    onlineUpgrade = cfg.getBoolean("index", null, "onlineUpgrade", true);
+    runReindexMsg =
+        "No index versions ready; run java -jar " +
+        sitePaths.gerrit_war.toAbsolutePath() +
+        " reindex";
   }
 
   @Override
   public void start() {
-    String runReindex =
-      "No index versions ready; run java -jar " +
-      sitePaths.gerrit_war.toAbsolutePath() +
-      " reindex";
-
-    FileBasedConfig cfg;
+    GerritIndexStatus cfg;
     try {
-      cfg = loadGerritIndexConfig(sitePaths);
+      cfg = new GerritIndexStatus(sitePaths);
     } catch (ConfigInvalidException | IOException e) {
       throw fail(e);
     }
 
     if (!Files.exists(sitePaths.index_dir)) {
-      throw new ProvisionException(runReindex);
+      throw new ProvisionException(runReindexMsg);
     } else if (!Files.exists(sitePaths.index_dir)) {
       log.warn("Not a directory: %s", sitePaths.index_dir.toAbsolutePath());
-      throw new ProvisionException(runReindex);
+      throw new ProvisionException(runReindexMsg);
     }
 
-    TreeMap<Integer, Version> versions = scanVersions(cfg);
+    for (IndexDefinition<?, ?, ?> def : defs.values()) {
+      initIndex(def, cfg);
+    }
+  }
+
+  private <K, V, I extends Index<K, V>> void initIndex(
+      IndexDefinition<K, V, I> def, GerritIndexStatus cfg) {
+    TreeMap<Integer, Version<V>> versions = scanVersions(def, cfg);
     // Search from the most recent ready version.
     // Write to the most recent ready version and the most recent version.
-    Version search = null;
-    List<Version> write = Lists.newArrayListWithCapacity(2);
-    for (Version v : versions.descendingMap().values()) {
+    Version<V> search = null;
+    List<Version<V>> write = Lists.newArrayListWithCapacity(2);
+    for (Version<V> v : versions.descendingMap().values()) {
       if (v.schema == null) {
         continue;
       }
@@ -156,27 +143,34 @@
       }
     }
     if (search == null) {
-      throw new ProvisionException(runReindex);
+      throw new ProvisionException(runReindexMsg);
     }
 
-    markNotReady(cfg, versions.values(), write);
-    LuceneChangeIndex searchIndex =
-        (LuceneChangeIndex) indexFactory.create(search.schema);
+    IndexFactory<K, V, I> factory = def.getIndexFactory();
+    I searchIndex = factory.create(search.schema);
+    IndexCollection<K, V, I> indexes = def.getIndexCollection();
     indexes.setSearchIndex(searchIndex);
-    for (Version v : write) {
+    for (Version<V> v : write) {
       if (v.schema != null) {
         if (v.version != search.version) {
-          indexes.addWriteIndex(indexFactory.create(v.schema));
+          indexes.addWriteIndex(factory.create(v.schema));
         } else {
           indexes.addWriteIndex(searchIndex);
         }
       }
     }
 
+    markNotReady(cfg, def.getName(), versions.values(), write);
+
     int latest = write.get(0).version;
     if (onlineUpgrade && latest != search.version) {
-      reindexer = new OnlineReindexer<>(changeDef, latest);
-      reindexer.start();
+      OnlineReindexer<K, V, I> reindexer = new OnlineReindexer<>(def, latest);
+      synchronized (this) {
+        if (!reindexers.containsKey(def.getName())) {
+          reindexers.put(def.getName(), reindexer);
+          reindexer.start();
+        }
+      }
     }
   }
 
@@ -186,10 +180,11 @@
    * @return true if started, otherwise false.
    * @throws ReindexerAlreadyRunningException
    */
-  public synchronized boolean startReindexer()
+  public synchronized boolean startReindexer(String name)
       throws ReindexerAlreadyRunningException {
-    validateReindexerNotRunning();
-    if (!isCurrentIndexVersionLatest()) {
+    OnlineReindexer<?, ?, ?> reindexer = reindexers.get(name);
+    validateReindexerNotRunning(reindexer);
+    if (!isCurrentIndexVersionLatest(name, reindexer)) {
       reindexer.start();
       return true;
     }
@@ -202,49 +197,57 @@
    * @return true if index was activate, otherwise false.
    * @throws ReindexerAlreadyRunningException
    */
-  public synchronized boolean activateLatestIndex()
+  public synchronized boolean activateLatestIndex(String name)
       throws ReindexerAlreadyRunningException {
-    validateReindexerNotRunning();
-    if (!isCurrentIndexVersionLatest()) {
+    OnlineReindexer<?, ?, ?> reindexer = reindexers.get(name);
+    validateReindexerNotRunning(reindexer);
+    if (!isCurrentIndexVersionLatest(name, reindexer)) {
       reindexer.activateIndex();
       return true;
     }
     return false;
   }
 
-  private boolean isCurrentIndexVersionLatest() {
+  private boolean isCurrentIndexVersionLatest(
+      String name, OnlineReindexer<?, ?, ?> reindexer) {
+    int readVersion = defs.get(name).getIndexCollection().getSearchIndex()
+        .getSchema().getVersion();
     return reindexer == null
-        || reindexer.getVersion() == indexes.getSearchIndex().getSchema()
-            .getVersion();
+        || reindexer.getVersion() == readVersion;
   }
 
-  private void validateReindexerNotRunning()
+  private static void validateReindexerNotRunning(
+      OnlineReindexer<?, ?, ?> reindexer)
       throws ReindexerAlreadyRunningException {
     if (reindexer != null && reindexer.isRunning()) {
       throw new ReindexerAlreadyRunningException();
     }
   }
 
-  private TreeMap<Integer, Version> scanVersions(Config cfg) {
-    TreeMap<Integer, Version> versions = Maps.newTreeMap();
-    for (Schema<ChangeData> schema : changeDef.getSchemas().values()) {
-      Path p = getDir(sitePaths, CHANGES_PREFIX, schema);
+  private <K, V, I extends Index<K, V>> TreeMap<Integer, Version<V>>
+      scanVersions(IndexDefinition<K, V, I> def, GerritIndexStatus cfg) {
+    TreeMap<Integer, Version<V>> versions = new TreeMap<>();
+    for (Schema<V> schema : def.getSchemas().values()) {
+      // This part is Lucene-specific.
+      Path p = getDir(sitePaths, def.getName(), schema);
       boolean isDir = Files.isDirectory(p);
       if (Files.exists(p) && !isDir) {
         log.warn("Not a directory: %s", p.toAbsolutePath());
       }
       int v = schema.getVersion();
-      versions.put(v, new Version(schema, v, isDir, getReady(cfg, v)));
+      versions.put(v, new Version<>(
+          schema, v, isDir, cfg.getReady(def.getName(), v)));
     }
 
+    String prefix = def.getName() + "_";
     try (DirectoryStream<Path> paths =
         Files.newDirectoryStream(sitePaths.index_dir)) {
       for (Path p : paths) {
         String n = p.getFileName().toString();
-        if (!n.startsWith(CHANGES_PREFIX)) {
+        if (!n.startsWith(prefix)) {
           continue;
         }
-        String versionStr = n.substring(CHANGES_PREFIX.length());
+        String versionStr = n.substring(prefix.length());
         Integer v = Ints.tryParse(versionStr);
         if (v == null || versionStr.length() != 4) {
           log.warn("Unrecognized version in index directory: {}",
@@ -252,7 +255,8 @@
           continue;
         }
         if (!versions.containsKey(v)) {
-          versions.put(v, new Version(null, v, true, getReady(cfg, v)));
+          versions.put(v, new Version<V>(
+              null, v, true, cfg.getReady(def.getName(), v)));
         }
       }
     } catch (IOException e) {
@@ -261,12 +265,12 @@
     return versions;
   }
 
-  private void markNotReady(FileBasedConfig cfg, Iterable<Version> versions,
-      Collection<Version> inUse) {
+  private <V> void markNotReady(GerritIndexStatus cfg, String name,
+      Iterable<Version<V>> versions, Collection<Version<V>> inUse) {
     boolean dirty = false;
-    for (Version v : versions) {
+    for (Version<V> v : versions) {
       if (!inUse.contains(v) && v.exists) {
-        setReady(cfg, v.version, false);
+        cfg.setReady(name, v.version, false);
         dirty = true;
       }
     }
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/WrappableSearcherManager.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/WrappableSearcherManager.java
index 7bcd0a6..7fc058c 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/WrappableSearcherManager.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/WrappableSearcherManager.java
@@ -91,7 +91,7 @@
    *
    * @throws IOException if there is a low-level I/O error
    */
-  public WrappableSearcherManager(IndexWriter writer, boolean applyAllDeletes, SearcherFactory searcherFactory) throws IOException {
+  WrappableSearcherManager(IndexWriter writer, boolean applyAllDeletes, SearcherFactory searcherFactory) throws IOException {
     if (searcherFactory == null) {
       searcherFactory = new SearcherFactory();
     }
@@ -108,7 +108,7 @@
    *
    * @throws IOException if there is a low-level I/O error
    */
-  public WrappableSearcherManager(Directory dir, SearcherFactory searcherFactory) throws IOException {
+  WrappableSearcherManager(Directory dir, SearcherFactory searcherFactory) throws IOException {
     if (searcherFactory == null) {
       searcherFactory = new SearcherFactory();
     }
@@ -127,7 +127,7 @@
    *
    * @throws IOException if there is a low-level I/O error
    */
-  public WrappableSearcherManager(DirectoryReader reader, SearcherFactory searcherFactory) throws IOException {
+  WrappableSearcherManager(DirectoryReader reader, SearcherFactory searcherFactory) throws IOException {
     if (searcherFactory == null) {
       searcherFactory = new SearcherFactory();
     }
@@ -143,7 +143,8 @@
   @Override
   protected IndexSearcher refreshIfNeeded(IndexSearcher referenceToRefresh) throws IOException {
     final IndexReader r = referenceToRefresh.getIndexReader();
-    assert r instanceof DirectoryReader: "searcher's IndexReader should be a DirectoryReader, but got " + r;
+    assert r instanceof DirectoryReader :
+      "searcher's IndexReader should be a DirectoryReader, but got " + r;
     final IndexReader newReader = DirectoryReader.openIfChanged((DirectoryReader) r);
     if (newReader == null) {
       return null;
@@ -171,7 +172,8 @@
     final IndexSearcher searcher = acquire();
     try {
       final IndexReader r = searcher.getIndexReader();
-      assert r instanceof DirectoryReader: "searcher's IndexReader should be a DirectoryReader, but got " + r;
+      assert r instanceof DirectoryReader :
+        "searcher's IndexReader should be a DirectoryReader, but got " + r;
       return ((DirectoryReader) r).isCurrent();
     } finally {
       release(searcher);
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
index d24c8a0..3e65912 100644
--- 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
@@ -31,6 +31,7 @@
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.AuthResult;
+import com.google.gerrit.server.auth.oauth.OAuthTokenCache;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -58,8 +59,8 @@
   private final Provider<IdentifiedUser> identifiedUser;
   private final AccountManager accountManager;
   private final CanonicalWebUrl urlProvider;
+  private final OAuthTokenCache tokenCache;
   private OAuthServiceProvider serviceProvider;
-  private OAuthToken token;
   private OAuthUserInfo user;
   private String redirectToken;
   private boolean linkMode;
@@ -68,16 +69,18 @@
   OAuthSession(DynamicItem<WebSession> webSession,
       Provider<IdentifiedUser> identifiedUser,
       AccountManager accountManager,
-      CanonicalWebUrl urlProvider) {
+      CanonicalWebUrl urlProvider,
+      OAuthTokenCache tokenCache) {
     this.state = generateRandomState();
     this.identifiedUser = identifiedUser;
     this.webSession = webSession;
     this.accountManager = accountManager;
     this.urlProvider = urlProvider;
+    this.tokenCache = tokenCache;
   }
 
   boolean isLoggedIn() {
-    return token != null && user != null;
+    return tokenCache.has(user);
   }
 
   boolean isOAuthFinal(HttpServletRequest request) {
@@ -95,9 +98,12 @@
       }
 
       log.debug("Login-Retrieve-User " + this);
-      token = oauth.getAccessToken(new OAuthVerifier(request.getParameter("code")));
-
+      OAuthToken token = oauth.getAccessToken(
+          new OAuthVerifier(request.getParameter("code")));
       user = oauth.getUserInfo(token);
+      if (user != null && token != null) {
+        tokenCache.put(user, token);
+      }
 
       if (isLoggedIn()) {
         log.debug("Login-SUCCESS " + this);
@@ -211,7 +217,7 @@
   }
 
   void logout() {
-    token = null;
+    tokenCache.remove(user);
     user = null;
     redirectToken = null;
     serviceProvider = null;
@@ -243,7 +249,8 @@
 
   @Override
   public String toString() {
-    return "OAuthSession [token=" + token + ", user=" + user + "]";
+    return "OAuthSession [token=" + tokenCache.get(user) + ", user=" + user
+        + "]";
   }
 
   public void setServiceProvider(OAuthServiceProvider provider) {
diff --git a/gerrit-openid/BUCK b/gerrit-openid/BUCK
index bfbdb43..5eace7b 100644
--- a/gerrit-openid/BUCK
+++ b/gerrit-openid/BUCK
@@ -19,8 +19,8 @@
     '//lib/commons:codec',
     '//lib/guice:guice',
     '//lib/guice:guice-servlet',
+    '//lib/jgit/org.eclipse.jgit:jgit',
     '//lib/log:api',
-    '@jgit//org.eclipse.jgit:jgit',
   ],
   visibility = ['PUBLIC'],
 )
diff --git a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/DiscoveryResult.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/DiscoveryResult.java
index 37051da..2ee3b6b 100644
--- a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/DiscoveryResult.java
+++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/DiscoveryResult.java
@@ -17,7 +17,7 @@
 import java.util.Map;
 
 final class DiscoveryResult {
-  static enum Status {
+  enum Status {
     /** Provider was discovered and {@code providerUrl} is valid. */
     VALID,
 
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 8b05c72..3a40252 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
@@ -20,7 +20,6 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Sets;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.auth.openid.OpenIdUrls;
@@ -46,6 +45,7 @@
 import org.w3c.dom.Element;
 
 import java.io.IOException;
+import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 
@@ -102,7 +102,7 @@
       suggestProviders = ImmutableSet.of();
       ssoUrl = authConfig.getOpenIdSsoUrl();
     } else {
-      Set<String> providers = Sets.newHashSet();
+      Set<String> providers = new HashSet<>();
       for (Map.Entry<String, String> e : ALL_PROVIDERS.entrySet()) {
         if (impl.isAllowedOpenID(e.getValue())) {
           providers.add(e.getKey());
@@ -218,7 +218,7 @@
     url.append(r.providerUrl);
     if (r.providerArgs != null && !r.providerArgs.isEmpty()) {
       boolean first = true;
-      for(Map.Entry<String, String> arg : r.providerArgs.entrySet()) {
+      for (Map.Entry<String, String> arg : r.providerArgs.entrySet()) {
         if (first) {
           url.append('?');
           first = false;
diff --git a/gerrit-patch-commonsnet/src/main/java/org/apache/commons/net/smtp/AuthSMTPClient.java b/gerrit-patch-commonsnet/src/main/java/org/apache/commons/net/smtp/AuthSMTPClient.java
index ece982d..10d94d8 100644
--- a/gerrit-patch-commonsnet/src/main/java/org/apache/commons/net/smtp/AuthSMTPClient.java
+++ b/gerrit-patch-commonsnet/src/main/java/org/apache/commons/net/smtp/AuthSMTPClient.java
@@ -165,7 +165,7 @@
     }
 
     String cmd = encodeBase64(smtpUser.getBytes(UTF_8));
-    if(sendCommand(cmd) != 334) {
+    if (sendCommand(cmd) != 334) {
       return false;
     }
 
diff --git a/gerrit-patch-jgit/BUCK b/gerrit-patch-jgit/BUCK
index 90f5e4e..d76b6fa 100644
--- a/gerrit-patch-jgit/BUCK
+++ b/gerrit-patch-jgit/BUCK
@@ -29,7 +29,7 @@
 
 genrule(
   name = 'jgit_edit_src',
-  cmd = 'unzip -qd $TMP $(location @jgit//org.eclipse.jgit:jgit_src) ' +
+  cmd = 'unzip -qd $TMP $(location //lib/jgit/org.eclipse.jgit:jgit_src) ' +
     'org/eclipse/jgit/diff/Edit.java;' +
     'cd $TMP;' +
     'zip -Dq $OUT org/eclipse/jgit/diff/Edit.java',
@@ -46,7 +46,7 @@
   ],
   deps = [
     '//lib:gson',
-    '@jgit//org.eclipse.jgit:jgit',
+    '//lib/jgit/org.eclipse.jgit:jgit',
   ],
   visibility = ['PUBLIC'],
 )
@@ -56,8 +56,8 @@
   srcs = glob(['src/test/java/**/*.java']),
   deps = [
     ':server',
+    '//lib/jgit/org.eclipse.jgit:jgit',
     '//lib:junit',
-    '@jgit//org.eclipse.jgit:jgit',
   ],
   source_under_test = [':server'],
   visibility = ['//tools/eclipse:classpath'],
diff --git a/gerrit-pgm/BUCK b/gerrit-pgm/BUCK
index 0737c44..e62079f 100644
--- a/gerrit-pgm/BUCK
+++ b/gerrit-pgm/BUCK
@@ -15,9 +15,9 @@
   '//lib/guice:guice',
   '//lib/guice:guice-assistedinject',
   '//lib/guice:guice-servlet',
+  '//lib/jgit/org.eclipse.jgit:jgit',
   '//lib/log:api',
   '//lib/log:log4j',
-  '@jgit//org.eclipse.jgit:jgit',
 ]
 
 DEPS = BASE_JETTY_DEPS + [
@@ -176,8 +176,8 @@
     '//lib:junit',
     '//lib/easymock:easymock',
     '//lib/guice:guice',
-    '@jgit//org.eclipse.jgit:jgit',
-    '@jgit//org.eclipse.jgit.junit:junit',
+    '//lib/jgit/org.eclipse.jgit:jgit',
+    '//lib/jgit/org.eclipse.jgit.junit:junit',
   ],
   source_under_test = [':pgm'],
 )
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 08b85c5..e4f2c0ba 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
@@ -467,7 +467,6 @@
     modules.add(H2CacheBasedWebSession.module());
     modules.add(sysInjector.getInstance(GitOverHttpModule.class));
     modules.add(sysInjector.getInstance(WebModule.class));
-    modules.add(sysInjector.getInstance(StaticModule.class));
     modules.add(sysInjector.getInstance(RequireSslFilter.Module.class));
     modules.add(new HttpPluginModule());
     if (sshd) {
@@ -485,6 +484,9 @@
     }
     modules.add(sysInjector.getInstance(GetUserFilter.Module.class));
 
+    // StaticModule contains a "/*" wildcard, place it last.
+    modules.add(sysInjector.getInstance(StaticModule.class));
+
     return sysInjector.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 f57f95f..da81843 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
@@ -52,7 +52,7 @@
   private boolean noAutoStart;
 
   @Option(name = "--skip-plugins", usage = "Don't install plugins")
-  private boolean skipPlugins = false;
+  private boolean skipPlugins;
 
   @Option(name = "--list-plugins", usage = "List available plugins")
   private boolean listPlugins;
@@ -60,6 +60,10 @@
   @Option(name = "--install-plugin", usage = "Install given plugin without asking")
   private List<String> installPlugins;
 
+  @Option(name = "--install-all-plugins",
+      usage = "Install all plugins from war without asking")
+  private boolean installAllPlugins;
+
   @Option(name = "--secure-store-lib",
       usage = "Path to jar providing SecureStore implementation class")
   private String secureStoreLib;
@@ -93,8 +97,14 @@
 
     if (!skipPlugins) {
       final List<PluginData> plugins =
-          InitPlugins.listPluginsAndRemoveTempFiles(init.site, pluginsDistribution);
+          InitPlugins.listPluginsAndRemoveTempFiles(init.site,
+              pluginsDistribution);
       ConsoleUI ui = ConsoleUI.getInstance(false);
+      if (installAllPlugins && !nullOrEmpty(installPlugins)) {
+        ui.message(
+            "Cannot use --install-plugin together with --install-all-plugins.\n");
+        return true;
+      }
       verifyInstallPluginList(ui, plugins);
       if (listPlugins) {
         if (!plugins.isEmpty()) {
@@ -113,7 +123,7 @@
 
   @Override
   protected void afterInit(SiteRun run) throws Exception {
-    List<Module> modules = Lists.newArrayList();
+    List<Module> modules = new ArrayList<>();
     modules.add(new AbstractModule() {
       @Override
       protected void configure() {
@@ -134,6 +144,11 @@
   }
 
   @Override
+  protected boolean installAllPlugins() {
+    return installAllPlugins;
+  }
+
+  @Override
   protected ConsoleUI getConsoleUI() {
     return ConsoleUI.getInstance(batchMode);
   }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/JythonShell.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/JythonShell.java
index d3643f3..b092784 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/JythonShell.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/JythonShell.java
@@ -134,7 +134,7 @@
       new Class[]  { String.class, pyObject },
       new Object[] { getDefaultBanner() +
         " running for Gerrit " + com.google.gerrit.common.Version.getVersion(),
-        null });
+        null, });
   }
 
   public void set(String key, Object content) {
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 224c75c..bd9ed8f 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
@@ -116,7 +116,7 @@
   private class Worker extends Thread {
     @Override
     public void run() {
-      try (ReviewDb db = database.open()){
+      try (ReviewDb db = database.open()) {
         for (;;) {
           final AccountExternalId extId = next();
           if (extId == null) {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNoteDb.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNoteDb.java
index 7784bfe..047789e 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNoteDb.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNoteDb.java
@@ -16,16 +16,19 @@
 
 import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
 
+import com.google.common.base.Function;
+import com.google.common.base.Predicates;
 import com.google.common.base.Stopwatch;
 import com.google.common.collect.ArrayListMultimap;
-import com.google.common.collect.Lists;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Multimap;
-import com.google.common.util.concurrent.AsyncFunction;
+import com.google.common.collect.Ordering;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.common.util.concurrent.MoreExecutors;
-import com.google.gerrit.common.FormatUtil;
+import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.lifecycle.LifecycleManager;
@@ -36,11 +39,10 @@
 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.change.ChangeResource;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.MultiProgressMonitor;
-import com.google.gerrit.server.git.MultiProgressMonitor.Task;
 import com.google.gerrit.server.git.SearchingChangeCacheImpl;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.index.DummyIndexModule;
@@ -50,7 +52,6 @@
 import com.google.gerrit.server.schema.DisabledChangesReviewDbWrapper;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 
@@ -68,11 +69,12 @@
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
 
 public class RebuildNoteDb extends SiteProgram {
   private static final Logger log =
@@ -82,6 +84,14 @@
       usage = "Number of threads to use for rebuilding NoteDb")
   private int threads = Runtime.getRuntime().availableProcessors();
 
+  @Option(name = "--project",
+      usage = "Projects to rebuild; recommended for debugging only")
+  private List<String> projects = new ArrayList<>();
+
+  @Option(name = "--change",
+      usage = "Individual change numbers to rebuild; recommended for debugging only")
+  private List<Integer> changes = new ArrayList<>();
+
   private Injector dbInjector;
   private Injector sysInjector;
 
@@ -120,7 +130,7 @@
     sysInjector = createSysInjector();
     sysInjector.injectMembers(this);
     if (!notesMigration.enabled()) {
-      die("NoteDb is not enabled.");
+      throw die("NoteDb is not enabled.");
     }
     LifecycleManager sysManager = new LifecycleManager();
     sysManager.add(sysInjector);
@@ -129,57 +139,46 @@
     ListeningExecutorService executor = newExecutor();
     System.out.println("Rebuilding the NoteDb");
 
-    Multimap<Project.NameKey, Change.Id> changesByProject =
+    final ImmutableMultimap<Project.NameKey, Change.Id> changesByProject =
         getChangesByProject();
-    AtomicBoolean ok = new AtomicBoolean(true);
+    boolean ok;
     Stopwatch sw = Stopwatch.createStarted();
-    try (Repository allUsersRepo =
-        repoManager.openMetadataRepository(allUsersName)) {
+    try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) {
       deleteRefs(RefNames.REFS_DRAFT_COMMENTS, allUsersRepo);
-      for (Project.NameKey project : changesByProject.keySet()) {
-        try {
-          List<ListenableFuture<?>> futures = Lists.newArrayList();
 
-          // Here, we elide the project name to 50 characters to ensure that
-          // the whole monitor line for a project fits on one line (<80 chars).
-          final MultiProgressMonitor mpm = new MultiProgressMonitor(System.out,
-              FormatUtil.elide(project.get(), 50));
-          Task doneTask =
-              mpm.beginSubTask("done", changesByProject.get(project).size());
-          Task failedTask =
-              mpm.beginSubTask("failed", MultiProgressMonitor.UNKNOWN);
-
-          for (Change.Id id : changesByProject.get(project)) {
-            // TODO(dborowitz): This can fail if the project no longer exists.
-            // We might not want to just skip conversion of those changes, and
-            // instead move them somewhere like a special lost+found repo.
-            ListenableFuture<?> future = rebuilder.rebuildAsync(id, executor);
-            futures.add(future);
-            future.addListener(
-                new RebuildListener(id, future, ok, doneTask, failedTask),
-                MoreExecutors.directExecutor());
-          }
-
-          mpm.waitFor(Futures.transformAsync(Futures.successfulAsList(futures),
-              new AsyncFunction<List<?>, Void>() {
-                @Override
-                public ListenableFuture<Void> apply(List<?> input) {
-                  mpm.end();
-                  return Futures.immediateFuture(null);
+      List<ListenableFuture<Boolean>> futures = new ArrayList<>();
+      List<Project.NameKey> projectNames = Ordering.usingToString()
+          .sortedCopy(changesByProject.keySet());
+      for (final Project.NameKey project : projectNames) {
+        ListenableFuture<Boolean> future = executor.submit(
+            new Callable<Boolean>() {
+              @Override
+              public Boolean call() {
+                try (ReviewDb db = unwrap(schemaFactory.open())) {
+                  return rebuilder.rebuildProject(
+                      db, changesByProject, project, allUsersRepo);
+                } catch (Exception e) {
+                  log.error("Error rebuilding project " + project, e);
+                  return false;
                 }
-              }));
-        } catch (Exception e) {
-          log.error("Error rebuilding NoteDb", e);
-          ok.set(false);
-          break;
-        }
+              }
+            });
+        futures.add(future);
+      }
+
+      try {
+        ok = Iterables.all(
+            Futures.allAsList(futures).get(), Predicates.equalTo(true));
+      } catch (InterruptedException | ExecutionException e) {
+        log.error("Error rebuilding projects", e);
+        ok = false;
       }
     }
 
     double t = sw.elapsed(TimeUnit.MILLISECONDS) / 1000d;
     System.out.format("Rebuild %d changes in %.01fs (%.01f/s)\n",
         changesByProject.size(), t, changesByProject.size() / t);
-    return ok.get() ? 0 : 1;
+    return ok ? 0 : 1;
   }
 
   private static void execute(BatchRefUpdate bru, Repository repo)
@@ -208,7 +207,7 @@
   }
 
   private Injector createSysInjector() {
-    return dbInjector.createChildInjector(new AbstractModule() {
+    return dbInjector.createChildInjector(new FactoryModule() {
       @Override
       public void configure() {
         install(dbInjector.getInstance(BatchProgramModule.class));
@@ -216,6 +215,7 @@
         DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(
             ReindexAfterUpdate.class);
         install(new DummyIndexModule());
+        factory(ChangeResource.Factory.class);
       }
     });
   }
@@ -229,17 +229,41 @@
     }
   }
 
-  private Multimap<Project.NameKey, Change.Id> getChangesByProject()
+  private ImmutableMultimap<Project.NameKey, Change.Id> getChangesByProject()
       throws OrmException {
     // Memorize all changes so we can close the db connection and allow
     // rebuilder threads to use the full connection pool.
     Multimap<Project.NameKey, Change.Id> changesByProject =
         ArrayListMultimap.create();
     try (ReviewDb db = schemaFactory.open()) {
-      for (Change c : unwrap(db).changes().all()) {
-        changesByProject.put(c.getProject(), c.getId());
+      if (projects.isEmpty() && !changes.isEmpty()) {
+        Iterable<Change> todo = unwrap(db).changes().get(
+            Iterables.transform(changes, new Function<Integer, Change.Id>() {
+              @Override
+              public Change.Id apply(Integer in) {
+                return new Change.Id(in);
+              }
+            }));
+        for (Change c : todo) {
+          changesByProject.put(c.getProject(), c.getId());
+        }
+      } else {
+        for (Change c : unwrap(db).changes().all()) {
+          boolean include = false;
+          if (projects.isEmpty() && changes.isEmpty()) {
+            include = true;
+          } else if (!projects.isEmpty()
+              && projects.contains(c.getProject().get())) {
+            include = true;
+          } else if (!changes.isEmpty() && changes.contains(c.getId().get())) {
+            include = true;
+          }
+          if (include) {
+            changesByProject.put(c.getProject(), c.getId());
+          }
+        }
       }
-      return changesByProject;
+      return ImmutableMultimap.copyOf(changesByProject);
     }
   }
 
@@ -249,54 +273,4 @@
     }
     return db;
   }
-
-  private static class RebuildListener implements Runnable {
-    private Change.Id changeId;
-    private ListenableFuture<?> future;
-    private AtomicBoolean ok;
-    private Task doneTask;
-    private Task failedTask;
-
-
-    private RebuildListener(Change.Id changeId, ListenableFuture<?> future,
-        AtomicBoolean ok, Task doneTask, Task failedTask) {
-      this.changeId = changeId;
-      this.future = future;
-      this.ok = ok;
-      this.doneTask = doneTask;
-      this.failedTask = failedTask;
-    }
-
-    @Override
-    public void run() {
-      try {
-        future.get();
-        doneTask.update(1);
-      } catch (ExecutionException | InterruptedException e) {
-        fail(e);
-      } catch (RuntimeException e) {
-        failAndThrow(e);
-      } catch (Error e) {
-        // Can't join with RuntimeException because "RuntimeException
-        // | Error" becomes Throwable, which messes with signatures.
-        failAndThrow(e);
-      }
-    }
-
-    private void fail(Throwable t) {
-      log.error("Failed to rebuild change " + changeId, t);
-      ok.set(false);
-      failedTask.update(1);
-    }
-
-    private void failAndThrow(RuntimeException e) {
-      fail(e);
-      throw e;
-    }
-
-    private void failAndThrow(Error e) {
-      fail(e);
-      throw e;
-    }
-  }
 }
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 90c8f9f..9cd7c6a 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,13 +17,14 @@
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
 
-import com.google.common.collect.Lists;
 import com.google.gerrit.common.Die;
+import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.lucene.LuceneIndexModule;
 import com.google.gerrit.pgm.util.BatchProgramModule;
 import com.google.gerrit.pgm.util.SiteProgram;
 import com.google.gerrit.pgm.util.ThreadLimiter;
+import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.ScanningChangeCacheImpl;
 import com.google.gerrit.server.index.Index;
@@ -42,6 +43,7 @@
 import org.kohsuke.args4j.Option;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
@@ -114,7 +116,7 @@
     if (changesVersion != null) {
       versions.put(ChangeSchemaDefinitions.INSTANCE.getName(), changesVersion);
     }
-    List<Module> modules = Lists.newArrayList();
+    List<Module> modules = new ArrayList<>();
     Module indexModule;
     switch (IndexModule.getIndexType(dbInjector)) {
       case LUCENE:
@@ -129,6 +131,12 @@
     // will have just deleted the old (possibly corrupt) index.
     modules.add(ScanningChangeCacheImpl.module());
     modules.add(dbInjector.getInstance(BatchProgramModule.class));
+    modules.add(new FactoryModule() {
+      @Override
+      protected void configure() {
+        factory(ChangeResource.Factory.class);
+      }
+    });
 
     return dbInjector.createChildInjector(modules);
   }
@@ -159,7 +167,7 @@
     int n = result.doneCount() + result.failedCount();
     double t = result.elapsed(TimeUnit.MILLISECONDS) / 1000d;
     System.out.format("Reindexed %d documents in %s index in %.01fs (%.01f/s)\n",
-        n, def.getName(), t, n/t);
+        n, def.getName(), t, n / t);
     if (result.success()) {
       index.markReady(true);
     }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/BaseInit.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/BaseInit.java
index 2136723..540ba0b 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/BaseInit.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/BaseInit.java
@@ -20,11 +20,11 @@
 
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
-import com.google.common.collect.Lists;
 import com.google.gerrit.common.Die;
 import com.google.gerrit.common.IoUtil;
 import com.google.gerrit.pgm.init.api.ConsoleUI;
 import com.google.gerrit.pgm.init.api.InitFlags;
+import com.google.gerrit.pgm.init.api.InstallAllPlugins;
 import com.google.gerrit.pgm.init.api.InstallPlugins;
 import com.google.gerrit.pgm.init.api.LibraryDownload;
 import com.google.gerrit.pgm.util.SiteProgram;
@@ -197,6 +197,10 @@
     }
   }
 
+  protected boolean installAllPlugins() {
+    return false;
+  }
+
   protected boolean getAutoStart() {
     return false;
   }
@@ -242,9 +246,11 @@
         bind(Path.class).annotatedWith(SitePath.class).toInstance(sitePath);
         List<String> plugins =
             MoreObjects.firstNonNull(
-                getInstallPlugins(), Lists.<String> newArrayList());
+                getInstallPlugins(), new ArrayList<String>());
         bind(new TypeLiteral<List<String>>() {}).annotatedWith(
             InstallPlugins.class).toInstance(plugins);
+        bind(new TypeLiteral<Boolean>() {}).annotatedWith(
+            InstallAllPlugins.class).toInstance(installAllPlugins());
         bind(PluginsDistribution.class).toInstance(pluginsDistribution);
 
         String secureStoreClassName;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java
index 40a07b4..2de71cc 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java
@@ -43,14 +43,17 @@
 public class InitAdminUser implements InitStep {
   private final ConsoleUI ui;
   private final InitFlags flags;
+  private final VersionedAuthorizedKeysOnInit.Factory authorizedKeysFactory;
   private SchemaFactory<ReviewDb> dbFactory;
 
   @Inject
   InitAdminUser(
       InitFlags flags,
-      ConsoleUI ui) {
+      ConsoleUI ui,
+      VersionedAuthorizedKeysOnInit.Factory authorizedKeysFactory) {
     this.flags = flags;
     this.ui = ui;
+    this.authorizedKeysFactory = authorizedKeysFactory;
   }
 
   @Override
@@ -110,7 +113,10 @@
           db.accountGroupMembers().insert(Collections.singleton(m));
 
           if (sshKey != null) {
-            db.accountSshKeys().insert(Collections.singleton(sshKey));
+            VersionedAuthorizedKeysOnInit authorizedKeys =
+                authorizedKeysFactory.create(id).load();
+            authorizedKeys.addKey(sshKey.getSshPublicKey());
+            authorizedKeys.save("Added SSH key for initial admin user\n");
           }
         }
       }
@@ -158,6 +164,6 @@
           "Cannot add public SSH key: %s is not a file", keyFile));
     }
     String content = new String(Files.readAllBytes(p), UTF_8);
-    return new AccountSshKey(new AccountSshKey.Id(id, 0), content);
+    return new AccountSshKey(new AccountSshKey.Id(id, 1), content);
   }
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java
index 0ba731a..7e4d3c1 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java
@@ -21,7 +21,7 @@
 import com.google.gerrit.pgm.init.api.ConsoleUI;
 import com.google.gerrit.pgm.init.api.InitStep;
 import com.google.gerrit.pgm.init.api.Section;
-import com.google.gerrit.server.api.config.GerritServerIdProvider;
+import com.google.gerrit.server.config.GerritServerIdProvider;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.Binding;
 import com.google.inject.Guice;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitIndex.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitIndex.java
index 0883ac6..77c466e 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitIndex.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitIndex.java
@@ -59,11 +59,11 @@
 
     IndexType type = index.select("Type", "type", IndexType.LUCENE);
     for (SchemaDefinitions<?> def : IndexModule.ALL_SCHEMA_DEFS) {
-      // TODO(dborowitz): Totally broken for non-change indexes.
       AbstractLuceneIndex.setReady(
-          site, def.getLatest().getVersion(), true);
+          site, def.getName(), def.getLatest().getVersion(), true);
     }
     if ((site.isNew || isEmptySite()) && type == IndexType.LUCENE) {
+      // Do nothing
     } else {
       final String message = String.format(
         "\nThe index must be %sbuilt before starting Gerrit:\n"
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java
index 57a1a30..b5aa625 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java
@@ -42,6 +42,7 @@
     bind(Libraries.class);
     bind(LibraryDownloader.class);
     factory(Section.Factory.class);
+    factory(VersionedAuthorizedKeysOnInit.Factory.class);
 
     // Steps are executed in the order listed here.
     //
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 7fdd7e2..43d7d3b 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,7 +15,6 @@
 package com.google.gerrit.pgm.init;
 
 import com.google.common.collect.FluentIterable;
-import com.google.common.collect.Lists;
 import com.google.gerrit.common.PluginData;
 import com.google.gerrit.pgm.init.api.ConsoleUI;
 import com.google.gerrit.pgm.init.api.InitFlags;
@@ -30,6 +29,7 @@
 import java.io.InputStream;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Comparator;
 import java.util.List;
@@ -55,7 +55,7 @@
   private static List<PluginData> listPlugins(final SitePaths site,
       final boolean deleteTempPluginFile, PluginsDistribution pluginsDistribution)
           throws IOException {
-    final List<PluginData> result = Lists.newArrayList();
+    final List<PluginData> result = new ArrayList<>();
     pluginsDistribution.foreach(new PluginsDistribution.Processor() {
       @Override
       public void process(String pluginName, InputStream in) throws IOException {
@@ -68,10 +68,11 @@
       }
     });
     return FluentIterable.from(result).toSortedList(new Comparator<PluginData>() {
-      @Override
-      public int compare(PluginData a, PluginData b) {
-        return a.name.compareTo(b.name);
-      }});
+        @Override
+        public int compare(PluginData a, PluginData b) {
+          return a.name.compareTo(b.name);
+        }
+      });
   }
 
   private final ConsoleUI ui;
@@ -121,17 +122,18 @@
         Path p = site.plugins_dir.resolve(plugin.name + ".jar");
         boolean upgrade = Files.exists(p);
 
-        if (!(initFlags.installPlugins.contains(pluginName) || ui.yesno(upgrade,
-            "Install plugin %s version %s", pluginName, plugin.version))) {
+        if (!(initFlags.installPlugins.contains(pluginName)
+            || initFlags.installAllPlugins
+            || ui.yesno(upgrade, "Install plugin %s version %s", pluginName,
+                plugin.version))) {
           Files.deleteIfExists(tmpPlugin);
           continue;
         }
 
         if (upgrade) {
           final String installedPluginVersion = getVersion(p);
-          if (!ui.yesno(upgrade,
-              "version %s is already installed, overwrite it",
-              installedPluginVersion)) {
+          if (!ui.yesno(upgrade, "%s %s is already installed, overwrite it",
+              plugin.name, installedPluginVersion)) {
             Files.deleteIfExists(tmpPlugin);
             continue;
           }
@@ -144,6 +146,12 @@
         }
         try {
           Files.move(tmpPlugin, p);
+          if (upgrade) {
+            // or update that is not an upgrade
+            ui.message("Updated %s to %s\n", plugin.name, plugin.version);
+          } else {
+            ui.message("Installed %s %s\n", plugin.name, plugin.version);
+          }
         } catch (IOException e) {
           throw new IOException("Failed to install plugin " + pluginName
               + ": " + tmpPlugin.toAbsolutePath() + " -> "
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 bdebd6c..cb4439a 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
@@ -105,22 +105,22 @@
 
         System.err.print(" rsa...");
         System.err.flush();
-        Runtime.getRuntime().exec(new String[] {"ssh-keygen", //
-            "-q" /* quiet */, //
-            "-t", "rsa", //
-            "-P", "", //
-            "-C", comment, //
-            "-f", site.ssh_rsa.toAbsolutePath().toString() //
+        Runtime.getRuntime().exec(new String[] {"ssh-keygen",
+            "-q" /* quiet */,
+            "-t", "rsa",
+            "-P", "",
+            "-C", comment,
+            "-f", site.ssh_rsa.toAbsolutePath().toString(),
             }).waitFor();
 
         System.err.print(" dsa...");
         System.err.flush();
-        Runtime.getRuntime().exec(new String[] {"ssh-keygen", //
-            "-q" /* quiet */, //
-            "-t", "dsa", //
-            "-P", "", //
-            "-C", comment, //
-            "-f", site.ssh_dsa.toAbsolutePath().toString() //
+        Runtime.getRuntime().exec(new String[] {"ssh-keygen",
+            "-q" /* quiet */,
+            "-t", "dsa",
+            "-P", "",
+            "-C", comment,
+            "-f", site.ssh_dsa.toAbsolutePath().toString(),
             }).waitFor();
 
       } else {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Libraries.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Libraries.java
index 282e75a..b4c672a 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Libraries.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Libraries.java
@@ -116,7 +116,7 @@
     return doGet(cfg, name, key, true);
   }
 
-  private static final String doGet(Config cfg, String name, String key,
+  private static String doGet(Config cfg, String name, String key,
       boolean required) {
     String val = cfg.getString("library", name, key);
     if ((val == null || val.isEmpty()) && required) {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/PostgreSQLInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/PostgreSQLInitializer.java
index ffb0017..65a66de 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/PostgreSQLInitializer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/PostgreSQLInitializer.java
@@ -29,4 +29,4 @@
     databaseSection.string("Database username", "username", username());
     databaseSection.password("username", "password");
   }
-}
\ No newline at end of file
+}
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 9060bf0..7f86a8a 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
@@ -105,6 +105,7 @@
     extractMailExample("ChangeSubject.vm");
     extractMailExample("Comment.vm");
     extractMailExample("CommentFooter.vm");
+    extractMailExample("DeleteReviewer.vm");
     extractMailExample("DeleteVote.vm");
     extractMailExample("Footer.vm");
     extractMailExample("Merged.vm");
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/VersionedAuthorizedKeysOnInit.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/VersionedAuthorizedKeysOnInit.java
new file mode 100644
index 0000000..1c72600
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/VersionedAuthorizedKeysOnInit.java
@@ -0,0 +1,198 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.common.base.Preconditions.checkState;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Strings;
+import com.google.gerrit.pgm.init.api.InitFlags;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountSshKey;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.account.AuthorizedKeys;
+import com.google.gerrit.server.account.VersionedAuthorizedKeys;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.git.VersionedMetaData;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.RepositoryCache.FileKey;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.util.FS;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.List;
+
+public class VersionedAuthorizedKeysOnInit extends VersionedMetaData {
+  public interface Factory {
+    VersionedAuthorizedKeysOnInit create(Account.Id accountId);
+  }
+
+  private final Account.Id accountId;
+  private final String ref;
+  private final String project;
+  private final SitePaths site;
+  private final InitFlags flags;
+
+  private List<Optional<AccountSshKey>> keys;
+  private ObjectId revision;
+
+  @Inject
+  public VersionedAuthorizedKeysOnInit(
+      AllUsersNameOnInitProvider allUsers,
+      SitePaths site,
+      InitFlags flags,
+      @Assisted Account.Id accountId) {
+
+    this.project = allUsers.get();
+    this.site = site;
+    this.flags = flags;
+    this.accountId = accountId;
+    this.ref = RefNames.refsUsers(accountId);
+  }
+
+  @Override
+  protected String getRefName() {
+    return ref;
+  }
+
+  public VersionedAuthorizedKeysOnInit load()
+      throws IOException, ConfigInvalidException {
+    File path = getPath();
+    if (path != null) {
+      try (Repository repo = new FileRepository(path)) {
+        load(repo);
+      }
+    }
+    return this;
+  }
+
+  private File getPath() {
+    Path basePath = site.resolve(flags.cfg.getString("gerrit", null, "basePath"));
+    if (basePath == null) {
+      throw new IllegalStateException("gerrit.basePath must be configured");
+    }
+    return FileKey.resolve(basePath.resolve(project).toFile(), FS.DETECTED);
+  }
+
+  @Override
+  protected void onLoad() throws IOException, ConfigInvalidException {
+    revision = getRevision();
+    keys = AuthorizedKeys.parse(accountId, readUTF8(AuthorizedKeys.FILE_NAME));
+  }
+
+  public AccountSshKey addKey(String pub) {
+    checkState(keys != null, "SSH keys not loaded yet");
+    int seq = keys.isEmpty() ? 1 : keys.size() + 1;
+    AccountSshKey.Id keyId = new AccountSshKey.Id(accountId, seq);
+    AccountSshKey key =
+        new VersionedAuthorizedKeys.SimpleSshKeyCreator().create(keyId, pub);
+    keys.add(Optional.of(key));
+    return key;
+  }
+
+  public void save(String message) throws IOException {
+    save(new PersonIdent("Gerrit Initialization", "init@gerrit"), message);
+  }
+
+  private void save(PersonIdent ident, String msg) throws IOException {
+    File path = getPath();
+    if (path == null) {
+      throw new IOException(project + " does not exist.");
+    }
+
+    try (Repository repo = new FileRepository(path);
+        ObjectInserter i = repo.newObjectInserter();
+        ObjectReader r = repo.newObjectReader();
+        RevWalk rw = new RevWalk(reader)) {
+      inserter = i;
+      reader = r;
+
+      RevTree srcTree = revision != null ? rw.parseTree(revision) : null;
+      newTree = readTree(srcTree);
+
+      CommitBuilder commit = new CommitBuilder();
+      commit.setAuthor(ident);
+      commit.setCommitter(ident);
+      commit.setMessage(msg);
+
+      onSave(commit);
+      ObjectId res = newTree.writeTree(inserter);
+      if (res.equals(srcTree)) {
+        return;
+      }
+
+      commit.setTreeId(res);
+      if (revision != null) {
+        commit.addParentId(revision);
+      }
+      ObjectId newRevision = inserter.insert(commit);
+      updateRef(repo, ident, newRevision, "commit: " + msg);
+      revision = newRevision;
+    } finally {
+      inserter = null;
+      reader = null;
+    }
+  }
+
+  @Override
+  protected boolean onSave(CommitBuilder commit) throws IOException {
+    if (Strings.isNullOrEmpty(commit.getMessage())) {
+      commit.setMessage("Updated SSH keys\n");
+    }
+
+    saveUTF8(AuthorizedKeys.FILE_NAME, AuthorizedKeys.serialize(keys));
+    return true;
+  }
+
+  private void updateRef(Repository repo, PersonIdent ident,
+      ObjectId newRevision, String refLogMsg) throws IOException {
+    RefUpdate ru = repo.updateRef(getRefName());
+    ru.setRefLogIdent(ident);
+    ru.setNewObjectId(newRevision);
+    ru.setExpectedOldObjectId(revision);
+    ru.setRefLogMessage(refLogMsg, false);
+    RefUpdate.Result r = ru.update();
+    switch(r) {
+      case FAST_FORWARD:
+      case NEW:
+      case NO_CHANGE:
+        break;
+      case FORCED:
+      case IO_FAILURE:
+      case LOCK_FAILURE:
+      case NOT_ATTEMPTED:
+      case REJECTED:
+      case REJECTED_CURRENT_BRANCH:
+      case RENAMED:
+      default:
+        throw new IOException("Failed to update " + getRefName() + " of "
+            + project + ": " + r.name());
+    }
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java
index cb58c6e..f7d9d4a 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java
@@ -180,7 +180,7 @@
     ru.setExpectedOldObjectId(revision);
     ru.setRefLogMessage(refLogMsg, false);
     RefUpdate.Result r = ru.update();
-    switch(r) {
+    switch (r) {
       case FAST_FORWARD:
       case NEW:
       case NO_CHANGE:
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InitFlags.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InitFlags.java
index bdd8b86..f0674d6 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InitFlags.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InitFlags.java
@@ -45,17 +45,19 @@
   public final FileBasedConfig cfg;
   public final SecureStore sec;
   public final List<String> installPlugins;
+  public final boolean installAllPlugins;
 
   @VisibleForTesting
   @Inject
   public InitFlags(final SitePaths site,
       final SecureStore secureStore,
-      @InstallPlugins final List<String> installPlugins) throws IOException,
-      ConfigInvalidException {
+      @InstallPlugins final List<String> installPlugins,
+      @InstallAllPlugins final Boolean installAllPlugins) throws IOException,
+          ConfigInvalidException {
     sec = secureStore;
     this.installPlugins = installPlugins;
+    this.installAllPlugins = installAllPlugins;
     cfg = new FileBasedConfig(site.gerrit_config.toFile(), FS.DETECTED);
-
     cfg.load();
   }
 }
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFactory.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InstallAllPlugins.java
similarity index 63%
copy from gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFactory.java
copy to gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InstallAllPlugins.java
index f68b629..809a197 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFactory.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InstallAllPlugins.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 The Android Open Source Project
+// Copyright (C) 2016 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,9 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.prettify.client;
+package com.google.gerrit.pgm.init.api;
 
-/** Creates a new PrettyFormatter instance for one formatting run. */
-public interface PrettyFactory {
-  PrettyFormatter get();
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@BindingAnnotation
+@Retention(RetentionPolicy.RUNTIME)
+public @interface InstallAllPlugins {
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/rules/PrologCompiler.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/rules/PrologCompiler.java
index 064cc19..8d3c766 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/rules/PrologCompiler.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/rules/PrologCompiler.java
@@ -66,7 +66,7 @@
     PrologCompiler create(Repository git);
   }
 
-  public static enum Status {
+  public enum Status {
     NO_RULES, COMPILED
   }
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/PerThreadReviewDbModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/PerThreadReviewDbModule.java
index eb12937..9ef31ff 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/PerThreadReviewDbModule.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/PerThreadReviewDbModule.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.pgm.util;
 
-import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -24,6 +23,7 @@
 import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
 
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
@@ -43,7 +43,7 @@
   @Override
   protected void configure() {
     final List<ReviewDb> dbs = Collections.synchronizedList(
-        Lists.<ReviewDb> newArrayList());
+        new ArrayList<ReviewDb>());
     final ThreadLocal<ReviewDb> localDb = new ThreadLocal<>();
 
     bind(ReviewDb.class).toProvider(new Provider<ReviewDb>() {
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 7991423..9e2da5c 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,7 +18,6 @@
 import static com.google.inject.Scopes.SINGLETON;
 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;
@@ -224,7 +223,7 @@
       throw new RuntimeException(e);
     }
 
-    List<Module> modules = Lists.newArrayList();
+    List<Module> modules = new ArrayList<>();
     modules.add(new AbstractModule() {
       @Override
       protected void configure() {
diff --git a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/libraries.config b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/libraries.config
index 39d647d..4d9d0f0 100644
--- a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/libraries.config
+++ b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/libraries.config
@@ -16,14 +16,14 @@
 # Version should match lib/bouncycastle/BUCK
 [library "bouncyCastleProvider"]
   name = Bouncy Castle Crypto Provider v152
-  url = http://www.bouncycastle.org/download/bcprov-jdk15on-152.jar
+  url = https://repo1.maven.org/maven2/org/bouncycastle/bcprov-jdk15on/1.52/bcprov-jdk15on-1.52.jar
   sha1 = 88a941faf9819d371e3174b5ed56a3f3f7d73269
   remove = bcprov-.*[.]jar
 
 # Version should match lib/bouncycastle/BUCK
 [library "bouncyCastleSSL"]
   name = Bouncy Castle Crypto SSL v152
-  url = http://www.bouncycastle.org/download/bcpkix-jdk15on-152.jar
+  url = https://repo1.maven.org/maven2/org/bouncycastle/bcpkix-jdk15on/1.52/bcpkix-jdk15on-1.52.jar
   sha1 = b8ffac2bbc6626f86909589c8cc63637cc936504
   needs = bouncyCastleProvider
   remove = bcpkix-.*[.]jar
@@ -31,14 +31,14 @@
 # Version should match lib/bouncycastle/BUCK
 [library "bouncyCastlePGP"]
   name = Bouncy Castle Crypto OpenPGP v152
-  url = http://www.bouncycastle.org/download/bcpg-jdk15on-152.jar
+  url = https://repo1.maven.org/maven2/org/bouncycastle/bcpg-jdk15on/1.52/bcpg-jdk15on-1.52.jar
   sha1 = ff4665a4b5633ff6894209d5dd10b7e612291858
   needs = bouncyCastleProvider
   remove = bcpg-.*[.]jar
 
 [library "mysqlDriver"]
   name = MySQL Connector/J 5.1.21
-  url = http://repo2.maven.org/maven2/mysql/mysql-connector-java/5.1.21/mysql-connector-java-5.1.21.jar
+  url = https://repo1.maven.org/maven2/mysql/mysql-connector-java/5.1.21/mysql-connector-java-5.1.21.jar
   sha1 = 7abbd19fc2e2d5b92c0895af8520f7fa30266be9
   remove = mysql-connector-java-.*[.]jar
 
diff --git a/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java b/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java
index dc7ce59..af30f73 100644
--- a/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java
+++ b/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java
@@ -71,8 +71,8 @@
     old.save();
 
     final InMemorySecureStore secureStore = new InMemorySecureStore();
-    final InitFlags flags =
-        new InitFlags(site, secureStore, Collections.<String> emptyList());
+    final InitFlags flags = new InitFlags(site, secureStore,
+        Collections.<String> emptyList(), false);
     final ConsoleUI ui = createStrictMock(ConsoleUI.class);
     Section.Factory sections = new Section.Factory() {
       @Override
diff --git a/gerrit-plugin-api/BUCK b/gerrit-plugin-api/BUCK
index 2a55afe..df084ea 100644
--- a/gerrit-plugin-api/BUCK
+++ b/gerrit-plugin-api/BUCK
@@ -28,6 +28,7 @@
     '//gerrit-gwtexpui:server',
     '//gerrit-reviewdb:server',
     '//lib:args4j',
+    '//lib:blame-cache',
     '//lib/dropwizard:dropwizard-core',
     '//lib:guava',
     '//lib:gwtorm',
@@ -39,11 +40,11 @@
     '//lib/guice:guice',
     '//lib/guice:guice-assistedinject',
     '//lib/guice:guice-servlet',
+    '//lib/jgit/org.eclipse.jgit:jgit',
+    '//lib/jgit/org.eclipse.jgit.http.server:jgit-servlet',
     '//lib/joda:joda-time',
     '//lib/log:api',
     '//lib/mina:sshd',
-    '@jgit//org.eclipse.jgit:jgit',
-    '@jgit//org.eclipse.jgit.http.server:jgit-servlet',
   ],
   visibility = ['PUBLIC'],
 )
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 270e15c..e32a0d6 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
@@ -31,9 +31,6 @@
     <requiredProperty key="Implementation-Vendor">
       <defaultValue>Gerrit Code Review</defaultValue>
     </requiredProperty>
-    <requiredProperty key="Implementation-Url">
-      <defaultValue>https://www.gerritcodereview.com/</defaultValue>
-    </requiredProperty>
 
     <requiredProperty key="gerritApiType">
       <defaultValue>plugin</defaultValue>
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/pom.xml b/gerrit-plugin-archetype/src/main/resources/archetype-resources/pom.xml
index a6103b1..026e21d 100644
--- a/gerrit-plugin-archetype/src/main/resources/archetype-resources/pom.xml
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/pom.xml
@@ -50,7 +50,6 @@
 #end
 
               <Implementation-Vendor>${Implementation-Vendor}</Implementation-Vendor>
-              <Implementation-URL>${Implementation-Url}</Implementation-URL>
 
               <Implementation-Title>${Gerrit-ApiType} ${project.artifactId}</Implementation-Title>
               <Implementation-Version>${project.version}</Implementation-Version>
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 3c3508c..32a603b 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
@@ -21,9 +21,6 @@
     <requiredProperty key="Implementation-Vendor">
       <defaultValue>Gerrit Code Review</defaultValue>
     </requiredProperty>
-    <requiredProperty key="Implementation-Url">
-      <defaultValue>https://www.gerritcodereview.com/</defaultValue>
-    </requiredProperty>
     <requiredProperty key="Gwt-Version">
       <defaultValue>2.7.0</defaultValue>
     </requiredProperty>
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/BUCK b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/BUCK
index b224bf6..f33929d 100644
--- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/BUCK
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/BUCK
@@ -10,7 +10,6 @@
     'Gerrit-ApiType: plugin',
     'Gerrit-ApiVersion: ${gerritApiVersion}',
     'Gerrit-Module: ${package}.Module',
-    'Gerrit-HttpModule: ${package}.HttpModule',
   ],
 )
 
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/pom.xml b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/pom.xml
index d67c7cb..2c7fe88 100644
--- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/pom.xml
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/pom.xml
@@ -45,7 +45,6 @@
               <Gerrit-Module>${package}.Module</Gerrit-Module>
               <Gerrit-HttpModule>${package}.HttpModule</Gerrit-HttpModule>
               <Implementation-Vendor>${Implementation-Vendor}</Implementation-Vendor>
-              <Implementation-URL>${Implementation-Url}</Implementation-URL>
 
               <Implementation-Title>${Gerrit-ApiType} ${project.artifactId}</Implementation-Title>
               <Implementation-Version>${project.version}</Implementation-Version>
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java
deleted file mode 100644
index 4f043d0..0000000
--- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java
+++ /dev/null
@@ -1,29 +0,0 @@
-// 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 ${package};
-
-import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.extensions.webui.GwtPlugin;
-import com.google.gerrit.extensions.webui.WebUiPlugin;
-import com.google.gerrit.httpd.plugins.HttpPluginModule;
-
-public class HttpModule extends HttpPluginModule {
-
-  @Override
-  protected void configureServlets() {
-    DynamicSet.bind(binder(), WebUiPlugin.class)
-        .toInstance(new GwtPlugin("hello_gwt_plugin"));
-  }
-}
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/Module.java b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/Module.java
index c734bb7..73e5695 100644
--- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/Module.java
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/Module.java
@@ -15,7 +15,9 @@
 package ${package};
 
 import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.webui.GwtPlugin;
 import com.google.gerrit.extensions.webui.TopMenu;
+import com.google.gerrit.extensions.webui.WebUiPlugin;
 import com.google.inject.AbstractModule;
 
 public class Module extends AbstractModule {
@@ -23,5 +25,7 @@
   @Override
   protected void configure() {
     DynamicSet.bind(binder(), TopMenu.class).to(HelloMenu.class);
+    DynamicSet.bind(binder(), WebUiPlugin.class)
+        .toInstance(new GwtPlugin("hello_gwt_plugin"));
   }
 }
diff --git a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/Plugin.java b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/Plugin.java
index f4f427b..14e3155 100644
--- a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/Plugin.java
+++ b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/Plugin.java
@@ -39,48 +39,48 @@
   }
 
   /** Installed name of the plugin. */
-  public final String getName() {
+  public String getName() {
     return getPluginName();
   }
 
   /** Installed name of the plugin. */
-  public final native String getPluginName()
+  public native String getPluginName()
   /*-{ return this.getPluginName() }-*/;
 
   /** Navigate the UI to the screen identified by the token. */
-  public final native void go(String token)
+  public native void go(String token)
   /*-{ return this.go(token) }-*/;
 
   /** Refresh the current UI. */
-  public final native void refresh()
+  public native void refresh()
   /*-{ return this.refresh() }-*/;
 
   /** Refresh Gerrit's menu bar. */
-  public final native void refreshMenuBar()
+  public native void refreshMenuBar()
   /*-{ return this.refreshMenuBar() }-*/;
 
   /** @return the preferences of the currently signed in user, the default preferences if not signed in */
-  public final native GeneralPreferences getUserPreferences()
+  public native GeneralPreferences getUserPreferences()
   /*-{ return this.getUserPreferences() }-*/;
 
   /** Refresh the user preferences of the current user. */
-  public final native void refreshUserPreferences()
+  public native void refreshUserPreferences()
   /*-{ return this.refreshUserPreferences() }-*/;
 
   /** @return the server info */
-  public final native ServerInfo getServerInfo()
+  public native ServerInfo getServerInfo()
   /*-{ return this.getServerInfo() }-*/;
 
   /** @return the current user */
-  public final native AccountInfo getCurrentUser()
+  public native AccountInfo getCurrentUser()
   /*-{ return this.getCurrentUser() }-*/;
 
   /** Check if user is signed in. */
-  public final native boolean isSignedIn()
+  public native boolean isSignedIn()
   /*-{ return this.isSignedIn() }-*/;
 
   /** Show message in Gerrit's ErrorDialog. */
-  public final native void showError(String message)
+  public native void showError(String message)
   /*-{ return this.showError(message) }-*/;
 
   /**
@@ -90,11 +90,11 @@
    *        regular expression matching use {@code screenRegex()} .
    * @param entry callback function invoked to create the screen widgets.
    */
-  public final void screen(String token, Screen.EntryPoint entry) {
+  public void screen(String token, Screen.EntryPoint entry) {
     screen(token, wrap(entry));
   }
 
-  private final native void screen(String t, JavaScriptObject e)
+  private native void screen(String t, JavaScriptObject e)
   /*-{ this.screen(t, e) }-*/;
 
   /**
@@ -105,11 +105,11 @@
    *        {@code Screen} object passed into the {@code Screen.EntryPoint}.
    * @param entry callback function invoked to create the screen widgets.
    */
-  public final void screenRegex(String regex, Screen.EntryPoint entry) {
+  public void screenRegex(String regex, Screen.EntryPoint entry) {
     screenRegex(regex, wrap(entry));
   }
 
-  private final native void screenRegex(String p, JavaScriptObject e)
+  private native void screenRegex(String p, JavaScriptObject e)
   /*-{ this.screen(new $wnd.RegExp(p), e) }-*/;
 
   /**
@@ -118,11 +118,11 @@
    * @param token literal anchor token appearing after the plugin name.
    * @param entry callback function invoked to create the settings screen widgets.
    */
-  public final void settingsScreen(String token, String menu, Screen.EntryPoint entry) {
+  public void settingsScreen(String token, String menu, Screen.EntryPoint entry) {
     settingsScreen(token, menu, wrap(entry));
   }
 
-  private final native void settingsScreen(String t, String m, JavaScriptObject e)
+  private native void settingsScreen(String t, String m, JavaScriptObject e)
   /*-{ this.settingsScreen(t, m, e) }-*/;
 
   /**
@@ -132,11 +132,11 @@
    *        registered.
    * @param entry callback function invoked to create the panel widgets.
    */
-  public final void panel(GerritUiExtensionPoint extensionPoint, Panel.EntryPoint entry) {
+  public void panel(GerritUiExtensionPoint extensionPoint, Panel.EntryPoint entry) {
     panel(extensionPoint.name(), wrap(entry));
   }
 
-  private final native void panel(String i, JavaScriptObject e)
+  private native void panel(String i, JavaScriptObject e)
   /*-{ this.panel(i, e) }-*/;
 
   protected Plugin() {
@@ -144,17 +144,17 @@
 
   native void _initialized() /*-{ this._success = true }-*/;
   native void _loaded() /*-{ this._loadedGwt() }-*/;
-  private static final native Plugin install(String u)
+  private static native Plugin install(String u)
   /*-{ return $wnd.Gerrit.installGwt(u) }-*/;
 
-  private static final native JavaScriptObject wrap(Screen.EntryPoint b) /*-{
+  private static native JavaScriptObject wrap(Screen.EntryPoint b) /*-{
     return $entry(function(c){
       b.@com.google.gerrit.plugin.client.screen.Screen.EntryPoint::onLoad(Lcom/google/gerrit/plugin/client/screen/Screen;)(
         @com.google.gerrit.plugin.client.screen.Screen::new(Lcom/google/gerrit/plugin/client/screen/Screen$Context;)(c));
     });
   }-*/;
 
-  private static final native JavaScriptObject wrap(Panel.EntryPoint b) /*-{
+  private static native JavaScriptObject wrap(Panel.EntryPoint b) /*-{
     return $entry(function(c){
       b.@com.google.gerrit.plugin.client.extension.Panel.EntryPoint::onLoad(Lcom/google/gerrit/plugin/client/extension/Panel;)(
         @com.google.gerrit.plugin.client.extension.Panel::new(Lcom/google/gerrit/plugin/client/extension/Panel$Context;)(c));
diff --git a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/extension/Panel.java b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/extension/Panel.java
index 1fff691..0200a14 100644
--- a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/extension/Panel.java
+++ b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/extension/Panel.java
@@ -60,20 +60,20 @@
   }
 
   static final class Context extends JavaScriptObject {
-    final native Element body() /*-{ return this.body }-*/;
+    native Element body() /*-{ return this.body }-*/;
 
-    final native String get(String k) /*-{ return this.p[k]; }-*/;
-    final native int getInt(String k, int d) /*-{
+    native String get(String k) /*-{ return this.p[k]; }-*/;
+    native int getInt(String k, int d) /*-{
       return this.p.hasOwnProperty(k) ? this.p[k] : d
     }-*/;
-    final native int getBoolean(String k, boolean d) /*-{
+    native int getBoolean(String k, boolean d) /*-{
       return this.p.hasOwnProperty(k) ? this.p[k] : d
     }-*/;
-    final native JavaScriptObject getObject(String k)
+    native JavaScriptObject getObject(String k)
     /*-{ return this.p[k]; }-*/;
 
 
-    final native void detach(Panel p) /*-{
+    native void detach(Panel p) /*-{
       this.onUnload($entry(function(){
         p.@com.google.gwt.user.client.ui.Widget::onDetach()();
       }));
diff --git a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/screen/Screen.java b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/screen/Screen.java
index f91464f..5e0ba4a 100644
--- a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/screen/Screen.java
+++ b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/screen/Screen.java
@@ -62,12 +62,12 @@
   }
 
   static final class Context extends JavaScriptObject {
-    final native Element body() /*-{ return this.body }-*/;
-    final native JsArrayString token_match() /*-{ return this.token_match }-*/;
-    final native void show() /*-{ this.show() }-*/;
-    final native void setTitle(String t) /*-{ this.setTitle(t) }-*/;
-    final native void setWindowTitle(String t) /*-{ this.setWindowTitle(t) }-*/;
-    final native void detach(Screen s) /*-{
+    native Element body() /*-{ return this.body }-*/;
+    native JsArrayString token_match() /*-{ return this.token_match }-*/;
+    native void show() /*-{ this.show() }-*/;
+    native void setTitle(String t) /*-{ this.setTitle(t) }-*/;
+    native void setWindowTitle(String t) /*-{ this.setWindowTitle(t) }-*/;
+    native void detach(Screen s) /*-{
       this.onUnload($entry(function(){
         s.@com.google.gwt.user.client.ui.Widget::onDetach()();
       }));
@@ -87,7 +87,7 @@
   }
 
   /** @return the token suffix after {@code "/#/x/plugin-name/"}. */
-  public final String getToken() {
+  public String getToken() {
     return getToken(0);
   }
 
@@ -96,12 +96,12 @@
    *        group 0 is the entire token, see {@link #getToken()}.
    * @return the token from the regex match group.
    */
-  public final String getToken(int group) {
+  public String getToken(int group) {
     return ctx.token_match().get(group);
   }
 
   /** @return total number of token groups. */
-  public final int getTokenGroups() {
+  public int getTokenGroups() {
     return ctx.token_match().length();
   }
 
@@ -110,7 +110,7 @@
    *
    * @param titleText text to display above the widget.
    */
-  public final void setPageTitle(String titleText) {
+  public void setPageTitle(String titleText) {
     ctx.setTitle(titleText);
   }
 
@@ -119,7 +119,7 @@
    *
    * @param titleText text to display in the window title bar.
    */
-  public final void setWindowTitle(String titleText) {
+  public void setWindowTitle(String titleText) {
     ctx.setWindowTitle(titleText);
   }
 
@@ -128,13 +128,13 @@
    *
    * @param w child containing the content.
    */
-  public final void show(Widget w) {
+  public void show(Widget w) {
     setWidget(w);
     ctx.show();
   }
 
   /** Show this screen in the web interface. */
-  public final void show() {
+  public void show() {
     ctx.show();
   }
 }
diff --git a/gerrit-plugin-js-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml b/gerrit-plugin-js-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
index fbf1e46..ef0e96c 100644
--- a/gerrit-plugin-js-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
+++ b/gerrit-plugin-js-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
@@ -21,9 +21,6 @@
     <requiredProperty key="Implementation-Vendor">
       <defaultValue>Gerrit Code Review</defaultValue>
     </requiredProperty>
-    <requiredProperty key="Implementation-Url">
-      <defaultValue>https://gerrit.googlesource.com/</defaultValue>
-    </requiredProperty>
 
     <requiredProperty key="gerritApiType">
       <defaultValue>js</defaultValue>
diff --git a/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/pom.xml b/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/pom.xml
index f24d81e..8f4aadd 100644
--- a/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/pom.xml
+++ b/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/pom.xml
@@ -44,7 +44,6 @@
             <manifestEntries>
               <Gerrit-PluginName>${pluginName}</Gerrit-PluginName>
               <Implementation-Vendor>${Implementation-Vendor}</Implementation-Vendor>
-              <Implementation-URL>${Implementation-Url}</Implementation-URL>
 
               <Implementation-Title>${Gerrit-ApiType} ${project.artifactId}</Implementation-Title>
               <Implementation-Version>${project.version}</Implementation-Version>
diff --git a/gerrit-prettify/BUCK b/gerrit-prettify/BUCK
index baaf56d..bf2e02a 100644
--- a/gerrit-prettify/BUCK
+++ b/gerrit-prettify/BUCK
@@ -3,15 +3,10 @@
 gwt_module(
   name = 'client',
   srcs = glob([
-    SRC + 'client/**/*.java',
     SRC + 'common/**/*.java',
   ]),
   gwt_xml = SRC + 'PrettyFormatter.gwt.xml',
-  resources = glob([
-    'src/main/java/com/google/gerrit/prettify/client/*.properties',
-  ]),
   deps = [
-    ':google-code-prettify',
     '//gerrit-gwtexpui:SafeHtml',
   ],
   exported_deps = [
@@ -27,16 +22,6 @@
 )
 
 java_library(
-  name = 'google-code-prettify',
-  resources = glob([
-    'src/main/resources/com/google/gerrit/prettify/client/**/*',
-  ]),
-  deps = [
-    '//lib:LICENSE-Apache2.0',
-  ],
-)
-
-java_library(
   name = 'server',
   srcs = glob([SRC + 'common/**/*.java']),
   deps = [
@@ -44,7 +29,7 @@
     '//gerrit-reviewdb:server',
     '//lib:guava',
     '//lib:gwtjsonrpc',
-    '@jgit//org.eclipse.jgit:jgit',
+    '//lib/jgit/org.eclipse.jgit:jgit',
   ],
   visibility = ['PUBLIC'],
 )
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/PrettyFormatter.gwt.xml b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/PrettyFormatter.gwt.xml
index fd88f6c..06035d27 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/PrettyFormatter.gwt.xml
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/PrettyFormatter.gwt.xml
@@ -24,5 +24,4 @@
   <inherits name='com.google.gwt.resources.Resources'/>
   <inherits name='com.google.gwtexpui.safehtml.SafeHtml'/>
   <source path='common' />
-  <source path='client' />
 </module>
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/ClientSideFormatter.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/ClientSideFormatter.java
deleted file mode 100644
index 34ddde2..0000000
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/ClientSideFormatter.java
+++ /dev/null
@@ -1,80 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.prettify.client;
-
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.user.client.ui.RootPanel;
-
-/** Evaluates prettify using the host browser's JavaScript engine. */
-public class ClientSideFormatter extends PrettyFormatter {
-  public static final PrettyFactory FACTORY = new PrettyFactory() {
-    @Override
-    public PrettyFormatter get() {
-      return new ClientSideFormatter();
-    }
-  };
-
-  private static final PrivateScopeImpl prettify;
-
-  static {
-    Resources.I.prettify_css().ensureInjected();
-    Resources.I.gerrit_css().ensureInjected();
-
-    prettify = GWT.create(PrivateScopeImpl.class);
-    RootPanel.get().add(prettify);
-
-    prettify.compile(Resources.I.core());
-    prettify.compile(Resources.I.lang_apollo());
-    prettify.compile(Resources.I.lang_basic());
-    prettify.compile(Resources.I.lang_clj());
-    prettify.compile(Resources.I.lang_css());
-    prettify.compile(Resources.I.lang_dart());
-    prettify.compile(Resources.I.lang_erlang());
-    prettify.compile(Resources.I.lang_go());
-    prettify.compile(Resources.I.lang_hs());
-    prettify.compile(Resources.I.lang_lisp());
-    prettify.compile(Resources.I.lang_llvm());
-    prettify.compile(Resources.I.lang_lua());
-    prettify.compile(Resources.I.lang_matlab());
-    prettify.compile(Resources.I.lang_ml());
-    prettify.compile(Resources.I.lang_mumps());
-    prettify.compile(Resources.I.lang_n());
-    prettify.compile(Resources.I.lang_pascal());
-    prettify.compile(Resources.I.lang_proto());
-    prettify.compile(Resources.I.lang_r());
-    prettify.compile(Resources.I.lang_rd());
-    prettify.compile(Resources.I.lang_scala());
-    prettify.compile(Resources.I.lang_sql());
-    prettify.compile(Resources.I.lang_tcl());
-    prettify.compile(Resources.I.lang_tex());
-    prettify.compile(Resources.I.lang_vb());
-    prettify.compile(Resources.I.lang_vhdl());
-    prettify.compile(Resources.I.lang_wiki());
-    prettify.compile(Resources.I.lang_xq());
-    prettify.compile(Resources.I.lang_yaml());
-  }
-
-  @Override
-  protected String prettify(String html, String type) {
-    return go(prettify.getContext(), html, type);
-  }
-
-  private static native String go(JavaScriptObject ctx, String srcText,
-      String srcType)
-  /*-{
-     return ctx.prettyPrintOne(srcText, srcType);
-  }-*/;
-}
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettifyConstants.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettifyConstants.java
deleted file mode 100644
index c191fa5..0000000
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettifyConstants.java
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.prettify.client;
-
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.i18n.client.Constants;
-
-public interface PrettifyConstants extends Constants {
-  static final PrettifyConstants C = GWT.create(PrettifyConstants.class);
-
-  String wseTabAfterSpace();
-  String wseTrailingSpace();
-  String wseBareCR();
-  String leCR();
-}
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettifyConstants.properties b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettifyConstants.properties
deleted file mode 100644
index 97ab0cf..0000000
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettifyConstants.properties
+++ /dev/null
@@ -1,4 +0,0 @@
-wseTabAfterSpace=Whitespace error: Tab after space
-wseTrailingSpace=Whitespace error: Trailing space at end of line
-wseBareCR=CR without LF
-leCR=Carriage Return
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
deleted file mode 100644
index 49dc2fc..0000000
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFormatter.java
+++ /dev/null
@@ -1,561 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.prettify.client;
-
-import com.google.gerrit.extensions.client.DiffPreferencesInfo;
-import com.google.gerrit.prettify.common.SparseFileContent;
-import com.google.gwtexpui.safehtml.client.SafeHtml;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-
-import org.eclipse.jgit.diff.Edit;
-import org.eclipse.jgit.diff.ReplaceEdit;
-
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-public abstract class PrettyFormatter implements SparseHtmlFile {
-  public abstract static class EditFilter {
-    abstract String getStyleName();
-
-    abstract int getBegin(Edit edit);
-
-    abstract int getEnd(Edit edit);
-  }
-
-  public static final EditFilter A = new EditFilter() {
-    @Override
-    String getStyleName() {
-      return "wdd";
-    }
-
-    @Override
-    int getBegin(Edit edit) {
-      return edit.getBeginA();
-    }
-
-    @Override
-    int getEnd(Edit edit) {
-      return edit.getEndA();
-    }
-  };
-
-  public static final EditFilter B = new EditFilter() {
-    @Override
-    String getStyleName() {
-      return "wdi";
-    }
-
-    @Override
-    int getBegin(Edit edit) {
-      return edit.getBeginB();
-    }
-
-    @Override
-    int getEnd(Edit edit) {
-      return edit.getEndB();
-    }
-  };
-
-  protected SparseFileContent content;
-  protected EditFilter side;
-  protected List<Edit> edits;
-  protected DiffPreferencesInfo diffPrefs;
-  protected String fileName;
-  protected Set<Integer> trailingEdits;
-
-  private int col;
-  private int lineIdx;
-  private Tag lastTag;
-  private StringBuilder buf;
-
-  @Override
-  public SafeHtml getSafeHtmlLine(int lineNo) {
-    return SafeHtml.asis(content.get(lineNo));
-  }
-
-  @Override
-  public int size() {
-    return content.size();
-  }
-
-  @Override
-  public boolean contains(int idx) {
-    return content.contains(idx);
-  }
-
-  @Override
-  public boolean hasTrailingEdit(int idx) {
-    return trailingEdits.contains(idx);
-  }
-
-  public void setEditFilter(EditFilter f) {
-    side = f;
-  }
-
-  public void setEditList(List<Edit> all) {
-    edits = all;
-  }
-
-  public void setDiffPrefs(DiffPreferencesInfo how) {
-    diffPrefs = how;
-  }
-
-  public void setFileName(String fileName) {
-    this.fileName = fileName;
-  }
-
-  /**
-   * Parse and format a complete source code file.
-   *
-   * @param src raw content of the file to format. The line strings will be HTML
-   *        escaped before processing, so it must be the raw text.
-   */
-  public void format(SparseFileContent src) {
-    content = new SparseFileContent();
-    content.setSize(src.size());
-    trailingEdits = new HashSet<>();
-
-    String html = toHTML(src);
-
-    html = expandTabs(html);
-    if (diffPrefs.syntaxHighlighting && getFileType() != null
-        && src.isWholeFile()) {
-      // The prettify parsers don't like &#39; as an entity for the
-      // single quote character. Replace them all out so we don't
-      // confuse the parser.
-      //
-      html = html.replaceAll("&#39;", "'");
-
-      // If a line is modified at its end and the line ending is changed from
-      // '\n' to '\r\n' then the '\r' of the new line is part of the modified
-      // text. If intraline diffs are highlighted the modified text is
-      // surrounded by a 'span' tag. As result '\r' and '\n' of the new line get
-      // separated by '</span>'. For the prettify parser this now looks like two
-      // separate line endings. This messes up the line counting below.
-      // Drop any '\r' to avoid this problem.
-      html = html.replaceAll("\r</span>(<span class=\"wdc\">)?\n", "</span>$1\n");
-
-      html = html.replaceAll("(\r)?\n", " $1\n");
-      html = prettify(html, getFileType());
-      html = html.replaceAll(" (\r)?\n", "$1\n");
-    }
-
-    int pos = 0;
-    int textChunkStart = 0;
-
-    lastTag = Tag.NULL;
-    col = 0;
-    lineIdx = 0;
-
-    buf = new StringBuilder();
-    while (pos <= html.length()) {
-      int tagStart = html.indexOf('<', pos);
-      int lf = html.indexOf('\n', pos);
-
-      if (tagStart < 0 && lf < 0) {
-        // No more tags remaining. What's left is plain text.
-        //
-        assert lastTag == Tag.NULL;
-        pos = html.length();
-        if (textChunkStart < pos) {
-          htmlText(html.substring(textChunkStart, pos));
-        }
-        if (0 < buf.length()) {
-          content.addLine(src.mapIndexToLine(lineIdx), buf.toString());
-        }
-        break;
-      }
-
-      // Line end occurs before the next HTML tag. Break the line.
-      if (0 <= lf && (lf < tagStart || tagStart < 0)) {
-        if (textChunkStart < lf) {
-          lastTag.open(buf, html);
-          htmlText(html.substring(textChunkStart, lf));
-        }
-        pos = lf + 1;
-        textChunkStart = pos;
-
-        lastTag.close(buf, html);
-        content.addLine(src.mapIndexToLine(lineIdx++), buf.toString());
-        buf = new StringBuilder();
-        col = 0;
-        continue;
-      }
-
-      // Assume no attribute contains '>' and that all tags
-      // within the HTML will be well-formed.
-      //
-      int tagEnd = html.indexOf('>', tagStart);
-      assert tagStart < tagEnd;
-      pos = tagEnd + 1;
-
-      // Handle any text between the end of the last tag,
-      // and the start of this tag.
-      //
-      if (textChunkStart < tagStart) {
-        lastTag.open(buf, html);
-        htmlText(html.substring(textChunkStart, tagStart));
-      }
-      textChunkStart = pos;
-
-      if (html.charAt(tagStart + 1) == '/') {
-        lastTag = lastTag.pop(buf, html);
-
-      } else if (html.charAt(tagEnd - 1) != '/') {
-        lastTag = new Tag(lastTag, tagStart, tagEnd);
-      }
-    }
-    buf = null;
-  }
-
-  private void htmlText(String txt) {
-    int pos = 0;
-    while (pos < txt.length()) {
-      int start = txt.indexOf('&', pos);
-      if (start < 0) {
-        break;
-      }
-
-      cleanText(txt, pos, start);
-      pos = txt.indexOf(';', start + 1) + 1;
-
-      if (diffPrefs.lineLength <= col) {
-        buf.append("<br />");
-        col = 0;
-      }
-
-      buf.append(txt.substring(start, pos));
-      col++;
-    }
-
-    cleanText(txt, pos, txt.length());
-  }
-
-  private void cleanText(String txt, int pos, int end) {
-    while (pos < end) {
-      int free = diffPrefs.lineLength - col;
-      if (free <= 0) {
-        // The current line is full. Throw an explicit line break
-        // onto the end, and we'll continue on the next line.
-        //
-        buf.append("<br />");
-        col = 0;
-        free = diffPrefs.lineLength;
-      }
-
-      int n = Math.min(end - pos, free);
-      buf.append(txt.substring(pos, pos + n));
-      col += n;
-      pos += n;
-    }
-  }
-
-  /** Run the prettify engine over the text and return the result. */
-  protected abstract String prettify(String html, String type);
-
-  private static class Tag {
-    static final Tag NULL = new Tag(null, 0, 0) {
-      @Override
-      void open(StringBuilder buf, String html) {
-      }
-
-      @Override
-      void close(StringBuilder buf, String html) {
-      }
-
-      @Override
-      Tag pop(StringBuilder buf, String html) {
-        return this;
-      }
-    };
-
-    final Tag parent;
-    final int start;
-    final int end;
-    boolean open;
-
-    Tag(Tag p, int s, int e) {
-      parent = p;
-      start = s;
-      end = e;
-    }
-
-    void open(StringBuilder buf, String html) {
-      if (!open) {
-        parent.open(buf, html);
-        buf.append(html.substring(start, end + 1));
-        open = true;
-      }
-    }
-
-    void close(StringBuilder buf, String html) {
-      pop(buf, html);
-      parent.close(buf, html);
-    }
-
-    Tag pop(StringBuilder buf, String html) {
-      if (open) {
-        int sp = html.indexOf(' ', start + 1);
-        if (sp < 0 || end < sp) {
-          sp = end;
-        }
-
-        buf.append("</");
-        buf.append(html.substring(start + 1, sp));
-        buf.append('>');
-        open = false;
-      }
-      return parent;
-    }
-  }
-
-  private String toHTML(SparseFileContent src) {
-    SafeHtml html;
-
-    if (diffPrefs.intralineDifference) {
-      html = colorLineEdits(src);
-    } else {
-      SafeHtmlBuilder b = new SafeHtmlBuilder();
-      for (int index = src.first(); index < src.size(); index = src.next(index)) {
-        b.append(src.get(index));
-        b.append('\n');
-      }
-      html = b;
-
-      final String r = "<span class=\"wse\"" //
-          + " title=\"" + PrettifyConstants.C.wseBareCR() + "\"" //
-          + ">&nbsp;</span>$1";
-      html = html.replaceAll("\r([^\n])", r);
-    }
-
-    if (diffPrefs.showWhitespaceErrors) {
-      // We need to do whitespace errors before showing tabs, because
-      // these patterns rely on \t as a literal, before it expands.
-      //
-      html = showTabAfterSpace(html);
-      html = showTrailingWhitespace(html);
-    }
-
-    if (diffPrefs.showLineEndings){
-      html = showLineEndings(html);
-    }
-
-    if (diffPrefs.showTabs) {
-      String t = 1 < diffPrefs.tabSize ? "\t" : "";
-      html = html.replaceAll("\t", "<span class=\"vt\">\u00BB</span>" + t);
-    }
-
-    return html.asString();
-  }
-
-  private SafeHtml colorLineEdits(SparseFileContent src) {
-    // Make a copy of the edits with a sentinel that is after all lines
-    // 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<>(this.edits.size() + 1);
-    edits.addAll(this.edits);
-    edits.add(new Edit(src.size(), src.size()));
-
-    SafeHtmlBuilder buf = new SafeHtmlBuilder();
-
-    int curIdx = 0;
-    Edit curEdit = edits.get(curIdx);
-
-    ReplaceEdit lastReplace = null;
-    List<Edit> charEdits = null;
-    int lastPos = 0;
-    int lastIdx = 0;
-
-    for (int index = src.first(); index < src.size(); index = src.next(index)) {
-      int cmp = compare(index, curEdit);
-      while (0 < cmp) {
-        // The index is after the edit. Skip to the next edit.
-        //
-        curEdit = edits.get(curIdx++);
-        cmp = compare(index, curEdit);
-      }
-
-      if (cmp < 0) {
-        // index occurs before the edit. This is a line of context.
-        //
-        appendShowBareCR(buf, src.get(index), true);
-        buf.append('\n');
-        continue;
-      }
-
-      // index occurs within the edit. The line is a modification.
-      //
-      if (curEdit instanceof ReplaceEdit) {
-        if (lastReplace != curEdit) {
-          lastReplace = (ReplaceEdit) curEdit;
-          charEdits = lastReplace.getInternalEdits();
-          lastPos = 0;
-          lastIdx = 0;
-        }
-
-        String line = src.get(index) + "\n";
-        for (int c = 0; c < line.length();) {
-          if (charEdits == null || (charEdits.size() <= lastIdx)) {
-            appendShowBareCR(buf, line.substring(c), false);
-            break;
-          }
-
-          final Edit edit = charEdits.get(lastIdx);
-          final int b = side.getBegin(edit) - lastPos;
-          final int e = side.getEnd(edit) - lastPos;
-
-          if (c < b) {
-            // There is text at the start of this line that is common
-            // with the other side. Copy it with no style around it.
-            //
-            final int cmnLen = Math.min(b, line.length());
-            buf.openSpan();
-            buf.setStyleName("wdc");
-            appendShowBareCR(buf, line.substring(c, cmnLen), //
-                cmnLen == line.length() - 1);
-            buf.closeSpan();
-            c = cmnLen;
-          }
-
-          final int modLen = Math.min(e, line.length());
-          if (c < e && c < modLen) {
-            buf.openSpan();
-            buf.setStyleName(side.getStyleName());
-            appendShowBareCR(buf, line.substring(c, modLen), //
-                modLen == line.length() - 1);
-            buf.closeSpan();
-            if (modLen == line.length()) {
-              trailingEdits.add(index);
-            }
-            c = modLen;
-          }
-
-          if (e <= c) {
-            lastIdx++;
-          }
-        }
-        lastPos += line.length();
-
-      } else {
-        appendShowBareCR(buf, src.get(index), true);
-        buf.append('\n');
-      }
-    }
-    return buf;
-  }
-
-  private void appendShowBareCR(SafeHtmlBuilder buf, String src, boolean end) {
-    while (!src.isEmpty()) {
-      int cr = src.indexOf('\r');
-      if (cr < 0) {
-        buf.append(src);
-        return;
-
-      } else if (end) {
-        if (cr == src.length() - 1) {
-          buf.append(src.substring(0, cr + 1));
-          return;
-        }
-      } else if (cr == src.length() - 2 && src.charAt(cr + 1) == '\n') {
-        buf.append(src);
-        return;
-      }
-
-      buf.append(src.substring(0, cr));
-      buf.openSpan();
-      buf.setStyleName("wse");
-      buf.setAttribute("title", PrettifyConstants.C.wseBareCR());
-      buf.nbsp();
-      buf.closeSpan();
-      src = src.substring(cr + 1);
-    }
-  }
-
-  private int compare(int index, Edit edit) {
-    if (index < side.getBegin(edit)) {
-      return -1; // index occurs before the edit.
-
-    } else if (index < side.getEnd(edit)) {
-      return 0; // index occurs within the edit.
-
-    } else {
-      return 1; // index occurs after the edit.
-    }
-  }
-
-  private SafeHtml showTabAfterSpace(SafeHtml src) {
-    final String m = "( ( |<span[^>]*>|</span>)*\t)";
-    final String r = "<span class=\"wse\"" //
-        + " title=\"" + PrettifyConstants.C.wseTabAfterSpace() + "\"" //
-        + ">$1</span>";
-    src = src.replaceFirst("^" + m, r);
-    src = src.replaceAll("\n" + m, "\n" + r);
-    return src;
-  }
-
-  private SafeHtml showTrailingWhitespace(SafeHtml src) {
-    final String r = "<span class=\"wse\"" //
-        + " title=\"" + PrettifyConstants.C.wseTrailingSpace() + "\"" //
-        + ">$1</span>$2";
-    src = src.replaceAll("([ \t][ \t]*)(\r?(</span>)?\n)", r);
-    src = src.replaceFirst("([ \t][ \t]*)(\r?(</span>)?\n?)$", r);
-    return src;
-  }
-
-  private SafeHtml showLineEndings(SafeHtml src) {
-    final String r = "<span class=\"lecr\""
-        + " title=\"" + PrettifyConstants.C.leCR() + "\"" //
-        + ">\\\\r</span>";
-    src = src.replaceAll("\r", r);
-    return src;
-  }
-
-  private String expandTabs(String html) {
-    StringBuilder tmp = new StringBuilder();
-    int i = 0;
-    if (diffPrefs.showTabs) {
-      i = 1;
-    }
-    for (; i < diffPrefs.tabSize; i++) {
-      tmp.append("&nbsp;");
-    }
-    return html.replaceAll("\t", tmp.toString());
-  }
-
-  private String getFileType() {
-    String srcType = fileName;
-    if (srcType == null) {
-      return null;
-    }
-
-    int dot = srcType.lastIndexOf('.');
-    if (dot < 0) {
-      return null;
-    }
-
-    if (0 < dot) {
-      srcType = srcType.substring(dot + 1);
-    }
-
-    if ("txt".equalsIgnoreCase(srcType)) {
-      return null;
-    }
-
-    return srcType;
-  }
-}
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrivateScopeImpl.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrivateScopeImpl.java
deleted file mode 100644
index 65ee212..0000000
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrivateScopeImpl.java
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.prettify.client;
-
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.resources.client.TextResource;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.NamedFrame;
-
-/**
- * Creates a private JavaScript environment, typically inside an IFrame.
- * <p>
- * Instances must be created through {@code GWT.create(PrivateScopeImpl.class)}.
- * A scope must remain attached to the primary document for its entire life.
- * Behavior is undefined if a scope is detached and attached again later. It is
- * best to attach the scope with {@code RootPanel.get().add(scope)} as soon as
- * it has been created.
- */
-public class PrivateScopeImpl extends Composite {
-  private static int scopeId;
-
-  protected final String scopeName;
-
-  public PrivateScopeImpl() {
-    scopeName = nextScopeName();
-
-    NamedFrame frame = new NamedFrame(scopeName);
-    frame.setUrl("javascript:''");
-    initWidget(frame);
-
-    setVisible(false);
-  }
-
-  public void compile(TextResource js) {
-    eval(js.getText());
-  }
-
-  public void eval(String js) {
-    nativeEval(getContext(), js);
-  }
-
-  public JavaScriptObject getContext() {
-    return nativeGetContext(scopeName);
-  }
-
-  private static String nextScopeName() {
-    return "_PrivateScope" + (++scopeId);
-  }
-
-  private static native void nativeEval(JavaScriptObject ctx, String js)
-  /*-{ ctx.eval(js); }-*/;
-
-  private static native JavaScriptObject nativeGetContext(String scopeName)
-  /*-{ return $wnd[scopeName]; }-*/;
-}
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrivateScopeImplIE8.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrivateScopeImplIE8.java
deleted file mode 100644
index 0496d91..0000000
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrivateScopeImplIE8.java
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.prettify.client;
-
-import com.google.gwt.core.client.JavaScriptObject;
-
-/** MSIE requires us to initialize the document before we can use it. */
-public class PrivateScopeImplIE8 extends PrivateScopeImpl {
-  private JavaScriptObject context;
-
-  @Override
-  protected void onAttach() {
-    super.onAttach();
-    context = nativeInitContext(scopeName);
-  }
-
-  @Override
-  public JavaScriptObject getContext() {
-    return context;
-  }
-
-  private static native JavaScriptObject nativeInitContext(String scopeName)
-  /*-{
-    var fe = $wnd[scopeName];
-    fe.document.write(
-        '<script>'
-      + 'parent._PrivateScopeNewChild = this;'
-      + '</' + 'script>'
-    );
-    var ctx = $wnd._PrivateScopeNewChild;
-    $wnd._PrivateScopeNewChild = undefined;
-    return ctx;
-  }-*/;
-}
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/Resources.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/Resources.java
deleted file mode 100644
index 93c2988..0000000
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/Resources.java
+++ /dev/null
@@ -1,63 +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.prettify.client;
-
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.resources.client.ClientBundle;
-import com.google.gwt.resources.client.CssResource;
-import com.google.gwt.resources.client.TextResource;
-
-/** Loads the minimized form of prettify into the client. */
-interface Resources extends ClientBundle {
-  static final Resources I = GWT.create(Resources.class);
-
-  @Source("prettify.css")
-  CssResource prettify_css();
-
-  @Source("gerrit.css")
-  CssResource gerrit_css();
-
-  @Source("prettify.js")
-  TextResource core();
-
-  @Source("lang-apollo.js") TextResource lang_apollo();
-  @Source("lang-basic.js") TextResource lang_basic();
-  @Source("lang-clj.js") TextResource lang_clj();
-  @Source("lang-css.js") TextResource lang_css();
-  @Source("lang-dart.js") TextResource lang_dart();
-  @Source("lang-erlang.js") TextResource lang_erlang();
-  @Source("lang-go.js") TextResource lang_go();
-  @Source("lang-hs.js") TextResource lang_hs();
-  @Source("lang-lisp.js") TextResource lang_lisp();
-  @Source("lang-llvm.js") TextResource lang_llvm();
-  @Source("lang-lua.js") TextResource lang_lua();
-  @Source("lang-matlab.js") TextResource lang_matlab();
-  @Source("lang-ml.js") TextResource lang_ml();
-  @Source("lang-mumps.js") TextResource lang_mumps();
-  @Source("lang-n.js") TextResource lang_n();
-  @Source("lang-pascal.js") TextResource lang_pascal();
-  @Source("lang-proto.js") TextResource lang_proto();
-  @Source("lang-r.js") TextResource lang_r();
-  @Source("lang-rd.js") TextResource lang_rd();
-  @Source("lang-scala.js") TextResource lang_scala();
-  @Source("lang-sql.js") TextResource lang_sql();
-  @Source("lang-tcl.js") TextResource lang_tcl();
-  @Source("lang-tex.js") TextResource lang_tex();
-  @Source("lang-vb.js") TextResource lang_vb();
-  @Source("lang-vhdl.js") TextResource lang_vhdl();
-  @Source("lang-wiki.js") TextResource lang_wiki();
-  @Source("lang-xq.js") TextResource lang_xq();
-  @Source("lang-yaml.js") TextResource lang_yaml();
-}
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/SparseHtmlFile.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/SparseHtmlFile.java
deleted file mode 100644
index 3fd34bf..0000000
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/SparseHtmlFile.java
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.prettify.client;
-
-import com.google.gwtexpui.safehtml.client.SafeHtml;
-
-public interface SparseHtmlFile {
-  /** @return the line of formatted HTML. */
-  SafeHtml getSafeHtmlLine(int lineNo);
-
-  /** @return the number of lines in this sparse list. */
-  int size();
-
-  /** @return true if the line is valid in this sparse list. */
-  boolean contains(int idx);
-
-  /** @return true if this line ends in the middle of a character edit span. */
-  boolean hasTrailingEdit(int idx);
-}
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/gerrit.css b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/gerrit.css
deleted file mode 100644
index 23e7e46..0000000
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/gerrit.css
+++ /dev/null
@@ -1,98 +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.
- */
-
-@external .wse;
-@external .lecr;
-@external .vt;
-@external .wdd;
-@external .wdi;
-
-@external .str;
-@external .kwd;
-@external .com;
-@external .typ;
-@external .lit;
-@external .pun;
-@external .pln;
-@external .tag;
-@external .atn;
-@external .atv;
-@external .dec;
-
-.wse {
-  background: red;
-  cursor: pointer;
-}
-
-.lecr {
-  border-bottom: #aaaaaa 1px dashed;
-  border-left: #aaaaaa 1px dashed;
-  padding-bottom: 0px;
-  margin: 0px 2px;
-  padding-left: 2px;
-  padding-right: 2px;
-  border-top: #aaaaaa 1px dashed;
-  border-right: #aaaaaa 1px dashed;
-  padding-top: 0px;
-  cursor: pointer;
-}
-
-.vt,
-.vt .str,
-.vt .kwd,
-.vt .com,
-.vt .typ,
-.vt .lit,
-.vt .pun,
-.vt .pln,
-.vt .tag,
-.vt .atn,
-.vt .atv,
-.vt .dec {
-  color: red;
-}
-
-.wdd {
-  background: #FAA;
-}
-.wdi {
-  background: #9F9;
-}
-
-/* Use special rules for special styles contained within a whitespace
- * error.  For these we want the whitespace error to take precedence
- * so we have to override the contained style.
- */
-.wse .vt, .wdd .vt,
-.wse .vt .pun, .wdd .vt .pun,
-.wse .vt .str, .wdd .vt .str,
-.wse .vt .kwd, .wdd .vt .kwd,
-.wse .vt .com, .wdd .vt .com,
-.wse .vt .typ, .wdd .vt .typ,
-.wse .vt .lit, .wdd .vt .lit,
-.wse .vt .pun, .wdd .vt .pun,
-.wse .vt .pln, .wdd .vt .pln,
-.wse .vt .tag, .wdd .vt .tag,
-.wse .vt .atn, .wdd .vt .atn,
-.wse .vt .atv, .wdd .vt .atv,
-.wse .vt .dec, .wdd .vt .dec {
-  color: black;
-}
-.wse .wdd {
-  background: red;
-}
-.wse .wdi {
-  background: red;
-}
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-apollo.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-apollo.js
deleted file mode 100644
index 99e4a97..0000000
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-apollo.js
+++ /dev/null
@@ -1,2 +0,0 @@
-PR.registerLangHandler(PR.createSimpleLexer([["com",/^#[^\n\r]*/,null,"#"],["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"],["str",/^"(?:[^"\\]|\\[\S\s])*(?:"|$)/,null,'"']],[["kwd",/^(?:ADS|AD|AUG|BZF|BZMF|CAE|CAF|CA|CCS|COM|CS|DAS|DCA|DCOM|DCS|DDOUBL|DIM|DOUBLE|DTCB|DTCF|DV|DXCH|EDRUPT|EXTEND|INCR|INDEX|NDX|INHINT|LXCH|MASK|MSK|MP|MSU|NOOP|OVSK|QXCH|RAND|READ|RELINT|RESUME|RETURN|ROR|RXOR|SQUARE|SU|TCR|TCAA|OVSK|TCF|TC|TS|WAND|WOR|WRITE|XCH|XLQ|XXALQ|ZL|ZQ|ADD|ADZ|SUB|SUZ|MPY|MPR|MPZ|DVP|COM|ABS|CLA|CLZ|LDQ|STO|STQ|ALS|LLS|LRS|TRA|TSQ|TMI|TOV|AXT|TIX|DLY|INP|OUT)\s/,
-null],["typ",/^(?:-?GENADR|=MINUS|2BCADR|VN|BOF|MM|-?2CADR|-?[1-6]DNADR|ADRES|BBCON|[ES]?BANK=?|BLOCK|BNKSUM|E?CADR|COUNT\*?|2?DEC\*?|-?DNCHAN|-?DNPTR|EQUALS|ERASE|MEMORY|2?OCT|REMADR|SETLOC|SUBRO|ORG|BSS|BES|SYN|EQU|DEFINE|END)\s/,null],["lit",/^'(?:-*(?:\w|\\[!-~])(?:[\w-]*|\\[!-~])[!=?]?)?/],["pln",/^-*(?:[!-z]|\\[!-~])(?:[\w-]*|\\[!-~])[!=?]?/],["pun",/^[^\w\t\n\r "'-);\\\xa0]+/]]),["apollo","agc","aea"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-basic.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-basic.js
deleted file mode 100644
index 6b784d4..0000000
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-basic.js
+++ /dev/null
@@ -1,3 +0,0 @@
-var a=null;
-PR.registerLangHandler(PR.createSimpleLexer([["str",/^"(?:[^\n\r"\\]|\\.)*(?:"|$)/,a,'"'],["pln",/^\s+/,a," \r\n\t\u00a0"]],[["com",/^REM[^\n\r]*/,a],["kwd",/^\b(?:AND|CLOSE|CLR|CMD|CONT|DATA|DEF ?FN|DIM|END|FOR|GET|GOSUB|GOTO|IF|INPUT|LET|LIST|LOAD|NEW|NEXT|NOT|ON|OPEN|OR|POKE|PRINT|READ|RESTORE|RETURN|RUN|SAVE|STEP|STOP|SYS|THEN|TO|VERIFY|WAIT)\b/,a],["pln",/^[a-z][^\W_]?(?:\$|%)?/i,a],["lit",/^(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?/i,a,"0123456789"],["pun",
-/^.[^\s\w"$%.]*/,a]]),["basic","cbm"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-clj.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-clj.js
deleted file mode 100644
index 1bb539c..0000000
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-clj.js
+++ /dev/null
@@ -1,18 +0,0 @@
-/*
- Copyright (C) 2011 Google Inc.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-var a=null;
-PR.registerLangHandler(PR.createSimpleLexer([["opn",/^[([{]+/,a,"([{"],["clo",/^[)\]}]+/,a,")]}"],["com",/^;[^\n\r]*/,a,";"],["pln",/^[\t\n\r \xa0]+/,a,"\t\n\r \u00a0"],["str",/^"(?:[^"\\]|\\[\S\s])*(?:"|$)/,a,'"']],[["kwd",/^(?:def|if|do|let|quote|var|fn|loop|recur|throw|try|monitor-enter|monitor-exit|defmacro|defn|defn-|macroexpand|macroexpand-1|for|doseq|dosync|dotimes|and|or|when|not|assert|doto|proxy|defstruct|first|rest|cons|defprotocol|deftype|defrecord|reify|defmulti|defmethod|meta|with-meta|ns|in-ns|create-ns|import|intern|refer|alias|namespace|resolve|ref|deref|refset|new|set!|memfn|to-array|into-array|aset|gen-class|reduce|map|filter|find|nil?|empty?|hash-map|hash-set|vec|vector|seq|flatten|reverse|assoc|dissoc|list|list?|disj|get|union|difference|intersection|extend|extend-type|extend-protocol|prn)\b/,a],
-["typ",/^:[\dA-Za-z-]+/]]),["clj"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-css.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-css.js
deleted file mode 100644
index d7a4640..0000000
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-css.js
+++ /dev/null
@@ -1,2 +0,0 @@
-PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n\u000c"]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]+)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],
-["com",/^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}\b/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-dart.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-dart.js
deleted file mode 100644
index eefccc9..0000000
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-dart.js
+++ /dev/null
@@ -1,3 +0,0 @@
-PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"]],[["com",/^#!.*/],["kwd",/^\b(?:import|library|part of|part|as|show|hide)\b/i],["com",/^\/\/.*/],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["kwd",/^\b(?:class|interface)\b/i],["kwd",/^\b(?:assert|break|case|catch|continue|default|do|else|finally|for|if|in|is|new|return|super|switch|this|throw|try|while)\b/i],["kwd",/^\b(?:abstract|const|extends|factory|final|get|implements|native|operator|set|static|typedef|var)\b/i],
-["typ",/^\b(?:bool|double|dynamic|int|num|object|string|void)\b/i],["kwd",/^\b(?:false|null|true)\b/i],["str",/^r?'''[\S\s]*?[^\\]'''/],["str",/^r?"""[\S\s]*?[^\\]"""/],["str",/^r?'('|[^\n\f\r]*?[^\\]')/],["str",/^r?"("|[^\n\f\r]*?[^\\]")/],["pln",/^[$_a-z]\w*/i],["pun",/^[!%&*+/:<-?^|~-]/],["lit",/^\b0x[\da-f]+/i],["lit",/^\b\d+(?:\.\d*)?(?:e[+-]?\d+)?/i],["lit",/^\b\.\d+(?:e[+-]?\d+)?/i],["pun",/^[(),.;[\]{}]/]]),
-["dart"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-erlang.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-erlang.js
deleted file mode 100644
index 27214a5..0000000
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-erlang.js
+++ /dev/null
@@ -1,2 +0,0 @@
-PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t-\r ]+/,null,"\t\n\u000b\u000c\r "],["str",/^"(?:[^\n\f\r"\\]|\\[\S\s])*(?:"|$)/,null,'"'],["lit",/^[a-z]\w*/],["lit",/^'(?:[^\n\f\r'\\]|\\[^&])+'?/,null,"'"],["lit",/^\?[^\t\n ({]+/,null,"?"],["lit",/^(?:0o[0-7]+|0x[\da-f]+|\d+(?:\.\d+)?(?:e[+-]?\d+)?)/i,null,"0123456789"]],[["com",/^%[^\n]*/],["kwd",/^(?:module|attributes|do|let|in|letrec|apply|call|primop|case|of|end|when|fun|try|catch|receive|after|char|integer|float,atom,string,var)\b/],
-["kwd",/^-[_a-z]+/],["typ",/^[A-Z_]\w*/],["pun",/^[,.;]/]]),["erlang","erl"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-go.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-go.js
deleted file mode 100644
index 1caca23..0000000
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-go.js
+++ /dev/null
@@ -1 +0,0 @@
-PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"],["pln",/^(?:"(?:[^"\\]|\\[\S\s])*(?:"|$)|'(?:[^'\\]|\\[\S\s])+(?:'|$)|`[^`]*(?:`|$))/,null,"\"'"]],[["com",/^(?:\/\/[^\n\r]*|\/\*[\S\s]*?\*\/)/],["pln",/^(?:[^"'/`]|\/(?![*/]))+/]]),["go"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-hs.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-hs.js
deleted file mode 100644
index ff3729b..0000000
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-hs.js
+++ /dev/null
@@ -1,2 +0,0 @@
-PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t-\r ]+/,null,"\t\n\u000b\u000c\r "],["str",/^"(?:[^\n\f\r"\\]|\\[\S\s])*(?:"|$)/,null,'"'],["str",/^'(?:[^\n\f\r'\\]|\\[^&])'?/,null,"'"],["lit",/^(?:0o[0-7]+|0x[\da-f]+|\d+(?:\.\d+)?(?:e[+-]?\d+)?)/i,null,"0123456789"]],[["com",/^(?:--+[^\n\f\r]*|{-(?:[^-]|-+[^}-])*-})/],["kwd",/^(?:case|class|data|default|deriving|do|else|if|import|in|infix|infixl|infixr|instance|let|module|newtype|of|then|type|where|_)(?=[^\d'A-Za-z]|$)/,
-null],["pln",/^(?:[A-Z][\w']*\.)*[A-Za-z][\w']*/],["pun",/^[^\d\t-\r "'A-Za-z]+/]]),["hs"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-lisp.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-lisp.js
deleted file mode 100644
index 9c8cfa5..0000000
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-lisp.js
+++ /dev/null
@@ -1,3 +0,0 @@
-var a=null;
-PR.registerLangHandler(PR.createSimpleLexer([["opn",/^\(+/,a,"("],["clo",/^\)+/,a,")"],["com",/^;[^\n\r]*/,a,";"],["pln",/^[\t\n\r \xa0]+/,a,"\t\n\r \u00a0"],["str",/^"(?:[^"\\]|\\[\S\s])*(?:"|$)/,a,'"']],[["kwd",/^(?:block|c[ad]+r|catch|con[ds]|def(?:ine|un)|do|eq|eql|equal|equalp|eval-when|flet|format|go|if|labels|lambda|let|load-time-value|locally|macrolet|multiple-value-call|nil|progn|progv|quote|require|return-from|setq|symbol-macrolet|t|tagbody|the|throw|unwind)\b/,a],
-["lit",/^[+-]?(?:[#0]x[\da-f]+|\d+\/\d+|(?:\.\d+|\d+(?:\.\d*)?)(?:[de][+-]?\d+)?)/i],["lit",/^'(?:-*(?:\w|\\[!-~])(?:[\w-]*|\\[!-~])[!=?]?)?/],["pln",/^-*(?:[_a-z]|\\[!-~])(?:[\w-]*|\\[!-~])[!=?]?/i],["pun",/^[^\w\t\n\r "'-);\\\xa0]+/]]),["cl","el","lisp","lsp","scm","ss","rkt"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-llvm.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-llvm.js
deleted file mode 100644
index 16fade2..0000000
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-llvm.js
+++ /dev/null
@@ -1 +0,0 @@
-PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"],["str",/^!?"(?:[^"\\]|\\[\S\s])*(?:"|$)/,null,'"'],["com",/^;[^\n\r]*/,null,";"]],[["pln",/^[!%@](?:[$\-.A-Z_a-z][\w$\-.]*|\d+)/],["kwd",/^[^\W\d]\w*/,null],["lit",/^\d+\.\d+/],["lit",/^(?:\d+|0[Xx][\dA-Fa-f]+)/],["pun",/^[(-*,:<->[\]{}]|\.\.\.$/]]),["llvm","ll"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-lua.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-lua.js
deleted file mode 100644
index 7e44cca..0000000
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-lua.js
+++ /dev/null
@@ -1,2 +0,0 @@
-PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"],["str",/^(?:"(?:[^"\\]|\\[\S\s])*(?:"|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$))/,null,"\"'"]],[["com",/^--(?:\[(=*)\[[\S\s]*?(?:]\1]|$)|[^\n\r]*)/],["str",/^\[(=*)\[[\S\s]*?(?:]\1]|$)/],["kwd",/^(?:and|break|do|else|elseif|end|false|for|function|if|in|local|nil|not|or|repeat|return|then|true|until|while)\b/,null],["lit",/^[+-]?(?:0x[\da-f]+|(?:\.\d+|\d+(?:\.\d*)?)(?:e[+-]?\d+)?)/i],
-["pln",/^[_a-z]\w*/i],["pun",/^[^\w\t\n\r \xa0][^\w\t\n\r "'+=\xa0-]*/]]),["lua"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-matlab.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-matlab.js
deleted file mode 100644
index d0d3516..0000000
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-matlab.js
+++ /dev/null
@@ -1,6 +0,0 @@
-var a=null,b=window.PR,c=[[b.PR_PLAIN,/^[\t-\r \xa0]+/,a," \t\r\n\u000b\u000c\u00a0"],[b.PR_COMMENT,/^%{[^%]*%+(?:[^%}][^%]*%+)*}/,a],[b.PR_COMMENT,/^%[^\n\r]*/,a,"%"],["syscmd",/^![^\n\r]*/,a,"!"]],d=[["linecont",/^\.\.\.\s*[\n\r]/,a],["err",/^\?\?\? [^\n\r]*/,a],["wrn",/^Warning: [^\n\r]*/,a],["codeoutput",/^>>\s+/,a],["codeoutput",/^octave:\d+>\s+/,a],["lang-matlab-operators",/^((?:[A-Za-z]\w*(?:\.[A-Za-z]\w*)*|[).\]}])')/,a],["lang-matlab-identifiers",/^([A-Za-z]\w*(?:\.[A-Za-z]\w*)*)(?!')/,a],
-[b.PR_STRING,/^'(?:[^']|'')*'/,a],[b.PR_LITERAL,/^[+-]?\.?\d+(?:\.\d*)?(?:[Ee][+-]?\d+)?[ij]?/,a],[b.PR_TAG,/^[()[\]{}]/,a],[b.PR_PUNCTUATION,/^[!&*-/:->@\\^|~]/,a]],e=[["lang-matlab-identifiers",/^([A-Za-z]\w*(?:\.[A-Za-z]\w*)*)/,a],[b.PR_TAG,/^[()[\]{}]/,a],[b.PR_PUNCTUATION,/^[!&*-/:->@\\^|~]/,a],["transpose",/^'/,a]];
-b.registerLangHandler(b.createSimpleLexer([],[[b.PR_KEYWORD,/^\b(?:break|case|catch|classdef|continue|else|elseif|end|for|function|global|if|otherwise|parfor|persistent|return|spmd|switch|try|while)\b/,a],["const",/^\b(?:true|false|inf|Inf|nan|NaN|eps|pi|ans|nargin|nargout|varargin|varargout)\b/,a],[b.PR_TYPE,/^\b(?:cell|struct|char|double|single|logical|u?int(?:8|16|32|64)|sparse)\b/,a],["fun",/^\b(?:abs|accumarray|acos(?:d|h)?|acot(?:d|h)?|acsc(?:d|h)?|actxcontrol(?:list|select)?|actxGetRunningServer|actxserver|addlistener|addpath|addpref|addtodate|airy|align|alim|all|allchild|alpha|alphamap|amd|ancestor|and|angle|annotation|any|area|arrayfun|asec(?:d|h)?|asin(?:d|h)?|assert|assignin|atan[2dh]?|audiodevinfo|audioplayer|audiorecorder|aufinfo|auread|autumn|auwrite|avifile|aviinfo|aviread|axes|axis|balance|bar(?:3|3h|h)?|base2dec|beep|BeginInvoke|bench|bessel[h-ky]|beta|betainc|betaincinv|betaln|bicg|bicgstab|bicgstabl|bin2dec|bitand|bitcmp|bitget|bitmax|bitnot|bitor|bitset|bitshift|bitxor|blanks|blkdiag|bone|box|brighten|brush|bsxfun|builddocsearchdb|builtin|bvp4c|bvp5c|bvpget|bvpinit|bvpset|bvpxtend|calendar|calllib|callSoapService|camdolly|cameratoolbar|camlight|camlookat|camorbit|campan|campos|camproj|camroll|camtarget|camup|camva|camzoom|cart2pol|cart2sph|cast|cat|caxis|cd|cdf2rdf|cdfepoch|cdfinfo|cdflib(?:.(?:close|closeVar|computeEpoch|computeEpoch16|create|createAttr|createVar|delete|deleteAttr|deleteAttrEntry|deleteAttrgEntry|deleteVar|deleteVarRecords|epoch16Breakdown|epochBreakdown|getAttrEntry|getAttrgEntry|getAttrMaxEntry|getAttrMaxgEntry|getAttrName|getAttrNum|getAttrScope|getCacheSize|getChecksum|getCompression|getCompressionCacheSize|getConstantNames|getConstantValue|getCopyright|getFileBackward|getFormat|getLibraryCopyright|getLibraryVersion|getMajority|getName|getNumAttrEntries|getNumAttrgEntries|getNumAttributes|getNumgAttributes|getReadOnlyMode|getStageCacheSize|getValidate|getVarAllocRecords|getVarBlockingFactor|getVarCacheSize|getVarCompression|getVarData|getVarMaxAllocRecNum|getVarMaxWrittenRecNum|getVarName|getVarNum|getVarNumRecsWritten|getVarPadValue|getVarRecordData|getVarReservePercent|getVarsMaxWrittenRecNum|getVarSparseRecords|getVersion|hyperGetVarData|hyperPutVarData|inquire|inquireAttr|inquireAttrEntry|inquireAttrgEntry|inquireVar|open|putAttrEntry|putAttrgEntry|putVarData|putVarRecordData|renameAttr|renameVar|setCacheSize|setChecksum|setCompression|setCompressionCacheSize|setFileBackward|setFormat|setMajority|setReadOnlyMode|setStageCacheSize|setValidate|setVarAllocBlockRecords|setVarBlockingFactor|setVarCacheSize|setVarCompression|setVarInitialRecs|setVarPadValue|SetVarReservePercent|setVarsCacheSize|setVarSparseRecords))?|cdfread|cdfwrite|ceil|cell2mat|cell2struct|celldisp|cellfun|cellplot|cellstr|cgs|checkcode|checkin|checkout|chol|cholinc|cholupdate|circshift|cla|clabel|class|clc|clear|clearvars|clf|clipboard|clock|close|closereq|cmopts|cmpermute|cmunique|colamd|colon|colorbar|colordef|colormap|colormapeditor|colperm|Combine|comet|comet3|commandhistory|commandwindow|compan|compass|complex|computer|cond|condeig|condest|coneplot|conj|containers.Map|contour(?:[3cf]|slice)?|contrast|conv|conv2|convhull|convhulln|convn|cool|copper|copyfile|copyobj|corrcoef|cos(?:d|h)?|cot(?:d|h)?|cov|cplxpair|cputime|createClassFromWsdl|createSoapMessage|cross|csc(?:d|h)?|csvread|csvwrite|ctranspose|cumprod|cumsum|cumtrapz|curl|customverctrl|cylinder|daqread|daspect|datacursormode|datatipinfo|date|datenum|datestr|datetick|datevec|dbclear|dbcont|dbdown|dblquad|dbmex|dbquit|dbstack|dbstatus|dbstep|dbstop|dbtype|dbup|dde23|ddeget|ddesd|ddeset|deal|deblank|dec2base|dec2bin|dec2hex|decic|deconv|del2|delaunay|delaunay3|delaunayn|DelaunayTri|delete|demo|depdir|depfun|det|detrend|deval|diag|dialog|diary|diff|diffuse|dir|disp|display|dither|divergence|dlmread|dlmwrite|dmperm|doc|docsearch|dos|dot|dragrect|drawnow|dsearch|dsearchn|dynamicprops|echo|echodemo|edit|eig|eigs|ellipj|ellipke|ellipsoid|empty|enableNETfromNetworkDrive|enableservice|EndInvoke|enumeration|eomday|eq|erf|erfc|erfcinv|erfcx|erfinv|error|errorbar|errordlg|etime|etree|etreeplot|eval|evalc|evalin|event.(?:EventData|listener|PropertyEvent|proplistener)|exifread|exist|exit|exp|expint|expm|expm1|export2wsdlg|eye|ezcontour|ezcontourf|ezmesh|ezmeshc|ezplot|ezplot3|ezpolar|ezsurf|ezsurfc|factor|factorial|fclose|feather|feature|feof|ferror|feval|fft|fft2|fftn|fftshift|fftw|fgetl|fgets|fieldnames|figure|figurepalette|fileattrib|filebrowser|filemarker|fileparts|fileread|filesep|fill|fill3|filter|filter2|find|findall|findfigs|findobj|findstr|finish|fitsdisp|fitsinfo|fitsread|fitswrite|fix|flag|flipdim|fliplr|flipud|floor|flow|fminbnd|fminsearch|fopen|format|fplot|fprintf|frame2im|fread|freqspace|frewind|fscanf|fseek|ftell|FTP|full|fullfile|func2str|functions|funm|fwrite|fzero|gallery|gamma|gammainc|gammaincinv|gammaln|gca|gcbf|gcbo|gcd|gcf|gco|ge|genpath|genvarname|get|getappdata|getenv|getfield|getframe|getpixelposition|getpref|ginput|gmres|gplot|grabcode|gradient|gray|graymon|grid|griddata(?:3|n)?|griddedInterpolant|gsvd|gt|gtext|guidata|guide|guihandles|gunzip|gzip|h5create|h5disp|h5info|h5read|h5readatt|h5write|h5writeatt|hadamard|handle|hankel|hdf|hdf5|hdf5info|hdf5read|hdf5write|hdfinfo|hdfread|hdftool|help|helpbrowser|helpdesk|helpdlg|helpwin|hess|hex2dec|hex2num|hgexport|hggroup|hgload|hgsave|hgsetget|hgtransform|hidden|hilb|hist|histc|hold|home|horzcat|hostid|hot|hsv|hsv2rgb|hypot|ichol|idivide|ifft|ifft2|ifftn|ifftshift|ilu|im2frame|im2java|imag|image|imagesc|imapprox|imfinfo|imformats|import|importdata|imread|imwrite|ind2rgb|ind2sub|inferiorto|info|inline|inmem|inpolygon|input|inputdlg|inputname|inputParser|inspect|instrcallback|instrfind|instrfindall|int2str|integral(?:2|3)?|interp(?:1|1q|2|3|ft|n)|interpstreamspeed|intersect|intmax|intmin|inv|invhilb|ipermute|isa|isappdata|iscell|iscellstr|ischar|iscolumn|isdir|isempty|isequal|isequaln|isequalwithequalnans|isfield|isfinite|isfloat|isglobal|ishandle|ishghandle|ishold|isinf|isinteger|isjava|iskeyword|isletter|islogical|ismac|ismatrix|ismember|ismethod|isnan|isnumeric|isobject|isocaps|isocolors|isonormals|isosurface|ispc|ispref|isprime|isprop|isreal|isrow|isscalar|issorted|isspace|issparse|isstr|isstrprop|isstruct|isstudent|isunix|isvarname|isvector|javaaddpath|javaArray|javachk|javaclasspath|javacomponent|javaMethod|javaMethodEDT|javaObject|javaObjectEDT|javarmpath|jet|keyboard|kron|lasterr|lasterror|lastwarn|lcm|ldivide|ldl|le|legend|legendre|length|libfunctions|libfunctionsview|libisloaded|libpointer|libstruct|license|light|lightangle|lighting|lin2mu|line|lines|linkaxes|linkdata|linkprop|linsolve|linspace|listdlg|listfonts|load|loadlibrary|loadobj|log|log10|log1p|log2|loglog|logm|logspace|lookfor|lower|ls|lscov|lsqnonneg|lsqr|lt|lu|luinc|magic|makehgtform|mat2cell|mat2str|material|matfile|matlab.io.MatFile|matlab.mixin.(?:Copyable|Heterogeneous(?:.getDefaultScalarElement)?)|matlabrc|matlabroot|max|maxNumCompThreads|mean|median|membrane|memmapfile|memory|menu|mesh|meshc|meshgrid|meshz|meta.(?:class(?:.fromName)?|DynamicProperty|EnumeratedValue|event|MetaData|method|package(?:.(?:fromName|getAllPackages))?|property)|metaclass|methods|methodsview|mex(?:.getCompilerConfigurations)?|MException|mexext|mfilename|min|minres|minus|mislocked|mkdir|mkpp|mldivide|mlint|mlintrpt|mlock|mmfileinfo|mmreader|mod|mode|more|move|movefile|movegui|movie|movie2avi|mpower|mrdivide|msgbox|mtimes|mu2lin|multibandread|multibandwrite|munlock|namelengthmax|nargchk|narginchk|nargoutchk|native2unicode|nccreate|ncdisp|nchoosek|ncinfo|ncread|ncreadatt|ncwrite|ncwriteatt|ncwriteschema|ndgrid|ndims|ne|NET(?:.(?:addAssembly|Assembly|convertArray|createArray|createGeneric|disableAutoRelease|enableAutoRelease|GenericClass|invokeGenericMethod|NetException|setStaticProperty))?|netcdf.(?:abort|close|copyAtt|create|defDim|defGrp|defVar|defVarChunking|defVarDeflate|defVarFill|defVarFletcher32|delAtt|endDef|getAtt|getChunkCache|getConstant|getConstantNames|getVar|inq|inqAtt|inqAttID|inqAttName|inqDim|inqDimID|inqDimIDs|inqFormat|inqGrpName|inqGrpNameFull|inqGrpParent|inqGrps|inqLibVers|inqNcid|inqUnlimDims|inqVar|inqVarChunking|inqVarDeflate|inqVarFill|inqVarFletcher32|inqVarID|inqVarIDs|open|putAtt|putVar|reDef|renameAtt|renameDim|renameVar|setChunkCache|setDefaultFormat|setFill|sync)|newplot|nextpow2|nnz|noanimate|nonzeros|norm|normest|not|notebook|now|nthroot|null|num2cell|num2hex|num2str|numel|nzmax|ode(?:113|15i|15s|23|23s|23t|23tb|45)|odeget|odeset|odextend|onCleanup|ones|open|openfig|opengl|openvar|optimget|optimset|or|ordeig|orderfields|ordqz|ordschur|orient|orth|pack|padecoef|pagesetupdlg|pan|pareto|parseSoapResponse|pascal|patch|path|path2rc|pathsep|pathtool|pause|pbaspect|pcg|pchip|pcode|pcolor|pdepe|pdeval|peaks|perl|perms|permute|pie|pink|pinv|planerot|playshow|plot|plot3|plotbrowser|plotedit|plotmatrix|plottools|plotyy|plus|pol2cart|polar|poly|polyarea|polyder|polyeig|polyfit|polyint|polyval|polyvalm|pow2|power|ppval|prefdir|preferences|primes|print|printdlg|printopt|printpreview|prod|profile|profsave|propedit|propertyeditor|psi|publish|PutCharArray|PutFullMatrix|PutWorkspaceData|pwd|qhull|qmr|qr|qrdelete|qrinsert|qrupdate|quad|quad2d|quadgk|quadl|quadv|questdlg|quit|quiver|quiver3|qz|rand|randi|randn|randperm|RandStream(?:.(?:create|getDefaultStream|getGlobalStream|list|setDefaultStream|setGlobalStream))?|rank|rat|rats|rbbox|rcond|rdivide|readasync|real|reallog|realmax|realmin|realpow|realsqrt|record|rectangle|rectint|recycle|reducepatch|reducevolume|refresh|refreshdata|regexp|regexpi|regexprep|regexptranslate|rehash|rem|Remove|RemoveAll|repmat|reset|reshape|residue|restoredefaultpath|rethrow|rgb2hsv|rgb2ind|rgbplot|ribbon|rmappdata|rmdir|rmfield|rmpath|rmpref|rng|roots|rose|rosser|rot90|rotate|rotate3d|round|rref|rsf2csf|run|save|saveas|saveobj|savepath|scatter|scatter3|schur|sec|secd|sech|selectmoveresize|semilogx|semilogy|sendmail|serial|set|setappdata|setdiff|setenv|setfield|setpixelposition|setpref|setstr|setxor|shading|shg|shiftdim|showplottool|shrinkfaces|sign|sin(?:d|h)?|size|slice|smooth3|snapnow|sort|sortrows|sound|soundsc|spalloc|spaugment|spconvert|spdiags|specular|speye|spfun|sph2cart|sphere|spinmap|spline|spones|spparms|sprand|sprandn|sprandsym|sprank|spring|sprintf|spy|sqrt|sqrtm|squeeze|ss2tf|sscanf|stairs|startup|std|stem|stem3|stopasync|str2double|str2func|str2mat|str2num|strcat|strcmp|strcmpi|stream2|stream3|streamline|streamparticles|streamribbon|streamslice|streamtube|strfind|strjust|strmatch|strncmp|strncmpi|strread|strrep|strtok|strtrim|struct2cell|structfun|strvcat|sub2ind|subplot|subsasgn|subsindex|subspace|subsref|substruct|subvolume|sum|summer|superclasses|superiorto|support|surf|surf2patch|surface|surfc|surfl|surfnorm|svd|svds|swapbytes|symamd|symbfact|symmlq|symrcm|symvar|system|tan(?:d|h)?|tar|tempdir|tempname|tetramesh|texlabel|text|textread|textscan|textwrap|tfqmr|throw|tic|Tiff(?:.(?:getTagNames|getVersion))?|timer|timerfind|timerfindall|times|timeseries|title|toc|todatenum|toeplitz|toolboxdir|trace|transpose|trapz|treelayout|treeplot|tril|trimesh|triplequad|triplot|TriRep|TriScatteredInterp|trisurf|triu|tscollection|tsearch|tsearchn|tstool|type|typecast|uibuttongroup|uicontextmenu|uicontrol|uigetdir|uigetfile|uigetpref|uiimport|uimenu|uiopen|uipanel|uipushtool|uiputfile|uiresume|uisave|uisetcolor|uisetfont|uisetpref|uistack|uitable|uitoggletool|uitoolbar|uiwait|uminus|undocheckout|unicode2native|union|unique|unix|unloadlibrary|unmesh|unmkpp|untar|unwrap|unzip|uplus|upper|urlread|urlwrite|usejava|userpath|validateattributes|validatestring|vander|var|vectorize|ver|verctrl|verLessThan|version|vertcat|VideoReader(?:.isPlatformSupported)?|VideoWriter(?:.getProfiles)?|view|viewmtx|visdiff|volumebounds|voronoi|voronoin|wait|waitbar|waitfor|waitforbuttonpress|warndlg|warning|waterfall|wavfinfo|wavplay|wavread|wavrecord|wavwrite|web|weekday|what|whatsnew|which|whitebg|who|whos|wilkinson|winopen|winqueryreg|winter|wk1finfo|wk1read|wk1write|workspace|xlabel|xlim|xlsfinfo|xlsread|xlswrite|xmlread|xmlwrite|xor|xslt|ylabel|ylim|zeros|zip|zlabel|zlim|zoom)\b/,
-a],["fun_tbx",/^\b(?:addedvarplot|andrewsplot|anova[12n]|ansaribradley|aoctool|barttest|bbdesign|beta(?:cdf|fit|inv|like|pdf|rnd|stat)|bino(?:cdf|fit|inv|pdf|rnd|stat)|biplot|bootci|bootstrp|boxplot|candexch|candgen|canoncorr|capability|capaplot|caseread|casewrite|categorical|ccdesign|cdfplot|chi2(?:cdf|gof|inv|pdf|rnd|stat)|cholcov|Classification(?:BaggedEnsemble|Discriminant(?:.(?:fit|make|template))?|Ensemble|KNN(?:.(?:fit|template))?|PartitionedEnsemble|PartitionedModel|Tree(?:.(?:fit|template))?)|classify|classregtree|cluster|clusterdata|cmdscale|combnk|Compact(?:Classification(?:Discriminant|Ensemble|Tree)|Regression(?:Ensemble|Tree)|TreeBagger)|confusionmat|controlchart|controlrules|cophenet|copula(?:cdf|fit|param|pdf|rnd|stat)|cordexch|corr|corrcov|coxphfit|createns|crosstab|crossval|cvpartition|datasample|dataset|daugment|dcovary|dendrogram|dfittool|disttool|dummyvar|dwtest|ecdf|ecdfhist|ev(?:cdf|fit|inv|like|pdf|rnd|stat)|ExhaustiveSearcher|exp(?:cdf|fit|inv|like|pdf|rnd|stat)|factoran|fcdf|ff2n|finv|fitdist|fitensemble|fpdf|fracfact|fracfactgen|friedman|frnd|fstat|fsurfht|fullfact|gagerr|gam(?:cdf|fit|inv|like|pdf|rnd|stat)|GeneralizedLinearModel(?:.fit)?|geo(?:cdf|inv|mean|pdf|rnd|stat)|gev(?:cdf|fit|inv|like|pdf|rnd|stat)|gline|glmfit|glmval|glyphplot|gmdistribution(?:.fit)?|gname|gp(?:cdf|fit|inv|like|pdf|rnd|stat)|gplotmatrix|grp2idx|grpstats|gscatter|haltonset|harmmean|hist3|histfit|hmm(?:decode|estimate|generate|train|viterbi)|hougen|hyge(?:cdf|inv|pdf|rnd|stat)|icdf|inconsistent|interactionplot|invpred|iqr|iwishrnd|jackknife|jbtest|johnsrnd|KDTreeSearcher|kmeans|knnsearch|kruskalwallis|ksdensity|kstest|kstest2|kurtosis|lasso|lassoglm|lassoPlot|leverage|lhsdesign|lhsnorm|lillietest|LinearModel(?:.fit)?|linhyptest|linkage|logn(?:cdf|fit|inv|like|pdf|rnd|stat)|lsline|mad|mahal|maineffectsplot|manova1|manovacluster|mdscale|mhsample|mle|mlecov|mnpdf|mnrfit|mnrnd|mnrval|moment|multcompare|multivarichart|mvn(?:cdf|pdf|rnd)|mvregress|mvregresslike|mvt(?:cdf|pdf|rnd)|NaiveBayes(?:.fit)?|nan(?:cov|max|mean|median|min|std|sum|var)|nbin(?:cdf|fit|inv|pdf|rnd|stat)|ncf(?:cdf|inv|pdf|rnd|stat)|nct(?:cdf|inv|pdf|rnd|stat)|ncx2(?:cdf|inv|pdf|rnd|stat)|NeighborSearcher|nlinfit|nlintool|nlmefit|nlmefitsa|nlparci|nlpredci|nnmf|nominal|NonLinearModel(?:.fit)?|norm(?:cdf|fit|inv|like|pdf|rnd|stat)|normplot|normspec|ordinal|outlierMeasure|parallelcoords|paretotails|partialcorr|pcacov|pcares|pdf|pdist|pdist2|pearsrnd|perfcurve|perms|piecewisedistribution|plsregress|poiss(?:cdf|fit|inv|pdf|rnd|tat)|polyconf|polytool|prctile|princomp|ProbDist(?:Kernel|Parametric|UnivKernel|UnivParam)?|probplot|procrustes|qqplot|qrandset|qrandstream|quantile|randg|random|randsample|randtool|range|rangesearch|ranksum|rayl(?:cdf|fit|inv|pdf|rnd|stat)|rcoplot|refcurve|refline|regress|Regression(?:BaggedEnsemble|Ensemble|PartitionedEnsemble|PartitionedModel|Tree(?:.(?:fit|template))?)|regstats|relieff|ridge|robustdemo|robustfit|rotatefactors|rowexch|rsmdemo|rstool|runstest|sampsizepwr|scatterhist|sequentialfs|signrank|signtest|silhouette|skewness|slicesample|sobolset|squareform|statget|statset|stepwise|stepwisefit|surfht|tabulate|tblread|tblwrite|tcdf|tdfread|tiedrank|tinv|tpdf|TreeBagger|treedisp|treefit|treeprune|treetest|treeval|trimmean|trnd|tstat|ttest|ttest2|unid(?:cdf|inv|pdf|rnd|stat)|unif(?:cdf|inv|it|pdf|rnd|stat)|vartest(?:2|n)?|wbl(?:cdf|fit|inv|like|pdf|rnd|stat)|wblplot|wishrnd|x2fx|xptread|zscore|ztest)\b/,
-a],["fun_tbx",/^\b(?:adapthisteq|analyze75info|analyze75read|applycform|applylut|axes2pix|bestblk|blockproc|bwarea|bwareaopen|bwboundaries|bwconncomp|bwconvhull|bwdist|bwdistgeodesic|bweuler|bwhitmiss|bwlabel|bwlabeln|bwmorph|bwpack|bwperim|bwselect|bwtraceboundary|bwulterode|bwunpack|checkerboard|col2im|colfilt|conndef|convmtx2|corner|cornermetric|corr2|cp2tform|cpcorr|cpselect|cpstruct2pairs|dct2|dctmtx|deconvblind|deconvlucy|deconvreg|deconvwnr|decorrstretch|demosaic|dicom(?:anon|dict|info|lookup|read|uid|write)|edge|edgetaper|entropy|entropyfilt|fan2para|fanbeam|findbounds|fliptform|freqz2|fsamp2|fspecial|ftrans2|fwind1|fwind2|getheight|getimage|getimagemodel|getline|getneighbors|getnhood|getpts|getrangefromclass|getrect|getsequence|gray2ind|graycomatrix|graycoprops|graydist|grayslice|graythresh|hdrread|hdrwrite|histeq|hough|houghlines|houghpeaks|iccfind|iccread|iccroot|iccwrite|idct2|ifanbeam|im2bw|im2col|im2double|im2int16|im2java2d|im2single|im2uint16|im2uint8|imabsdiff|imadd|imadjust|ImageAdapter|imageinfo|imagemodel|imapplymatrix|imattributes|imbothat|imclearborder|imclose|imcolormaptool|imcomplement|imcontour|imcontrast|imcrop|imdilate|imdisplayrange|imdistline|imdivide|imellipse|imerode|imextendedmax|imextendedmin|imfill|imfilter|imfindcircles|imfreehand|imfuse|imgca|imgcf|imgetfile|imhandles|imhist|imhmax|imhmin|imimposemin|imlincomb|imline|immagbox|immovie|immultiply|imnoise|imopen|imoverview|imoverviewpanel|impixel|impixelinfo|impixelinfoval|impixelregion|impixelregionpanel|implay|impoint|impoly|impositionrect|improfile|imputfile|impyramid|imreconstruct|imrect|imregconfig|imregionalmax|imregionalmin|imregister|imresize|imroi|imrotate|imsave|imscrollpanel|imshow|imshowpair|imsubtract|imtool|imtophat|imtransform|imview|ind2gray|ind2rgb|interfileinfo|interfileread|intlut|ippl|iptaddcallback|iptcheckconn|iptcheckhandle|iptcheckinput|iptcheckmap|iptchecknargin|iptcheckstrs|iptdemos|iptgetapi|iptGetPointerBehavior|iptgetpref|ipticondir|iptnum2ordinal|iptPointerManager|iptprefs|iptremovecallback|iptSetPointerBehavior|iptsetpref|iptwindowalign|iradon|isbw|isflat|isgray|isicc|isind|isnitf|isrgb|isrset|lab2double|lab2uint16|lab2uint8|label2rgb|labelmatrix|makecform|makeConstrainToRectFcn|makehdr|makelut|makeresampler|maketform|mat2gray|mean2|medfilt2|montage|nitfinfo|nitfread|nlfilter|normxcorr2|ntsc2rgb|openrset|ordfilt2|otf2psf|padarray|para2fan|phantom|poly2mask|psf2otf|qtdecomp|qtgetblk|qtsetblk|radon|rangefilt|reflect|regionprops|registration.metric.(?:MattesMutualInformation|MeanSquares)|registration.optimizer.(?:OnePlusOneEvolutionary|RegularStepGradientDescent)|rgb2gray|rgb2ntsc|rgb2ycbcr|roicolor|roifill|roifilt2|roipoly|rsetwrite|std2|stdfilt|strel|stretchlim|subimage|tformarray|tformfwd|tforminv|tonemap|translate|truesize|uintlut|viscircles|warp|watershed|whitepoint|wiener2|xyz2double|xyz2uint16|ycbcr2rgb)\b/,
-a],["fun_tbx",/^\b(?:bintprog|color|fgoalattain|fminbnd|fmincon|fminimax|fminsearch|fminunc|fseminf|fsolve|fzero|fzmult|gangstr|ktrlink|linprog|lsqcurvefit|lsqlin|lsqnonlin|lsqnonneg|optimget|optimset|optimtool|quadprog)\b/,a],["ident",/^[A-Za-z]\w*(?:\.[A-Za-z]\w*)*/,a]]),["matlab-identifiers"]);b.registerLangHandler(b.createSimpleLexer([],e),["matlab-operators"]);b.registerLangHandler(b.createSimpleLexer(c,d),["matlab"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-ml.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-ml.js
deleted file mode 100644
index 8ed2b0c..0000000
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-ml.js
+++ /dev/null
@@ -1,2 +0,0 @@
-PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"],["com",/^#(?:if[\t\n\r \xa0]+(?:[$_a-z][\w']*|``[^\t\n\r`]*(?:``|$))|else|endif|light)/i,null,"#"],["str",/^(?:"(?:[^"\\]|\\[\S\s])*(?:"|$)|'(?:[^'\\]|\\[\S\s])(?:'|$))/,null,"\"'"]],[["com",/^(?:\/\/[^\n\r]*|\(\*[\S\s]*?\*\))/],["kwd",/^(?:abstract|and|as|assert|begin|class|default|delegate|do|done|downcast|downto|elif|else|end|exception|extern|false|finally|for|fun|function|if|in|inherit|inline|interface|internal|lazy|let|match|member|module|mutable|namespace|new|null|of|open|or|override|private|public|rec|return|static|struct|then|to|true|try|type|upcast|use|val|void|when|while|with|yield|asr|land|lor|lsl|lsr|lxor|mod|sig|atomic|break|checked|component|const|constraint|constructor|continue|eager|event|external|fixed|functor|global|include|method|mixin|object|parallel|process|protected|pure|sealed|trait|virtual|volatile)\b/],
-["lit",/^[+-]?(?:0x[\da-f]+|(?:\.\d+|\d+(?:\.\d*)?)(?:e[+-]?\d+)?)/i],["pln",/^(?:[_a-z][\w']*[!#?]?|``[^\t\n\r`]*(?:``|$))/i],["pun",/^[^\w\t\n\r "'\xa0]+/]]),["fs","ml"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-mumps.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-mumps.js
deleted file mode 100644
index 8a6b3fd..0000000
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-mumps.js
+++ /dev/null
@@ -1,2 +0,0 @@
-PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"],["str",/^"(?:[^"]|\\.)*"/,null,'"']],[["com",/^;[^\n\r]*/,null,";"],["dec",/^\$(?:d|device|ec|ecode|es|estack|et|etrap|h|horolog|i|io|j|job|k|key|p|principal|q|quit|st|stack|s|storage|sy|system|t|test|tl|tlevel|tr|trestart|x|y|z[a-z]*|a|ascii|c|char|d|data|e|extract|f|find|fn|fnumber|g|get|j|justify|l|length|na|name|o|order|p|piece|ql|qlength|qs|qsubscript|q|query|r|random|re|reverse|s|select|st|stack|t|text|tr|translate|nan)\b/i,
-null],["kwd",/^(?:[^$]b|break|c|close|d|do|e|else|f|for|g|goto|h|halt|h|hang|i|if|j|job|k|kill|l|lock|m|merge|n|new|o|open|q|quit|r|read|s|set|tc|tcommit|tre|trestart|tro|trollback|ts|tstart|u|use|v|view|w|write|x|xecute)\b/i,null],["lit",/^[+-]?(?:\.\d+|\d+(?:\.\d*)?)(?:e[+-]?\d+)?/i],["pln",/^[a-z][^\W_]*/i],["pun",/^[^\w\t\n\r"$%;^\xa0]|_/]]),["mumps"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-n.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-n.js
deleted file mode 100644
index 27812a5..0000000
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-n.js
+++ /dev/null
@@ -1,4 +0,0 @@
-var a=null;
-PR.registerLangHandler(PR.createSimpleLexer([["str",/^(?:'(?:[^\n\r'\\]|\\.)*'|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,a,'"'],["com",/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/,a,"#"],["pln",/^\s+/,a," \r\n\t\u00a0"]],[["str",/^@"(?:[^"]|"")*(?:"|$)/,a],["str",/^<#[^#>]*(?:#>|$)/,a],["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,a],["com",/^\/\/[^\n\r]*/,a],["com",/^\/\*[\S\s]*?(?:\*\/|$)/,
-a],["kwd",/^(?:abstract|and|as|base|catch|class|def|delegate|enum|event|extern|false|finally|fun|implements|interface|internal|is|macro|match|matches|module|mutable|namespace|new|null|out|override|params|partial|private|protected|public|ref|sealed|static|struct|syntax|this|throw|true|try|type|typeof|using|variant|virtual|volatile|when|where|with|assert|assert2|async|break|checked|continue|do|else|ensures|for|foreach|if|late|lock|new|nolate|otherwise|regexp|repeat|requires|return|surroundwith|unchecked|unless|using|while|yield)\b/,
-a],["typ",/^(?:array|bool|byte|char|decimal|double|float|int|list|long|object|sbyte|short|string|ulong|uint|ufloat|ulong|ushort|void)\b/,a],["lit",/^@[$_a-z][\w$@]*/i,a],["typ",/^@[A-Z]+[a-z][\w$@]*/,a],["pln",/^'?[$_a-z][\w$@]*/i,a],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,a,"0123456789"],["pun",/^.[^\s\w"-$'./@`]*/,a]]),["n","nemerle"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-pascal.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-pascal.js
deleted file mode 100644
index 8435fad..0000000
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-pascal.js
+++ /dev/null
@@ -1,3 +0,0 @@
-var a=null;
-PR.registerLangHandler(PR.createSimpleLexer([["str",/^'(?:[^\n\r'\\]|\\.)*(?:'|$)/,a,"'"],["pln",/^\s+/,a," \r\n\t\u00a0"]],[["com",/^\(\*[\S\s]*?(?:\*\)|$)|^{[\S\s]*?(?:}|$)/,a],["kwd",/^(?:absolute|and|array|asm|assembler|begin|case|const|constructor|destructor|div|do|downto|else|end|external|for|forward|function|goto|if|implementation|in|inline|interface|interrupt|label|mod|not|object|of|or|packed|procedure|program|record|repeat|set|shl|shr|then|to|type|unit|until|uses|var|virtual|while|with|xor)\b/i,a],
-["lit",/^(?:true|false|self|nil)/i,a],["pln",/^[a-z][^\W_]*/i,a],["lit",/^(?:\$[\da-f]+|(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?)/i,a,"0123456789"],["pun",/^.[^\s\w$'./@]*/,a]]),["pascal"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-proto.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-proto.js
deleted file mode 100644
index f006ad8..0000000
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-proto.js
+++ /dev/null
@@ -1 +0,0 @@
-PR.registerLangHandler(PR.sourceDecorator({keywords:"bytes,default,double,enum,extend,extensions,false,group,import,max,message,option,optional,package,repeated,required,returns,rpc,service,syntax,to,true",types:/^(bool|(double|s?fixed|[su]?int)(32|64)|float|string)\b/,cStyleComments:!0}),["proto"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-r.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-r.js
deleted file mode 100644
index 99af8f8..0000000
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-r.js
+++ /dev/null
@@ -1,2 +0,0 @@
-PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"],["str",/^"(?:[^"\\]|\\[\S\s])*(?:"|$)/,null,'"'],["str",/^'(?:[^'\\]|\\[\S\s])*(?:'|$)/,null,"'"]],[["com",/^#.*/],["kwd",/^(?:if|else|for|while|repeat|in|next|break|return|switch|function)(?![\w.])/],["lit",/^0[Xx][\dA-Fa-f]+([Pp]\d+)?[Li]?/],["lit",/^[+-]?(\d+(\.\d+)?|\.\d+)([Ee][+-]?\d+)?[Li]?/],["lit",/^(?:NULL|NA(?:_(?:integer|real|complex|character)_)?|Inf|TRUE|FALSE|NaN|\.\.(?:\.|\d+))(?![\w.])/],
-["pun",/^(?:<<?-|->>?|-|==|<=|>=|<|>|&&?|!=|\|\|?|[!*+/^]|%.*?%|[$=@~]|:{1,3}|[(),;?[\]{}])/],["pln",/^(?:[A-Za-z]+[\w.]*|\.[^\W\d][\w.]*)(?![\w.])/],["str",/^`.+`/]]),["r","s","R","S","Splus"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-rd.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-rd.js
deleted file mode 100644
index 7a7e43f..0000000
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-rd.js
+++ /dev/null
@@ -1 +0,0 @@
-PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"],["com",/^%[^\n\r]*/,null,"%"]],[["lit",/^\\(?:cr|l?dots|R|tab)\b/],["kwd",/^\\[@-Za-z]+/],["kwd",/^#(?:ifn?def|endif)/],["pln",/^\\[{}]/],["pun",/^[()[\]{}]+/]]),["Rd","rd"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-scala.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-scala.js
deleted file mode 100644
index 3f97dba..0000000
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-scala.js
+++ /dev/null
@@ -1,2 +0,0 @@
-PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"],["str",/^"(?:""(?:""?(?!")|[^"\\]|\\.)*"{0,3}|(?:[^\n\r"\\]|\\.)*"?)/,null,'"'],["lit",/^`(?:[^\n\r\\`]|\\.)*`?/,null,"`"],["pun",/^[!#%&(--:-@[-^{-~]+/,null,"!#%&()*+,-:;<=>?@[\\]^{|}~"]],[["str",/^'(?:[^\n\r'\\]|\\(?:'|[^\n\r']+))'/],["lit",/^'[$A-Z_a-z][\w$]*(?![\w$'])/],["kwd",/^(?:abstract|case|catch|class|def|do|else|extends|final|finally|for|forSome|if|implicit|import|lazy|match|new|object|override|package|private|protected|requires|return|sealed|super|throw|trait|try|type|val|var|while|with|yield)\b/],
-["lit",/^(?:true|false|null|this)\b/],["lit",/^(?:0(?:[0-7]+|x[\da-f]+)l?|(?:0|[1-9]\d*)(?:(?:\.\d+)?(?:e[+-]?\d+)?f?|l?)|\\.\d+(?:e[+-]?\d+)?f?)/i],["typ",/^[$_]*[A-Z][\d$A-Z_]*[a-z][\w$]*/],["pln",/^[$A-Z_a-z][\w$]*/],["com",/^\/(?:\/.*|\*(?:\/|\**[^*/])*(?:\*+\/?)?)/],["pun",/^(?:\.+|\/)/]]),["scala"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-sql.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-sql.js
deleted file mode 100644
index 8ec4280..0000000
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-sql.js
+++ /dev/null
@@ -1,2 +0,0 @@
-PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"],["str",/^(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/,null,"\"'"]],[["com",/^(?:--[^\n\r]*|\/\*[\S\s]*?(?:\*\/|$))/],["kwd",/^(?:add|all|alter|and|any|apply|as|asc|authorization|backup|begin|between|break|browse|bulk|by|cascade|case|check|checkpoint|close|clustered|coalesce|collate|column|commit|compute|connect|constraint|contains|containstable|continue|convert|create|cross|current|current_date|current_time|current_timestamp|current_user|cursor|database|dbcc|deallocate|declare|default|delete|deny|desc|disk|distinct|distributed|double|drop|dummy|dump|else|end|errlvl|escape|except|exec|execute|exists|exit|fetch|file|fillfactor|following|for|foreign|freetext|freetexttable|from|full|function|goto|grant|group|having|holdlock|identity|identitycol|identity_insert|if|in|index|inner|insert|intersect|into|is|join|key|kill|left|like|lineno|load|match|matched|merge|natural|national|nocheck|nonclustered|nocycle|not|null|nullif|of|off|offsets|on|open|opendatasource|openquery|openrowset|openxml|option|or|order|outer|over|partition|percent|pivot|plan|preceding|precision|primary|print|proc|procedure|public|raiserror|read|readtext|reconfigure|references|replication|restore|restrict|return|revoke|right|rollback|rowcount|rowguidcol|rows?|rule|save|schema|select|session_user|set|setuser|shutdown|some|start|statistics|system_user|table|textsize|then|to|top|tran|transaction|trigger|truncate|tsequal|unbounded|union|unique|unpivot|update|updatetext|use|user|using|values|varying|view|waitfor|when|where|while|with|within|writetext|xml)(?=[^\w-]|$)/i,
-null],["lit",/^[+-]?(?:0x[\da-f]+|(?:\.\d+|\d+(?:\.\d*)?)(?:e[+-]?\d+)?)/i],["pln",/^[_a-z][\w-]*/i],["pun",/^[^\w\t\n\r "'\xa0][^\w\t\n\r "'+\xa0-]*/]]),["sql"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-tcl.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-tcl.js
deleted file mode 100644
index 490f562..0000000
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-tcl.js
+++ /dev/null
@@ -1,3 +0,0 @@
-var a=null;
-PR.registerLangHandler(PR.createSimpleLexer([["opn",/^{+/,a,"{"],["clo",/^}+/,a,"}"],["com",/^#[^\n\r]*/,a,"#"],["pln",/^[\t\n\r \xa0]+/,a,"\t\n\r \u00a0"],["str",/^"(?:[^"\\]|\\[\S\s])*(?:"|$)/,a,'"']],[["kwd",/^(?:after|append|apply|array|break|case|catch|continue|error|eval|exec|exit|expr|for|foreach|if|incr|info|proc|return|set|switch|trace|uplevel|upvar|while)\b/,a],["lit",/^[+-]?(?:[#0]x[\da-f]+|\d+\/\d+|(?:\.\d+|\d+(?:\.\d*)?)(?:[de][+-]?\d+)?)/i],["lit",
-/^'(?:-*(?:\w|\\[!-~])(?:[\w-]*|\\[!-~])[!=?]?)?/],["pln",/^-*(?:[_a-z]|\\[!-~])(?:[\w-]*|\\[!-~])[!=?]?/i],["pun",/^[^\w\t\n\r "'-);\\\xa0]+/]]),["tcl"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-tex.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-tex.js
deleted file mode 100644
index dcfdadd..0000000
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-tex.js
+++ /dev/null
@@ -1 +0,0 @@
-PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"],["com",/^%[^\n\r]*/,null,"%"]],[["kwd",/^\\[@-Za-z]+/],["kwd",/^\\./],["typ",/^[$&]/],["lit",/[+-]?(?:\.\d+|\d+(?:\.\d*)?)(cm|em|ex|in|pc|pt|bp|mm)/i],["pun",/^[()=[\]{}]+/]]),["latex","tex"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-vb.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-vb.js
deleted file mode 100644
index ddde464..0000000
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-vb.js
+++ /dev/null
@@ -1,2 +0,0 @@
-PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0\u2028\u2029]+/,null,"\t\n\r \u00a0\u2028\u2029"],["str",/^(?:["\u201c\u201d](?:[^"\u201c\u201d]|["\u201c\u201d]{2})(?:["\u201c\u201d]c|$)|["\u201c\u201d](?:[^"\u201c\u201d]|["\u201c\u201d]{2})*(?:["\u201c\u201d]|$))/i,null,'"\u201c\u201d'],["com",/^['\u2018\u2019](?:_(?:\r\n?|[^\r]?)|[^\n\r_\u2028\u2029])*/,null,"'\u2018\u2019"]],[["kwd",/^(?:addhandler|addressof|alias|and|andalso|ansi|as|assembly|auto|boolean|byref|byte|byval|call|case|catch|cbool|cbyte|cchar|cdate|cdbl|cdec|char|cint|class|clng|cobj|const|cshort|csng|cstr|ctype|date|decimal|declare|default|delegate|dim|directcast|do|double|each|else|elseif|end|endif|enum|erase|error|event|exit|finally|for|friend|function|get|gettype|gosub|goto|handles|if|implements|imports|in|inherits|integer|interface|is|let|lib|like|long|loop|me|mod|module|mustinherit|mustoverride|mybase|myclass|namespace|new|next|not|notinheritable|notoverridable|object|on|option|optional|or|orelse|overloads|overridable|overrides|paramarray|preserve|private|property|protected|public|raiseevent|readonly|redim|removehandler|resume|return|select|set|shadows|shared|short|single|static|step|stop|string|structure|sub|synclock|then|throw|to|try|typeof|unicode|until|variant|wend|when|while|with|withevents|writeonly|xor|endif|gosub|let|variant|wend)\b/i,
-null],["com",/^rem\b.*/i],["lit",/^(?:true\b|false\b|nothing\b|\d+(?:e[+-]?\d+[dfr]?|[dfilrs])?|(?:&h[\da-f]+|&o[0-7]+)[ils]?|\d*\.\d+(?:e[+-]?\d+)?[dfr]?|#\s+(?:\d+[/-]\d+[/-]\d+(?:\s+\d+:\d+(?::\d+)?(\s*(?:am|pm))?)?|\d+:\d+(?::\d+)?(\s*(?:am|pm))?)\s+#)/i],["pln",/^(?:(?:[a-z]|_\w)\w*(?:\[[!#%&@]+])?|\[(?:[a-z]|_\w)\w*])/i],["pun",/^[^\w\t\n\r "'[\]\xa0\u2018\u2019\u201c\u201d\u2028\u2029]+/],["pun",/^(?:\[|])/]]),["vb","vbs"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-vhdl.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-vhdl.js
deleted file mode 100644
index 51f3017..0000000
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-vhdl.js
+++ /dev/null
@@ -1,3 +0,0 @@
-PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"]],[["str",/^(?:[box]?"(?:[^"]|"")*"|'.')/i],["com",/^--[^\n\r]*/],["kwd",/^(?:abs|access|after|alias|all|and|architecture|array|assert|attribute|begin|block|body|buffer|bus|case|component|configuration|constant|disconnect|downto|else|elsif|end|entity|exit|file|for|function|generate|generic|group|guarded|if|impure|in|inertial|inout|is|label|library|linkage|literal|loop|map|mod|nand|new|next|nor|not|null|of|on|open|or|others|out|package|port|postponed|procedure|process|pure|range|record|register|reject|rem|report|return|rol|ror|select|severity|shared|signal|sla|sll|sra|srl|subtype|then|to|transport|type|unaffected|units|until|use|variable|wait|when|while|with|xnor|xor)(?=[^\w-]|$)/i,
-null],["typ",/^(?:bit|bit_vector|character|boolean|integer|real|time|string|severity_level|positive|natural|signed|unsigned|line|text|std_u?logic(?:_vector)?)(?=[^\w-]|$)/i,null],["typ",/^'(?:active|ascending|base|delayed|driving|driving_value|event|high|image|instance_name|last_active|last_event|last_value|left|leftof|length|low|path_name|pos|pred|quiet|range|reverse_range|right|rightof|simple_name|stable|succ|transaction|val|value)(?=[^\w-]|$)/i,null],["lit",/^\d+(?:_\d+)*(?:#[\w.\\]+#(?:[+-]?\d+(?:_\d+)*)?|(?:\.\d+(?:_\d+)*)?(?:e[+-]?\d+(?:_\d+)*)?)/i],
-["pln",/^(?:[a-z]\w*|\\[^\\]*\\)/i],["pun",/^[^\w\t\n\r "'\xa0][^\w\t\n\r "'\xa0-]*/]]),["vhdl","vhd"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-wiki.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-wiki.js
deleted file mode 100644
index 96c1e34..0000000
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-wiki.js
+++ /dev/null
@@ -1,2 +0,0 @@
-PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\d\t a-gi-z\xa0]+/,null,"\t \u00a0abcdefgijklmnopqrstuvwxyz0123456789"],["pun",/^[*=[\]^~]+/,null,"=*~^[]"]],[["lang-wiki.meta",/(?:^^|\r\n?|\n)(#[a-z]+)\b/],["lit",/^[A-Z][a-z][\da-z]+[A-Z][a-z][^\W_]+\b/],["lang-",/^{{{([\S\s]+?)}}}/],["lang-",/^`([^\n\r`]+)`/],["str",/^https?:\/\/[^\s#/?]*(?:\/[^\s#?]*)?(?:\?[^\s#]*)?(?:#\S*)?/i],["pln",/^(?:\r\n|[\S\s])[^\n\r#*=A-[^`h{~]*/]]),["wiki"]);
-PR.registerLangHandler(PR.createSimpleLexer([["kwd",/^#[a-z]+/i,null,"#"]],[]),["wiki.meta"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-xq.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-xq.js
deleted file mode 100644
index e323ae3..0000000
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-xq.js
+++ /dev/null
@@ -1,3 +0,0 @@
-PR.registerLangHandler(PR.createSimpleLexer([["var pln",/^\$[\w-]+/,null,"$"]],[["pln",/^[\s=][<>][\s=]/],["lit",/^@[\w-]+/],["tag",/^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["com",/^\(:[\S\s]*?:\)/],["pln",/^[(),/;[\]{}]$/],["str",/^(?:"(?:[^"\\{]|\\[\S\s])*(?:"|$)|'(?:[^'\\{]|\\[\S\s])*(?:'|$))/,null,"\"'"],["kwd",/^(?:xquery|where|version|variable|union|typeswitch|treat|to|then|text|stable|sortby|some|self|schema|satisfies|returns|return|ref|processing-instruction|preceding-sibling|preceding|precedes|parent|only|of|node|namespace|module|let|item|intersect|instance|in|import|if|function|for|follows|following-sibling|following|external|except|every|else|element|descending|descendant-or-self|descendant|define|default|declare|comment|child|cast|case|before|attribute|assert|ascending|as|ancestor-or-self|ancestor|after|eq|order|by|or|and|schema-element|document-node|node|at)\b/],
-["typ",/^(?:xs:yearMonthDuration|xs:unsignedLong|xs:time|xs:string|xs:short|xs:QName|xs:Name|xs:long|xs:integer|xs:int|xs:gYearMonth|xs:gYear|xs:gMonthDay|xs:gDay|xs:float|xs:duration|xs:double|xs:decimal|xs:dayTimeDuration|xs:dateTime|xs:date|xs:byte|xs:boolean|xs:anyURI|xf:yearMonthDuration)\b/,null],["fun pln",/^(?:xp:dereference|xinc:node-expand|xinc:link-references|xinc:link-expand|xhtml:restructure|xhtml:clean|xhtml:add-lists|xdmp:zip-manifest|xdmp:zip-get|xdmp:zip-create|xdmp:xquery-version|xdmp:word-convert|xdmp:with-namespaces|xdmp:version|xdmp:value|xdmp:user-roles|xdmp:user-last-login|xdmp:user|xdmp:url-encode|xdmp:url-decode|xdmp:uri-is-file|xdmp:uri-format|xdmp:uri-content-type|xdmp:unquote|xdmp:unpath|xdmp:triggers-database|xdmp:trace|xdmp:to-json|xdmp:tidy|xdmp:subbinary|xdmp:strftime|xdmp:spawn-in|xdmp:spawn|xdmp:sleep|xdmp:shutdown|xdmp:set-session-field|xdmp:set-response-encoding|xdmp:set-response-content-type|xdmp:set-response-code|xdmp:set-request-time-limit|xdmp:set|xdmp:servers|xdmp:server-status|xdmp:server-name|xdmp:server|xdmp:security-database|xdmp:security-assert|xdmp:schema-database|xdmp:save|xdmp:role-roles|xdmp:role|xdmp:rethrow|xdmp:restart|xdmp:request-timestamp|xdmp:request-status|xdmp:request-cancel|xdmp:request|xdmp:redirect-response|xdmp:random|xdmp:quote|xdmp:query-trace|xdmp:query-meters|xdmp:product-edition|xdmp:privilege-roles|xdmp:privilege|xdmp:pretty-print|xdmp:powerpoint-convert|xdmp:platform|xdmp:permission|xdmp:pdf-convert|xdmp:path|xdmp:octal-to-integer|xdmp:node-uri|xdmp:node-replace|xdmp:node-kind|xdmp:node-insert-child|xdmp:node-insert-before|xdmp:node-insert-after|xdmp:node-delete|xdmp:node-database|xdmp:mul64|xdmp:modules-root|xdmp:modules-database|xdmp:merging|xdmp:merge-cancel|xdmp:merge|xdmp:md5|xdmp:logout|xdmp:login|xdmp:log-level|xdmp:log|xdmp:lock-release|xdmp:lock-acquire|xdmp:load|xdmp:invoke-in|xdmp:invoke|xdmp:integer-to-octal|xdmp:integer-to-hex|xdmp:http-put|xdmp:http-post|xdmp:http-options|xdmp:http-head|xdmp:http-get|xdmp:http-delete|xdmp:hosts|xdmp:host-status|xdmp:host-name|xdmp:host|xdmp:hex-to-integer|xdmp:hash64|xdmp:hash32|xdmp:has-privilege|xdmp:groups|xdmp:group-serves|xdmp:group-servers|xdmp:group-name|xdmp:group-hosts|xdmp:group|xdmp:get-session-field-names|xdmp:get-session-field|xdmp:get-response-encoding|xdmp:get-response-code|xdmp:get-request-username|xdmp:get-request-user|xdmp:get-request-url|xdmp:get-request-protocol|xdmp:get-request-path|xdmp:get-request-method|xdmp:get-request-header-names|xdmp:get-request-header|xdmp:get-request-field-names|xdmp:get-request-field-filename|xdmp:get-request-field-content-type|xdmp:get-request-field|xdmp:get-request-client-certificate|xdmp:get-request-client-address|xdmp:get-request-body|xdmp:get-current-user|xdmp:get-current-roles|xdmp:get|xdmp:function-name|xdmp:function-module|xdmp:function|xdmp:from-json|xdmp:forests|xdmp:forest-status|xdmp:forest-restore|xdmp:forest-restart|xdmp:forest-name|xdmp:forest-delete|xdmp:forest-databases|xdmp:forest-counts|xdmp:forest-clear|xdmp:forest-backup|xdmp:forest|xdmp:filesystem-file|xdmp:filesystem-directory|xdmp:exists|xdmp:excel-convert|xdmp:eval-in|xdmp:eval|xdmp:estimate|xdmp:email|xdmp:element-content-type|xdmp:elapsed-time|xdmp:document-set-quality|xdmp:document-set-property|xdmp:document-set-properties|xdmp:document-set-permissions|xdmp:document-set-collections|xdmp:document-remove-properties|xdmp:document-remove-permissions|xdmp:document-remove-collections|xdmp:document-properties|xdmp:document-locks|xdmp:document-load|xdmp:document-insert|xdmp:document-get-quality|xdmp:document-get-properties|xdmp:document-get-permissions|xdmp:document-get-collections|xdmp:document-get|xdmp:document-forest|xdmp:document-delete|xdmp:document-add-properties|xdmp:document-add-permissions|xdmp:document-add-collections|xdmp:directory-properties|xdmp:directory-locks|xdmp:directory-delete|xdmp:directory-create|xdmp:directory|xdmp:diacritic-less|xdmp:describe|xdmp:default-permissions|xdmp:default-collections|xdmp:databases|xdmp:database-restore-validate|xdmp:database-restore-status|xdmp:database-restore-cancel|xdmp:database-restore|xdmp:database-name|xdmp:database-forests|xdmp:database-backup-validate|xdmp:database-backup-status|xdmp:database-backup-purge|xdmp:database-backup-cancel|xdmp:database-backup|xdmp:database|xdmp:collection-properties|xdmp:collection-locks|xdmp:collection-delete|xdmp:collation-canonical-uri|xdmp:castable-as|xdmp:can-grant-roles|xdmp:base64-encode|xdmp:base64-decode|xdmp:architecture|xdmp:apply|xdmp:amp-roles|xdmp:amp|xdmp:add64|xdmp:add-response-header|xdmp:access|trgr:trigger-set-recursive|trgr:trigger-set-permissions|trgr:trigger-set-name|trgr:trigger-set-module|trgr:trigger-set-event|trgr:trigger-set-description|trgr:trigger-remove-permissions|trgr:trigger-module|trgr:trigger-get-permissions|trgr:trigger-enable|trgr:trigger-disable|trgr:trigger-database-online-event|trgr:trigger-data-event|trgr:trigger-add-permissions|trgr:remove-trigger|trgr:property-content|trgr:pre-commit|trgr:post-commit|trgr:get-trigger-by-id|trgr:get-trigger|trgr:document-scope|trgr:document-content|trgr:directory-scope|trgr:create-trigger|trgr:collection-scope|trgr:any-property-content|thsr:set-entry|thsr:remove-term|thsr:remove-synonym|thsr:remove-entry|thsr:query-lookup|thsr:lookup|thsr:load|thsr:insert|thsr:expand|thsr:add-synonym|spell:suggest-detailed|spell:suggest|spell:remove-word|spell:make-dictionary|spell:load|spell:levenshtein-distance|spell:is-correct|spell:insert|spell:double-metaphone|spell:add-word|sec:users-collection|sec:user-set-roles|sec:user-set-password|sec:user-set-name|sec:user-set-description|sec:user-set-default-permissions|sec:user-set-default-collections|sec:user-remove-roles|sec:user-privileges|sec:user-get-roles|sec:user-get-description|sec:user-get-default-permissions|sec:user-get-default-collections|sec:user-doc-permissions|sec:user-doc-collections|sec:user-add-roles|sec:unprotect-collection|sec:uid-for-name|sec:set-realm|sec:security-version|sec:security-namespace|sec:security-installed|sec:security-collection|sec:roles-collection|sec:role-set-roles|sec:role-set-name|sec:role-set-description|sec:role-set-default-permissions|sec:role-set-default-collections|sec:role-remove-roles|sec:role-privileges|sec:role-get-roles|sec:role-get-description|sec:role-get-default-permissions|sec:role-get-default-collections|sec:role-doc-permissions|sec:role-doc-collections|sec:role-add-roles|sec:remove-user|sec:remove-role-from-users|sec:remove-role-from-role|sec:remove-role-from-privileges|sec:remove-role-from-amps|sec:remove-role|sec:remove-privilege|sec:remove-amp|sec:protect-collection|sec:privileges-collection|sec:privilege-set-roles|sec:privilege-set-name|sec:privilege-remove-roles|sec:privilege-get-roles|sec:privilege-add-roles|sec:priv-doc-permissions|sec:priv-doc-collections|sec:get-user-names|sec:get-unique-elem-id|sec:get-role-names|sec:get-role-ids|sec:get-privilege|sec:get-distinct-permissions|sec:get-collection|sec:get-amp|sec:create-user-with-role|sec:create-user|sec:create-role|sec:create-privilege|sec:create-amp|sec:collections-collection|sec:collection-set-permissions|sec:collection-remove-permissions|sec:collection-get-permissions|sec:collection-add-permissions|sec:check-admin|sec:amps-collection|sec:amp-set-roles|sec:amp-remove-roles|sec:amp-get-roles|sec:amp-doc-permissions|sec:amp-doc-collections|sec:amp-add-roles|search:unparse|search:suggest|search:snippet|search:search|search:resolve-nodes|search:resolve|search:remove-constraint|search:parse|search:get-default-options|search:estimate|search:check-options|prof:value|prof:reset|prof:report|prof:invoke|prof:eval|prof:enable|prof:disable|prof:allowed|ppt:clean|pki:template-set-request|pki:template-set-name|pki:template-set-key-type|pki:template-set-key-options|pki:template-set-description|pki:template-in-use|pki:template-get-version|pki:template-get-request|pki:template-get-name|pki:template-get-key-type|pki:template-get-key-options|pki:template-get-id|pki:template-get-description|pki:need-certificate|pki:is-temporary|pki:insert-trusted-certificates|pki:insert-template|pki:insert-signed-certificates|pki:insert-certificate-revocation-list|pki:get-trusted-certificate-ids|pki:get-template-ids|pki:get-template-certificate-authority|pki:get-template-by-name|pki:get-template|pki:get-pending-certificate-requests-xml|pki:get-pending-certificate-requests-pem|pki:get-pending-certificate-request|pki:get-certificates-for-template-xml|pki:get-certificates-for-template|pki:get-certificates|pki:get-certificate-xml|pki:get-certificate-pem|pki:get-certificate|pki:generate-temporary-certificate-if-necessary|pki:generate-temporary-certificate|pki:generate-template-certificate-authority|pki:generate-certificate-request|pki:delete-template|pki:delete-certificate|pki:create-template|pdf:make-toc|pdf:insert-toc-headers|pdf:get-toc|pdf:clean|p:status-transition|p:state-transition|p:remove|p:pipelines|p:insert|p:get-by-id|p:get|p:execute|p:create|p:condition|p:collection|p:action|ooxml:runs-merge|ooxml:package-uris|ooxml:package-parts-insert|ooxml:package-parts|msword:clean|mcgm:polygon|mcgm:point|mcgm:geospatial-query-from-elements|mcgm:geospatial-query|mcgm:circle|math:tanh|math:tan|math:sqrt|math:sinh|math:sin|math:pow|math:modf|math:log10|math:log|math:ldexp|math:frexp|math:fmod|math:floor|math:fabs|math:exp|math:cosh|math:cos|math:ceil|math:atan2|math:atan|math:asin|math:acos|map:put|map:map|map:keys|map:get|map:delete|map:count|map:clear|lnk:to|lnk:remove|lnk:insert|lnk:get|lnk:from|lnk:create|kml:polygon|kml:point|kml:interior-polygon|kml:geospatial-query-from-elements|kml:geospatial-query|kml:circle|kml:box|gml:polygon|gml:point|gml:interior-polygon|gml:geospatial-query-from-elements|gml:geospatial-query|gml:circle|gml:box|georss:point|georss:geospatial-query|georss:circle|geo:polygon|geo:point|geo:interior-polygon|geo:geospatial-query-from-elements|geo:geospatial-query|geo:circle|geo:box|fn:zero-or-one|fn:years-from-duration|fn:year-from-dateTime|fn:year-from-date|fn:upper-case|fn:unordered|fn:true|fn:translate|fn:trace|fn:tokenize|fn:timezone-from-time|fn:timezone-from-dateTime|fn:timezone-from-date|fn:sum|fn:subtract-dateTimes-yielding-yearMonthDuration|fn:subtract-dateTimes-yielding-dayTimeDuration|fn:substring-before|fn:substring-after|fn:substring|fn:subsequence|fn:string-to-codepoints|fn:string-pad|fn:string-length|fn:string-join|fn:string|fn:static-base-uri|fn:starts-with|fn:seconds-from-time|fn:seconds-from-duration|fn:seconds-from-dateTime|fn:round-half-to-even|fn:round|fn:root|fn:reverse|fn:resolve-uri|fn:resolve-QName|fn:replace|fn:remove|fn:QName|fn:prefix-from-QName|fn:position|fn:one-or-more|fn:number|fn:not|fn:normalize-unicode|fn:normalize-space|fn:node-name|fn:node-kind|fn:nilled|fn:namespace-uri-from-QName|fn:namespace-uri-for-prefix|fn:namespace-uri|fn:name|fn:months-from-duration|fn:month-from-dateTime|fn:month-from-date|fn:minutes-from-time|fn:minutes-from-duration|fn:minutes-from-dateTime|fn:min|fn:max|fn:matches|fn:lower-case|fn:local-name-from-QName|fn:local-name|fn:last|fn:lang|fn:iri-to-uri|fn:insert-before|fn:index-of|fn:in-scope-prefixes|fn:implicit-timezone|fn:idref|fn:id|fn:hours-from-time|fn:hours-from-duration|fn:hours-from-dateTime|fn:floor|fn:false|fn:expanded-QName|fn:exists|fn:exactly-one|fn:escape-uri|fn:escape-html-uri|fn:error|fn:ends-with|fn:encode-for-uri|fn:empty|fn:document-uri|fn:doc-available|fn:doc|fn:distinct-values|fn:distinct-nodes|fn:default-collation|fn:deep-equal|fn:days-from-duration|fn:day-from-dateTime|fn:day-from-date|fn:data|fn:current-time|fn:current-dateTime|fn:current-date|fn:count|fn:contains|fn:concat|fn:compare|fn:collection|fn:codepoints-to-string|fn:codepoint-equal|fn:ceiling|fn:boolean|fn:base-uri|fn:avg|fn:adjust-time-to-timezone|fn:adjust-dateTime-to-timezone|fn:adjust-date-to-timezone|fn:abs|feed:unsubscribe|feed:subscription|feed:subscribe|feed:request|feed:item|feed:description|excel:clean|entity:enrich|dom:set-pipelines|dom:set-permissions|dom:set-name|dom:set-evaluation-context|dom:set-domain-scope|dom:set-description|dom:remove-pipeline|dom:remove-permissions|dom:remove|dom:get|dom:evaluation-context|dom:domains|dom:domain-scope|dom:create|dom:configuration-set-restart-user|dom:configuration-set-permissions|dom:configuration-set-evaluation-context|dom:configuration-set-default-domain|dom:configuration-get|dom:configuration-create|dom:collection|dom:add-pipeline|dom:add-permissions|dls:retention-rules|dls:retention-rule-remove|dls:retention-rule-insert|dls:retention-rule|dls:purge|dls:node-expand|dls:link-references|dls:link-expand|dls:documents-query|dls:document-versions-query|dls:document-version-uri|dls:document-version-query|dls:document-version-delete|dls:document-version-as-of|dls:document-version|dls:document-update|dls:document-unmanage|dls:document-set-quality|dls:document-set-property|dls:document-set-properties|dls:document-set-permissions|dls:document-set-collections|dls:document-retention-rules|dls:document-remove-properties|dls:document-remove-permissions|dls:document-remove-collections|dls:document-purge|dls:document-manage|dls:document-is-managed|dls:document-insert-and-manage|dls:document-include-query|dls:document-history|dls:document-get-permissions|dls:document-extract-part|dls:document-delete|dls:document-checkout-status|dls:document-checkout|dls:document-checkin|dls:document-add-properties|dls:document-add-permissions|dls:document-add-collections|dls:break-checkout|dls:author-query|dls:as-of-query|dbk:convert|dbg:wait|dbg:value|dbg:stopped|dbg:stop|dbg:step|dbg:status|dbg:stack|dbg:out|dbg:next|dbg:line|dbg:invoke|dbg:function|dbg:finish|dbg:expr|dbg:eval|dbg:disconnect|dbg:detach|dbg:continue|dbg:connect|dbg:clear|dbg:breakpoints|dbg:break|dbg:attached|dbg:attach|cvt:save-converted-documents|cvt:part-uri|cvt:destination-uri|cvt:basepath|cvt:basename|cts:words|cts:word-query-weight|cts:word-query-text|cts:word-query-options|cts:word-query|cts:word-match|cts:walk|cts:uris|cts:uri-match|cts:train|cts:tokenize|cts:thresholds|cts:stem|cts:similar-query-weight|cts:similar-query-nodes|cts:similar-query|cts:shortest-distance|cts:search|cts:score|cts:reverse-query-weight|cts:reverse-query-nodes|cts:reverse-query|cts:remainder|cts:registered-query-weight|cts:registered-query-options|cts:registered-query-ids|cts:registered-query|cts:register|cts:query|cts:quality|cts:properties-query-query|cts:properties-query|cts:polygon-vertices|cts:polygon|cts:point-longitude|cts:point-latitude|cts:point|cts:or-query-queries|cts:or-query|cts:not-query-weight|cts:not-query-query|cts:not-query|cts:near-query-weight|cts:near-query-queries|cts:near-query-options|cts:near-query-distance|cts:near-query|cts:highlight|cts:geospatial-co-occurrences|cts:frequency|cts:fitness|cts:field-words|cts:field-word-query-weight|cts:field-word-query-text|cts:field-word-query-options|cts:field-word-query-field-name|cts:field-word-query|cts:field-word-match|cts:entity-highlight|cts:element-words|cts:element-word-query-weight|cts:element-word-query-text|cts:element-word-query-options|cts:element-word-query-element-name|cts:element-word-query|cts:element-word-match|cts:element-values|cts:element-value-ranges|cts:element-value-query-weight|cts:element-value-query-text|cts:element-value-query-options|cts:element-value-query-element-name|cts:element-value-query|cts:element-value-match|cts:element-value-geospatial-co-occurrences|cts:element-value-co-occurrences|cts:element-range-query-weight|cts:element-range-query-value|cts:element-range-query-options|cts:element-range-query-operator|cts:element-range-query-element-name|cts:element-range-query|cts:element-query-query|cts:element-query-element-name|cts:element-query|cts:element-pair-geospatial-values|cts:element-pair-geospatial-value-match|cts:element-pair-geospatial-query-weight|cts:element-pair-geospatial-query-region|cts:element-pair-geospatial-query-options|cts:element-pair-geospatial-query-longitude-name|cts:element-pair-geospatial-query-latitude-name|cts:element-pair-geospatial-query-element-name|cts:element-pair-geospatial-query|cts:element-pair-geospatial-boxes|cts:element-geospatial-values|cts:element-geospatial-value-match|cts:element-geospatial-query-weight|cts:element-geospatial-query-region|cts:element-geospatial-query-options|cts:element-geospatial-query-element-name|cts:element-geospatial-query|cts:element-geospatial-boxes|cts:element-child-geospatial-values|cts:element-child-geospatial-value-match|cts:element-child-geospatial-query-weight|cts:element-child-geospatial-query-region|cts:element-child-geospatial-query-options|cts:element-child-geospatial-query-element-name|cts:element-child-geospatial-query-child-name|cts:element-child-geospatial-query|cts:element-child-geospatial-boxes|cts:element-attribute-words|cts:element-attribute-word-query-weight|cts:element-attribute-word-query-text|cts:element-attribute-word-query-options|cts:element-attribute-word-query-element-name|cts:element-attribute-word-query-attribute-name|cts:element-attribute-word-query|cts:element-attribute-word-match|cts:element-attribute-values|cts:element-attribute-value-ranges|cts:element-attribute-value-query-weight|cts:element-attribute-value-query-text|cts:element-attribute-value-query-options|cts:element-attribute-value-query-element-name|cts:element-attribute-value-query-attribute-name|cts:element-attribute-value-query|cts:element-attribute-value-match|cts:element-attribute-value-geospatial-co-occurrences|cts:element-attribute-value-co-occurrences|cts:element-attribute-range-query-weight|cts:element-attribute-range-query-value|cts:element-attribute-range-query-options|cts:element-attribute-range-query-operator|cts:element-attribute-range-query-element-name|cts:element-attribute-range-query-attribute-name|cts:element-attribute-range-query|cts:element-attribute-pair-geospatial-values|cts:element-attribute-pair-geospatial-value-match|cts:element-attribute-pair-geospatial-query-weight|cts:element-attribute-pair-geospatial-query-region|cts:element-attribute-pair-geospatial-query-options|cts:element-attribute-pair-geospatial-query-longitude-name|cts:element-attribute-pair-geospatial-query-latitude-name|cts:element-attribute-pair-geospatial-query-element-name|cts:element-attribute-pair-geospatial-query|cts:element-attribute-pair-geospatial-boxes|cts:document-query-uris|cts:document-query|cts:distance|cts:directory-query-uris|cts:directory-query-depth|cts:directory-query|cts:destination|cts:deregister|cts:contains|cts:confidence|cts:collections|cts:collection-query-uris|cts:collection-query|cts:collection-match|cts:classify|cts:circle-radius|cts:circle-center|cts:circle|cts:box-west|cts:box-south|cts:box-north|cts:box-east|cts:box|cts:bearing|cts:arc-intersection|cts:and-query-queries|cts:and-query-options|cts:and-query|cts:and-not-query-positive-query|cts:and-not-query-negative-query|cts:and-not-query|css:get|css:convert|cpf:success|cpf:failure|cpf:document-set-state|cpf:document-set-processing-status|cpf:document-set-last-updated|cpf:document-set-error|cpf:document-get-state|cpf:document-get-processing-status|cpf:document-get-last-updated|cpf:document-get-error|cpf:check-transition|alert:spawn-matching-actions|alert:rule-user-id-query|alert:rule-set-user-id|alert:rule-set-query|alert:rule-set-options|alert:rule-set-name|alert:rule-set-description|alert:rule-set-action|alert:rule-remove|alert:rule-name-query|alert:rule-insert|alert:rule-id-query|alert:rule-get-user-id|alert:rule-get-query|alert:rule-get-options|alert:rule-get-name|alert:rule-get-id|alert:rule-get-description|alert:rule-get-action|alert:rule-action-query|alert:remove-triggers|alert:make-rule|alert:make-log-action|alert:make-config|alert:make-action|alert:invoke-matching-actions|alert:get-my-rules|alert:get-all-rules|alert:get-actions|alert:find-matching-rules|alert:create-triggers|alert:config-set-uri|alert:config-set-trigger-ids|alert:config-set-options|alert:config-set-name|alert:config-set-description|alert:config-set-cpf-domain-names|alert:config-set-cpf-domain-ids|alert:config-insert|alert:config-get-uri|alert:config-get-trigger-ids|alert:config-get-options|alert:config-get-name|alert:config-get-id|alert:config-get-description|alert:config-get-cpf-domain-names|alert:config-get-cpf-domain-ids|alert:config-get|alert:config-delete|alert:action-set-options|alert:action-set-name|alert:action-set-module-root|alert:action-set-module-db|alert:action-set-module|alert:action-set-description|alert:action-remove|alert:action-insert|alert:action-get-options|alert:action-get-name|alert:action-get-module-root|alert:action-get-module-db|alert:action-get-module|alert:action-get-description|zero-or-one|years-from-duration|year-from-dateTime|year-from-date|upper-case|unordered|true|translate|trace|tokenize|timezone-from-time|timezone-from-dateTime|timezone-from-date|sum|subtract-dateTimes-yielding-yearMonthDuration|subtract-dateTimes-yielding-dayTimeDuration|substring-before|substring-after|substring|subsequence|string-to-codepoints|string-pad|string-length|string-join|string|static-base-uri|starts-with|seconds-from-time|seconds-from-duration|seconds-from-dateTime|round-half-to-even|round|root|reverse|resolve-uri|resolve-QName|replace|remove|QName|prefix-from-QName|position|one-or-more|number|not|normalize-unicode|normalize-space|node-name|node-kind|nilled|namespace-uri-from-QName|namespace-uri-for-prefix|namespace-uri|name|months-from-duration|month-from-dateTime|month-from-date|minutes-from-time|minutes-from-duration|minutes-from-dateTime|min|max|matches|lower-case|local-name-from-QName|local-name|last|lang|iri-to-uri|insert-before|index-of|in-scope-prefixes|implicit-timezone|idref|id|hours-from-time|hours-from-duration|hours-from-dateTime|floor|false|expanded-QName|exists|exactly-one|escape-uri|escape-html-uri|error|ends-with|encode-for-uri|empty|document-uri|doc-available|doc|distinct-values|distinct-nodes|default-collation|deep-equal|days-from-duration|day-from-dateTime|day-from-date|data|current-time|current-dateTime|current-date|count|contains|concat|compare|collection|codepoints-to-string|codepoint-equal|ceiling|boolean|base-uri|avg|adjust-time-to-timezone|adjust-dateTime-to-timezone|adjust-date-to-timezone|abs)\b/],
-["pln",/^[\w:-]+/],["pln",/^[\t\n\r \xa0]+/]]),["xq","xquery"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-yaml.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-yaml.js
deleted file mode 100644
index c38729b..0000000
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-yaml.js
+++ /dev/null
@@ -1,2 +0,0 @@
-var a=null;
-PR.registerLangHandler(PR.createSimpleLexer([["pun",/^[:>?|]+/,a,":|>?"],["dec",/^%(?:YAML|TAG)[^\n\r#]+/,a,"%"],["typ",/^&\S+/,a,"&"],["typ",/^!\S*/,a,"!"],["str",/^"(?:[^"\\]|\\.)*(?:"|$)/,a,'"'],["str",/^'(?:[^']|'')*(?:'|$)/,a,"'"],["com",/^#[^\n\r]*/,a,"#"],["pln",/^\s+/,a," \t\r\n"]],[["dec",/^(?:---|\.\.\.)(?:[\n\r]|$)/],["pun",/^-/],["kwd",/^\w+:[\n\r ]/],["pln",/^\w+/]]),["yaml","yml"]);
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java
index 327a5b8..9e36fc1 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java
@@ -49,17 +49,13 @@
  * the internal SSH daemon. One record per SSH key uploaded by the user, keys
  * are checked in random order until a match is found.</li>
  *
- * <li>{@link StarredChange}: user has starred the change, tracking
- * notifications of updates on that change, or just book-marking it for faster
- * future reference. One record per starred change.</li>
- *
  * <li>{@link DiffPreferencesInfo}: user's preferences for rendering side-to-side
  * and unified diff</li>
  *
  * </ul>
  */
 public final class Account {
-  public static enum FieldName {
+  public enum FieldName {
     FULL_NAME, USER_NAME, REGISTER_NEW_EMAIL
   }
 
@@ -126,46 +122,23 @@
      *              We assume that the caller has trimmed any prefix.
      */
     public static Id fromRefPart(String name) {
-      if (name == null) {
-        return null;
-      }
+      Integer id = RefNames.parseShardedRefPart(name);
+      return id != null ? new Account.Id(id) : null;
+    }
 
-      String[] parts = name.split("/");
-      int n = parts.length;
-      if (n < 2) {
-        return null;
-      }
-
-      // Last 2 digits.
-      int le;
-      for (le = 0; le < parts[0].length(); le++) {
-        if (!Character.isDigit(parts[0].charAt(le))) {
-          return null;
-        }
-      }
-      if (le != 2) {
-        return null;
-      }
-
-      // Full ID.
-      int ie;
-      for (ie = 0; ie < parts[1].length(); ie++) {
-        if (!Character.isDigit(parts[1].charAt(ie))) {
-          if (ie == 0) {
-            return null;
-          } else {
-            break;
-          }
-        }
-      }
-
-      int shard = Integer.parseInt(parts[0]);
-      int id = Integer.parseInt(parts[1].substring(0, ie));
-
-      if (id % 100 != shard) {
-        return null;
-      }
-      return new Account.Id(id);
+    /**
+     * Parse an Account.Id out of the last part of a ref name.
+     * <p>
+     * The input is a ref name of the form {@code ".../1234"}, where the suffix
+     * is a non-sharded account ID. Ref names using a sharded ID should use
+     * {@link #fromRefPart(String)} instead for greater safety.
+     *
+     * @param name ref name
+     * @return account ID, or null if not numeric.
+     */
+    public static Id fromRefSuffix(String name) {
+      Integer id = RefNames.parseRefSuffix(name);
+      return id != null ? new Account.Id(id) : null;
     }
   }
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountSshKey.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountSshKey.java
index 8c4ac82..f63c618 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountSshKey.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountSshKey.java
@@ -14,18 +14,17 @@
 
 package com.google.gerrit.reviewdb.client;
 
-import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.IntKey;
 
+import java.util.Objects;
+
 /** An SSH key approved for use by an {@link Account}. */
 public final class AccountSshKey {
   public static class Id extends IntKey<Account.Id> {
     private static final long serialVersionUID = 1L;
 
-    @Column(id = 1)
     protected Account.Id accountId;
 
-    @Column(id = 2)
     protected int seq;
 
     protected Id() {
@@ -51,15 +50,16 @@
     protected void set(int newValue) {
       seq = newValue;
     }
+
+    public boolean isValid() {
+      return seq > 0;
+    }
   }
 
-  @Column(id = 1, name = Column.NONE)
   protected AccountSshKey.Id id;
 
-  @Column(id = 2, length = Integer.MAX_VALUE)
   protected String sshPublicKey;
 
-  @Column(id = 3)
   protected boolean valid;
 
   protected AccountSshKey() {
@@ -68,7 +68,7 @@
   public AccountSshKey(final AccountSshKey.Id i, final String pub) {
     id = i;
     sshPublicKey = pub;
-    valid = true; // We can assume it is fine.
+    valid = id.isValid();
   }
 
   public Account.Id getAccount() {
@@ -83,50 +83,50 @@
     return sshPublicKey;
   }
 
-  public String getAlgorithm() {
-    final String s = getSshPublicKey();
-    if (s == null || s.length() == 0) {
-      return "none";
+  private String getPublicKeyPart(int index, String defaultValue) {
+    String s = getSshPublicKey();
+    if (s != null && s.length() > 0) {
+      String[] parts = s.split(" ");
+      if (parts.length > index) {
+        return parts[index];
+      }
     }
+    return defaultValue;
+  }
 
-    final String[] parts = s.split(" ");
-    if (parts.length < 1) {
-      return "none";
-    }
-    return parts[0];
+  public String getAlgorithm() {
+    return getPublicKeyPart(0, "none");
   }
 
   public String getEncodedKey() {
-    final String s = getSshPublicKey();
-    if (s == null || s.length() == 0) {
-      return null;
-    }
-
-    final String[] parts = s.split(" ");
-    if (parts.length < 2) {
-      return null;
-    }
-    return parts[1];
+    return getPublicKeyPart(1, null);
   }
 
   public String getComment() {
-    final String s = getSshPublicKey();
-    if (s == null || s.length() == 0) {
-      return "";
-    }
-
-    final String[] parts = s.split(" ", 3);
-    if (parts.length < 3) {
-      return "";
-    }
-    return parts[2];
+    return getPublicKeyPart(2, "");
   }
 
   public boolean isValid() {
-    return valid;
+    return valid && id.isValid();
   }
 
   public void setInvalid() {
     valid = false;
   }
+
+  @Override
+  public boolean equals(Object o) {
+    if (o instanceof AccountSshKey) {
+      AccountSshKey other = (AccountSshKey) o;
+      return Objects.equals(id, other.id)
+          && Objects.equals(sshPublicKey, other.sshPublicKey)
+          && Objects.equals(valid, other.valid);
+    }
+    return false;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(id, sshPublicKey, valid);
+  }
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
index 5c70d3c..20f9b82 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
@@ -160,7 +160,15 @@
           RefNames.EDIT_PREFIX.length();
       int endChangeId = nextNonDigit(ref, startChangeId);
       String id = ref.substring(startChangeId, endChangeId);
-      return new Change.Id(Integer.parseInt(id));
+      if (id != null && !id.isEmpty()) {
+        return new Change.Id(Integer.parseInt(id));
+      }
+      return null;
+    }
+
+    public static Id fromRefPart(String ref) {
+      Integer id = RefNames.parseShardedRefPart(ref);
+      return id != null ? new Change.Id(id) : null;
     }
 
     static int startIndex(String ref) {
@@ -281,7 +289,7 @@
    * codes ('A'..'Z') indicate a change that is closed and cannot be further
    * modified.
    * */
-  public static enum Status {
+  public enum Status {
     /**
      * Change is open and pending review, or review is in progress.
      *
@@ -356,7 +364,7 @@
     private final boolean closed;
     private final ChangeStatus changeStatus;
 
-    private Status(char c, ChangeStatus cs) {
+    Status(char c, ChangeStatus cs) {
       code = c;
       closed = !(MIN_OPEN <= c && c <= MAX_OPEN);
       changeStatus = cs;
@@ -572,6 +580,10 @@
     return originalSubject != null ? originalSubject : subject;
   }
 
+  public String getOriginalSubjectOrNull() {
+    return originalSubject;
+  }
+
   /** Get the id of the most current {@link PatchSet} in this change. */
   public PatchSet.Id currentPatchSetId() {
     if (currentPatchSetId > 0) {
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ChangeMessage.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ChangeMessage.java
index 70169a0..898dc94 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ChangeMessage.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ChangeMessage.java
@@ -74,6 +74,10 @@
   @Column(id = 5, notNull = false)
   protected PatchSet.Id patchset;
 
+  /** Tag associated with change message */
+  @Column(id = 6, notNull = false)
+  protected String tag;
+
   protected ChangeMessage() {
   }
 
@@ -117,6 +121,14 @@
     message = s;
   }
 
+  public String getTag() {
+    return tag;
+  }
+
+  public void setTag(String tag) {
+    this.tag = tag;
+  }
+
   public PatchSet.Id getPatchSetId() {
     return patchset;
   }
@@ -132,6 +144,7 @@
         + ", author=" + author
         + ", writtenOn=" + writtenOn
         + ", patchset=" + patchset
+        + ", tag=" + tag
         + ", message=[" + message
         + "]}";
   }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/CommentRange.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/CommentRange.java
index b1b2615..5a98d94 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/CommentRange.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/CommentRange.java
@@ -96,4 +96,4 @@
     return "Range[startLine=" + startLine + ", startCharacter=" + startCharacter
         + ", endLine=" + endLine + ", endCharacter=" + endCharacter + "]";
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Patch.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Patch.java
index 3637914..6a55965 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Patch.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Patch.java
@@ -68,7 +68,7 @@
   }
 
   /** Type of modification made to the file path. */
-  public static enum ChangeType implements CodedEnum {
+  public enum ChangeType implements CodedEnum {
     /** Path is being created/introduced by this patch. */
     ADDED('A'),
 
@@ -89,7 +89,7 @@
 
     private final char code;
 
-    private ChangeType(final char c) {
+    ChangeType(final char c) {
       code = c;
     }
 
@@ -113,7 +113,7 @@
   }
 
   /** Type of formatting for this patch. */
-  public static enum PatchType implements CodedEnum {
+  public enum PatchType implements CodedEnum {
     /**
      * A textual difference between two versions.
      *
@@ -148,7 +148,7 @@
 
     private final char code;
 
-    private PatchType(final char c) {
+    PatchType(final char c) {
       code = c;
     }
 
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 c04b729..16b2d61 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
@@ -60,14 +60,14 @@
   public static final char STATUS_DRAFT = 'd';
   public static final char STATUS_PUBLISHED = 'P';
 
-  public static enum Status {
+  public enum Status {
     DRAFT(STATUS_DRAFT),
 
     PUBLISHED(STATUS_PUBLISHED);
 
     private final char code;
 
-    private Status(final char c) {
+    Status(final char c) {
       code = c;
     }
 
@@ -122,6 +122,9 @@
   @Column(id = 9, notNull = false)
   protected CommentRange range;
 
+  @Column(id = 10, notNull = false)
+  protected String tag;
+
   /**
    * The RevId for the commit to which this comment is referring.
    *
@@ -144,6 +147,25 @@
     setWrittenOn(when);
   }
 
+  public PatchLineComment(PatchLineComment o) {
+    key = o.key;
+    lineNbr = o.lineNbr;
+    author = o.author;
+    writtenOn = o.writtenOn;
+    status = o.status;
+    side = o.side;
+    message = o.message;
+    parentUuid = o.parentUuid;
+    revId = o.revId;
+    if (o.range != null) {
+      range = new CommentRange(
+          o.range.getStartLine(),
+          o.range.getStartCharacter(),
+          o.range.getEndLine(),
+          o.range.getEndCharacter());
+    }
+  }
+
   public PatchLineComment.Key getKey() {
     return key;
   }
@@ -230,6 +252,14 @@
     return revId;
   }
 
+  public void setTag(String tag) {
+    this.tag = tag;
+  }
+
+  public String getTag() {
+    return tag;
+  }
+
   @Override
   public boolean equals(Object o) {
     if (o instanceof PatchLineComment) {
@@ -243,7 +273,8 @@
           && Objects.equals(message, c.getMessage())
           && Objects.equals(parentUuid, c.getParentUuid())
           && Objects.equals(range, c.getRange())
-          && Objects.equals(revId, c.getRevId());
+          && Objects.equals(revId, c.getRevId())
+          && Objects.equals(tag, c.getTag());
     }
     return false;
   }
@@ -270,6 +301,7 @@
     builder.append("range=").append(Objects.toString(range, ""))
       .append(',');
     builder.append("revId=").append(revId != null ? revId.get() : "");
+    builder.append("tag=").append(Objects.toString(tag, ""));
     builder.append('}');
     return builder.toString();
   }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java
index c89be30..1d0d29b 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java
@@ -90,6 +90,9 @@
   @Column(id = 3)
   protected Timestamp granted;
 
+  @Column(id = 6, notNull = false)
+  protected String tag;
+
   // DELETED: id = 4 (changeOpen)
   // DELETED: id = 5 (changeSortKey)
 
@@ -145,6 +148,10 @@
     }
   }
 
+  public void setTag(String t) {
+    tag = t;
+  }
+
   public String getLabel() {
     return getLabelId().get();
   }
@@ -153,10 +160,14 @@
     return LabelId.LEGACY_SUBMIT_NAME.equals(getLabel());
   }
 
+  public String getTag() {
+    return tag;
+  }
+
   @Override
   public String toString() {
     return new StringBuilder().append('[').append(key).append(": ")
-        .append(value).append(']').toString();
+        .append(value).append(",tag:").append(tag).append(']').toString();
   }
 
   @Override
@@ -165,13 +176,14 @@
       PatchSetApproval p = (PatchSetApproval) o;
       return Objects.equals(key, p.key)
           && Objects.equals(value, p.value)
-          && Objects.equals(granted, p.granted);
+          && Objects.equals(granted, p.granted)
+          && Objects.equals(tag, p.tag);
     }
     return false;
   }
 
   @Override
   public int hashCode() {
-    return Objects.hash(key, value, granted);
+    return Objects.hash(key, value, granted, tag);
   }
 }
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 af9e75c..86e6894 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
@@ -24,7 +24,7 @@
 public final class Project {
   /** Project name key */
   public static class NameKey extends
-      StringKey<com.google.gwtorm.client.Key<?>>{
+      StringKey<com.google.gwtorm.client.Key<?>> {
     private static final long serialVersionUID = 1L;
 
     @Column(id = 1)
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 d2250cf..d7c546b 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,7 +14,6 @@
 
 package com.google.gerrit.reviewdb.client;
 
-
 /** Constants and utilities for Gerrit-specific ref names. */
 public class RefNames {
   public static final String REFS = "refs/";
@@ -79,6 +78,21 @@
     return ref;
   }
 
+  public static String changeMetaRef(Change.Id id) {
+    StringBuilder r = new StringBuilder();
+    r.append(REFS_CHANGES);
+    int n = id.get();
+    int m = n % 100;
+    if (m < 10) {
+      r.append('0');
+    }
+    r.append(m);
+    r.append('/');
+    r.append(n);
+    r.append(META_SUFFIX);
+    return r.toString();
+  }
+
   public static String refsUsers(Account.Id accountId) {
     StringBuilder r = new StringBuilder();
     r.append(REFS_USERS);
@@ -93,39 +107,38 @@
     return r.toString();
   }
 
-  public static String refsDraftComments(Account.Id accountId,
-      Change.Id changeId) {
-    StringBuilder r = buildRefsPrefix(REFS_DRAFT_COMMENTS, accountId);
-    r.append(changeId.get());
-    return r.toString();
-  }
-
-  public static String refsDraftCommentsPrefix(Account.Id accountId) {
-    return buildRefsPrefix(REFS_DRAFT_COMMENTS, accountId).toString();
-  }
-
-  public static String refsStarredChanges(Account.Id accountId,
-      Change.Id changeId) {
-    StringBuilder r = buildRefsPrefix(REFS_STARRED_CHANGES, accountId);
-    r.append(changeId.get());
-    return r.toString();
-  }
-
-  public static String refsStarredChangesPrefix(Account.Id accountId) {
-    return buildRefsPrefix(REFS_STARRED_CHANGES, accountId).toString();
-  }
-
-  private static StringBuilder buildRefsPrefix(String prefix,
+  public static String refsDraftComments(Change.Id changeId,
       Account.Id accountId) {
+    StringBuilder r = buildRefsPrefix(REFS_DRAFT_COMMENTS, changeId.get());
+    r.append(accountId.get());
+    return r.toString();
+  }
+
+  public static String refsDraftCommentsPrefix(Change.Id changeId) {
+    return buildRefsPrefix(REFS_DRAFT_COMMENTS, changeId.get()).toString();
+  }
+
+  public static String refsStarredChanges(Change.Id changeId,
+      Account.Id accountId) {
+    StringBuilder r = buildRefsPrefix(REFS_STARRED_CHANGES, changeId.get());
+    r.append(accountId.get());
+    return r.toString();
+  }
+
+  public static String refsStarredChangesPrefix(Change.Id changeId) {
+    return buildRefsPrefix(REFS_STARRED_CHANGES, changeId.get()).toString();
+  }
+
+  private static StringBuilder buildRefsPrefix(String prefix, int id) {
     StringBuilder r = new StringBuilder();
     r.append(prefix);
-    int n = accountId.get() % 100;
+    int n = id % 100;
     if (n < 10) {
       r.append('0');
     }
     r.append(n);
     r.append('/');
-    r.append(accountId.get());
+    r.append(id);
     r.append('/');
     return r;
   }
@@ -161,6 +174,69 @@
       .toString();
   }
 
+  static Integer parseShardedRefPart(String name) {
+    if (name == null) {
+      return null;
+    }
+
+    String[] parts = name.split("/");
+    int n = parts.length;
+    if (n < 2) {
+      return null;
+    }
+
+    // Last 2 digits.
+    int le;
+    for (le = 0; le < parts[0].length(); le++) {
+      if (!Character.isDigit(parts[0].charAt(le))) {
+        return null;
+      }
+    }
+    if (le != 2) {
+      return null;
+    }
+
+    // Full ID.
+    int ie;
+    for (ie = 0; ie < parts[1].length(); ie++) {
+      if (!Character.isDigit(parts[1].charAt(ie))) {
+        if (ie == 0) {
+          return null;
+        } else {
+          break;
+        }
+      }
+    }
+
+    int shard = Integer.parseInt(parts[0]);
+    int id = Integer.parseInt(parts[1].substring(0, ie));
+
+    if (id % 100 != shard) {
+      return null;
+    }
+    return id;
+  }
+
+  static Integer parseRefSuffix(String name) {
+    if (name == null) {
+      return null;
+    }
+    int i = name.length();
+    while (i > 0) {
+      char c = name.charAt(i - 1);
+      if (c == '/') {
+        break;
+      } else if (!Character.isDigit(c)) {
+        return null;
+      }
+      i--;
+    }
+    if (i == 0) {
+      return null;
+    }
+    return Integer.valueOf(name.substring(i, name.length()));
+  }
+
   private RefNames() {
   }
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/StarredChange.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/StarredChange.java
deleted file mode 100644
index d427b09..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/StarredChange.java
+++ /dev/null
@@ -1,77 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.reviewdb.client;
-
-import com.google.gwtorm.client.Column;
-import com.google.gwtorm.client.CompoundKey;
-
-/** A {@link Change} starred by an {@link Account}. */
-public class StarredChange {
-  public static class Key extends CompoundKey<Account.Id> {
-    private static final long serialVersionUID = 1L;
-
-    @Column(id = 1)
-    protected Account.Id accountId;
-
-    @Column(id = 2)
-    protected Change.Id changeId;
-
-    protected Key() {
-      accountId = new Account.Id();
-      changeId = new Change.Id();
-    }
-
-    public Key(final Account.Id a, final Change.Id g) {
-      accountId = a;
-      changeId = g;
-    }
-
-    @Override
-    public Account.Id getParentKey() {
-      return accountId;
-    }
-
-    public Change.Id getChangeId() {
-      return changeId;
-    }
-
-    @Override
-    public com.google.gwtorm.client.Key<?>[] members() {
-      return new com.google.gwtorm.client.Key<?>[] {changeId};
-    }
-  }
-
-  @Column(id = 1, name = Column.NONE)
-  protected Key key;
-
-  protected StarredChange() {
-  }
-
-  public StarredChange(final StarredChange.Key k) {
-    key = k;
-  }
-
-  public StarredChange.Key getKey() {
-    return key;
-  }
-
-  public Account.Id getAccountId() {
-    return key.accountId;
-  }
-
-  public Change.Id getChangeId() {
-    return key.changeId;
-  }
-}
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
deleted file mode 100644
index 6f71ba4..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountSshKeyAccess.java
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.reviewdb.server;
-
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountSshKey;
-import com.google.gwtorm.server.Access;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.PrimaryKey;
-import com.google.gwtorm.server.Query;
-import com.google.gwtorm.server.ResultSet;
-
-public interface AccountSshKeyAccess extends
-    Access<AccountSshKey, AccountSshKey.Id> {
-  @Override
-  @PrimaryKey("id")
-  AccountSshKey get(AccountSshKey.Id id) throws OrmException;
-
-  @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/java/com/google/gerrit/reviewdb/server/ReviewDb.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
index 5d073e2..9f62dc2 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
@@ -53,8 +53,7 @@
   @Relation(id = 7)
   AccountExternalIdAccess accountExternalIds();
 
-  @Relation(id = 8)
-  AccountSshKeyAccess accountSshKeys();
+  // Deleted @Relation(id = 8)
 
   @Relation(id = 10)
   AccountGroupAccess accountGroups();
@@ -68,10 +67,9 @@
   @Relation(id = 13)
   AccountGroupMemberAuditAccess accountGroupMembersAudit();
 
-  //Deleted @Relation(id = 17)
+  // Deleted @Relation(id = 17)
 
-  @Relation(id = 18)
-  StarredChangeAccess starredChanges();
+  // Deleted @Relation(id = 18)
 
   @Relation(id = 19)
   AccountProjectWatchAccess accountProjectWatches();
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbUtil.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbUtil.java
index 178b0fd..5d782dd 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbUtil.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbUtil.java
@@ -21,7 +21,7 @@
 
 /** Static utilities for ReviewDb types. */
 public class ReviewDbUtil {
-  private static final Function<IntKey<?>, Integer> INT_KEY_FUNCTION =
+  public static final Function<IntKey<?>, Integer> INT_KEY_FUNCTION =
       new Function<IntKey<?>, Integer>() {
         @Override
         public Integer apply(IntKey<?> in) {
@@ -38,7 +38,7 @@
       };
 
   private static final Ordering<? extends IntKey<?>> INT_KEY_ORDERING =
-      Ordering.natural().onResultOf(INT_KEY_FUNCTION);
+      Ordering.natural().nullsFirst().onResultOf(INT_KEY_FUNCTION).nullsFirst();
 
   @SuppressWarnings("unchecked")
   public static <K extends IntKey<?>> Ordering<K> intKeyOrdering() {
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
index deecba9..aa37974 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
@@ -89,11 +89,6 @@
   }
 
   @Override
-  public AccountSshKeyAccess accountSshKeys() {
-    return delegate.accountSshKeys();
-  }
-
-  @Override
   public AccountGroupAccess accountGroups() {
     return delegate.accountGroups();
   }
@@ -114,11 +109,6 @@
   }
 
   @Override
-  public StarredChangeAccess starredChanges() {
-    return delegate.starredChanges();
-  }
-
-  @Override
   public AccountProjectWatchAccess accountProjectWatches() {
     return delegate.accountProjectWatches();
   }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/StarredChangeAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/StarredChangeAccess.java
deleted file mode 100644
index 5f57fe7..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/StarredChangeAccess.java
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.reviewdb.server;
-
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.StarredChange;
-import com.google.gwtorm.server.Access;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.PrimaryKey;
-import com.google.gwtorm.server.Query;
-import com.google.gwtorm.server.ResultSet;
-
-public interface StarredChangeAccess extends
-    Access<StarredChange, StarredChange.Key> {
-  @Override
-  @PrimaryKey("key")
-  StarredChange get(StarredChange.Key key) throws OrmException;
-
-  @Query("WHERE key.accountId = ?")
-  ResultSet<StarredChange> byAccount(Account.Id id) throws OrmException;
-
-  @Query("WHERE key.changeId = ?")
-  ResultSet<StarredChange> byChange(Change.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 b8ebdde..2110295 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
@@ -48,11 +48,6 @@
 
 
 -- *********************************************************************
--- AccountSshKeyAccess
---    @PrimaryKey covers: byAccount, valid
-
-
--- *********************************************************************
 -- ApprovalCategoryAccess
 --    too small to bother indexing
 
@@ -86,10 +81,3 @@
 -- PatchSetAccess
 CREATE INDEX patch_sets_byRevision
 ON patch_sets (revision);
-
--- *********************************************************************
--- StarredChangeAccess
---    @PrimaryKey covers: byAccount
-
-CREATE INDEX starred_changes_byChange
-ON starred_changes (change_id);
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
index d7135a2..334b6c4 100644
--- 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
@@ -55,11 +55,6 @@
 #
 
 -- *********************************************************************
--- AccountSshKeyAccess
---    @PrimaryKey covers: byAccount, valid
-
-
--- *********************************************************************
 -- ApprovalCategoryAccess
 --    too small to bother indexing
 
@@ -95,11 +90,3 @@
 CREATE INDEX patch_sets_byRevision
 ON patch_sets (revision)
 #
-
--- *********************************************************************
--- StarredChangeAccess
---    @PrimaryKey covers: byAccount
-
-CREATE INDEX starred_changes_byChange
-ON starred_changes (change_id)
-#
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 b0b6b6f..d723667 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
@@ -96,11 +96,6 @@
 
 
 -- *********************************************************************
--- AccountSshKeyAccess
---    @PrimaryKey covers: byAccount, valid
-
-
--- *********************************************************************
 -- ApprovalCategoryAccess
 --    too small to bother indexing
 
@@ -135,11 +130,3 @@
 -- PatchSetAccess
 CREATE INDEX patch_sets_byRevision
 ON patch_sets (revision);
-
--- *********************************************************************
--- StarredChangeAccess
---    @PrimaryKey covers: byAccount
-
-CREATE INDEX starred_changes_byChange
-ON starred_changes (change_id);
-
diff --git a/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/AccountSshKeyTest.java b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/AccountSshKeyTest.java
new file mode 100644
index 0000000..139d360
--- /dev/null
+++ b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/AccountSshKeyTest.java
@@ -0,0 +1,50 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.reviewdb.client;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class AccountSshKeyTest {
+  private static final String KEY =
+      "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCgug5VyMXQGnem2H1KVC4/HcRcD4zzBqS"
+      + "uJBRWVonSSoz3RoAZ7bWXCVVGwchtXwUURD689wFYdiPecOrWOUgeeyRq754YWRhU+W28"
+      + "vf8IZixgjCmiBhaL2gt3wff6pP+NXJpTSA4aeWE5DfNK5tZlxlSxqkKOS8JRSUeNQov5T"
+      + "w== john.doe@example.com";
+
+  private final Account.Id accountId = new Account.Id(1);
+
+  @Test
+  public void testValidity() throws Exception {
+    AccountSshKey key = new AccountSshKey(
+        new AccountSshKey.Id(accountId, -1), KEY);
+    assertThat(key.isValid()).isFalse();
+    key = new AccountSshKey(new AccountSshKey.Id(accountId, 0), KEY);
+    assertThat(key.isValid()).isFalse();
+    key = new AccountSshKey(new AccountSshKey.Id(accountId, 1), KEY);
+    assertThat(key.isValid()).isTrue();
+  }
+
+  @Test
+  public void testGetters() throws Exception {
+    AccountSshKey key = new AccountSshKey(
+        new AccountSshKey.Id(accountId, 1), KEY);
+    assertThat(key.getSshPublicKey()).isEqualTo(KEY);
+    assertThat(key.getAlgorithm()).isEqualTo(KEY.split(" ")[0]);
+    assertThat(key.getEncodedKey()).isEqualTo(KEY.split(" ")[1]);
+    assertThat(key.getComment()).isEqualTo(KEY.split(" ")[2]);
+  }
+}
diff --git a/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/AccountTest.java b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/AccountTest.java
index e8dc5e0..00bf44e 100644
--- a/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/AccountTest.java
+++ b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/AccountTest.java
@@ -14,69 +14,48 @@
 
 package com.google.gerrit.reviewdb.client;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.reviewdb.client.Account.Id.fromRef;
+import static com.google.gerrit.reviewdb.client.Account.Id.fromRefPart;
+import static com.google.gerrit.reviewdb.client.Account.Id.fromRefSuffix;
 
 import org.junit.Test;
 
 public class AccountTest {
   @Test
   public void parseRefName() {
-    assertRef(1, "refs/users/01/1");
-    assertRef(1, "refs/users/01/1-drafts");
-    assertRef(1, "refs/users/01/1-drafts/2");
-    assertRef(1, "refs/users/01/1/edit/2");
+    assertThat(fromRef("refs/users/01/1")).isEqualTo(id(1));
+    assertThat(fromRef("refs/users/01/1-drafts")).isEqualTo(id(1));
+    assertThat(fromRef("refs/users/01/1-drafts/2")).isEqualTo(id(1));
+    assertThat(fromRef("refs/users/01/1/edit/2")).isEqualTo(id(1));
 
-    assertNotRef(null);
-    assertNotRef("");
+    assertThat(fromRef(null)).isNull();
+    assertThat(fromRef("")).isNull();
 
     // Invalid characters.
-    assertNotRef("refs/users/01a/1");
-    assertNotRef("refs/users/01/a1");
+    assertThat(fromRef("refs/users/01a/1")).isNull();
+    assertThat(fromRef("refs/users/01/a1")).isNull();
 
     // Mismatched shard.
-    assertNotRef("refs/users/01/23");
+    assertThat(fromRef("refs/users/01/23")).isNull();
 
     // Shard too short.
-    assertNotRef("refs/users/1/1");
+    assertThat(fromRef("refs/users/1/1")).isNull();
   }
 
   @Test
   public void parseRefNameParts() {
-    assertRefPart(1, "01/1");
-    assertRefPart(1, "01/1-drafts");
-    assertRefPart(1, "01/1-drafts/2");
-
-    assertNotRefPart(null);
-    assertNotRefPart("");
-
-    // This method assumes that the common prefix "refs/users/" will be removed.
-    assertNotRefPart("refs/users/01/1");
-
-    // Invalid characters.
-    assertNotRefPart("01a/1");
-    assertNotRefPart("01/a1");
-
-    // Mismatched shard.
-    assertNotRefPart("01/23");
-
-    // Shard too short.
-    assertNotRefPart("1/1");
+    assertThat(fromRefPart("01/1")).isEqualTo(id(1));
+    assertThat(fromRefPart("ab/cd")).isNull();
   }
 
-  private static void assertRef(int accountId, String refName) {
-    assertEquals(new Account.Id(accountId), Account.Id.fromRef(refName));
+  @Test
+  public void parseRefSuffix() {
+    assertThat(fromRefSuffix("12/34")).isEqualTo(id(34));
+    assertThat(fromRefSuffix("ab/cd")).isNull();
   }
 
-  private static void assertNotRef(String refName) {
-    assertNull(Account.Id.fromRef(refName));
-  }
-
-  private static void assertRefPart(int accountId, String refName) {
-    assertEquals(new Account.Id(accountId), Account.Id.fromRefPart(refName));
-  }
-
-  private static void assertNotRefPart(String refName) {
-    assertNull(Account.Id.fromRefPart(refName));
+  private Account.Id id(int n) {
+    return new Account.Id(n);
   }
 }
diff --git a/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/ChangeTest.java b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/ChangeTest.java
index 47f409a..cf2d289 100644
--- a/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/ChangeTest.java
+++ b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/ChangeTest.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.reviewdb.client;
 
 import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
 
 import org.junit.Test;
 
@@ -79,6 +81,27 @@
         .isEqualTo("refs/changes/34/1234/");
   }
 
+  @Test
+  public void parseRefNameParts() {
+    assertRefPart(1, "01/1");
+
+    assertNotRefPart(null);
+    assertNotRefPart("");
+
+    // This method assumes that the common prefix "refs/changes/" was removed.
+    assertNotRefPart("refs/changes/01/1");
+
+    // Invalid characters.
+    assertNotRefPart("01a/1");
+    assertNotRefPart("01/a1");
+
+    // Mismatched shard.
+    assertNotRefPart("01/23");
+
+    // Shard too short.
+    assertNotRefPart("1/1");
+  }
+
   private static void assertRef(int changeId, String refName) {
     assertThat(Change.Id.fromRef(refName)).isEqualTo(new Change.Id(changeId));
   }
@@ -86,4 +109,12 @@
   private static void assertNotRef(String refName) {
     assertThat(Change.Id.fromRef(refName)).isNull();
   }
+
+  private static void assertRefPart(int changeId, String refName) {
+    assertEquals(new Change.Id(changeId), Change.Id.fromRefPart(refName));
+  }
+
+  private static void assertNotRefPart(String refName) {
+    assertNull(Change.Id.fromRefPart(refName));
+  }
 }
diff --git a/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/RefNamesTest.java b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/RefNamesTest.java
index 844f893..946b76c 100644
--- a/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/RefNamesTest.java
+++ b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/RefNamesTest.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.reviewdb.client;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.reviewdb.client.RefNames.parseRefSuffix;
+import static com.google.gerrit.reviewdb.client.RefNames.parseShardedRefPart;
 
 import org.junit.Test;
 
@@ -38,26 +40,26 @@
 
   @Test
   public void refsDraftComments() throws Exception {
-    assertThat(RefNames.refsDraftComments(accountId, changeId))
-      .isEqualTo("refs/draft-comments/23/1011123/67473");
+    assertThat(RefNames.refsDraftComments(changeId, accountId))
+      .isEqualTo("refs/draft-comments/73/67473/1011123");
   }
 
   @Test
   public void refsDraftCommentsPrefix() throws Exception {
-    assertThat(RefNames.refsDraftCommentsPrefix(accountId))
-      .isEqualTo("refs/draft-comments/23/1011123/");
+    assertThat(RefNames.refsDraftCommentsPrefix(changeId))
+      .isEqualTo("refs/draft-comments/73/67473/");
   }
 
   @Test
   public void refsStarredChanges() throws Exception {
-    assertThat(RefNames.refsStarredChanges(accountId, changeId))
-      .isEqualTo("refs/starred-changes/23/1011123/67473");
+    assertThat(RefNames.refsStarredChanges(changeId, accountId))
+      .isEqualTo("refs/starred-changes/73/67473/1011123");
   }
 
   @Test
   public void refsStarredChangesPrefix() throws Exception {
-    assertThat(RefNames.refsStarredChangesPrefix(accountId))
-      .isEqualTo("refs/starred-changes/23/1011123/");
+    assertThat(RefNames.refsStarredChangesPrefix(changeId))
+      .isEqualTo("refs/starred-changes/73/67473/");
   }
 
   @Test
@@ -65,4 +67,42 @@
     assertThat(RefNames.refsEdit(accountId, changeId, psId))
       .isEqualTo("refs/users/23/1011123/edit-67473/42");
   }
+
+  @Test
+  public void testParseShardedRefsPart() throws Exception {
+    assertThat(parseShardedRefPart("01/1")).isEqualTo(1);
+    assertThat(parseShardedRefPart("01/1-drafts")).isEqualTo(1);
+    assertThat(parseShardedRefPart("01/1-drafts/2")).isEqualTo(1);
+
+    assertThat(parseShardedRefPart(null)).isNull();
+    assertThat(parseShardedRefPart("")).isNull();
+
+    // Prefix not stripped.
+    assertThat(parseShardedRefPart("refs/users/01/1")).isNull();
+
+    // Invalid characters.
+    assertThat(parseShardedRefPart("01a/1")).isNull();
+    assertThat(parseShardedRefPart("01/a1")).isNull();
+
+    // Mismatched shard.
+    assertThat(parseShardedRefPart("01/23")).isNull();
+
+    // Shard too short.
+    assertThat(parseShardedRefPart("1/1")).isNull();
+  }
+
+  @Test
+  public void testParseRefSuffix() throws Exception {
+    assertThat(parseRefSuffix("1/2/34")).isEqualTo(34);
+    assertThat(parseRefSuffix("/34")).isEqualTo(34);
+
+    assertThat(parseRefSuffix(null)).isNull();
+    assertThat(parseRefSuffix("")).isNull();
+    assertThat(parseRefSuffix("34")).isNull();
+    assertThat(parseRefSuffix("12/ab")).isNull();
+    assertThat(parseRefSuffix("12/a4")).isNull();
+    assertThat(parseRefSuffix("12/4a")).isNull();
+    assertThat(parseRefSuffix("a4")).isNull();
+    assertThat(parseRefSuffix("4a")).isNull();
+  }
 }
diff --git a/gerrit-server/BUCK b/gerrit-server/BUCK
index 9f66438..6aa9e2c 100644
--- a/gerrit-server/BUCK
+++ b/gerrit-server/BUCK
@@ -34,6 +34,7 @@
     '//gerrit-util-ssl:ssl',
     '//lib:args4j',
     '//lib:automaton',
+    '//lib:blame-cache',
     '//lib:grappa',
     '//lib:gson',
     '//lib:guava',
@@ -59,6 +60,8 @@
     '//lib/guice:guice',
     '//lib/guice:guice-assistedinject',
     '//lib/guice:guice-servlet',
+    '//lib/jgit/org.eclipse.jgit:jgit',
+    '//lib/jgit/org.eclipse.jgit.archive:jgit-archive',
     '//lib/joda:joda-time',
     '//lib/log:api',
     '//lib/log:jsonevent-layout',
@@ -70,8 +73,6 @@
     '//lib/ow2:ow2-asm-tree',
     '//lib/ow2:ow2-asm-util',
     '//lib/prolog:runtime',
-    '@jgit//org.eclipse.jgit:jgit',
-    '@jgit//org.eclipse.jgit.archive:jgit-archive',
   ],
   provided_deps = [
     '//lib:servlet-api-3_1',
@@ -98,12 +99,12 @@
   '//lib:truth',
   '//lib/guice:guice',
   '//lib/guice:guice-servlet',
+  '//lib/jgit/org.eclipse.jgit:jgit',
+  '//lib/jgit/org.eclipse.jgit.junit:junit',
   '//lib/joda:joda-time',
   '//lib/log:api',
   '//lib/log:impl_log4j',
   '//lib/log:log4j',
-  '@jgit//org.eclipse.jgit:jgit',
-  '@jgit//org.eclipse.jgit.junit:junit',
 ]
 
 TESTUTIL = glob([
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 b8d609d..458c99a 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
@@ -49,11 +49,11 @@
 import com.google.gerrit.server.events.DraftPublishedEvent;
 import com.google.gerrit.server.events.EventFactory;
 import com.google.gerrit.server.events.HashtagsChangedEvent;
-import com.google.gerrit.server.events.MergeFailedEvent;
 import com.google.gerrit.server.events.PatchSetCreatedEvent;
 import com.google.gerrit.server.events.ProjectCreatedEvent;
 import com.google.gerrit.server.events.RefUpdatedEvent;
 import com.google.gerrit.server.events.ReviewerAddedEvent;
+import com.google.gerrit.server.events.ReviewerDeletedEvent;
 import com.google.gerrit.server.events.TopicChangedEvent;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.WorkQueue;
@@ -168,9 +168,6 @@
     /** Path of the change merged hook. */
     private final Optional<Path> changeMergedHook;
 
-    /** Path of the merge failed hook. */
-    private final Optional<Path> mergeFailedHook;
-
     /** Path of the change abandoned hook. */
     private final Optional<Path> changeAbandonedHook;
 
@@ -183,6 +180,9 @@
     /** Path of the reviewer added hook. */
     private final Optional<Path> reviewerAddedHook;
 
+    /** Path of the reviewer deleted hook. */
+    private final Optional<Path> reviewerDeletedHook;
+
     /** Path of the topic changed hook. */
     private final Optional<Path> topicChangedHook;
 
@@ -264,11 +264,11 @@
         draftPublishedHook = hook(config, hooksPath, "draft-published");
         commentAddedHook = hook(config, hooksPath, "comment-added");
         changeMergedHook = hook(config, hooksPath, "change-merged");
-        mergeFailedHook = hook(config, hooksPath, "merge-failed");
         changeAbandonedHook = hook(config, hooksPath, "change-abandoned");
         changeRestoredHook = hook(config, hooksPath, "change-restored");
         refUpdatedHook = hook(config, hooksPath, "ref-updated");
         reviewerAddedHook = hook(config, hooksPath, "reviewer-added");
+        reviewerDeletedHook = hook(config, hooksPath, "reviewer-deleted");
         topicChangedHook = hook(config, hooksPath, "topic-changed");
         claSignedHook = hook(config, hooksPath, "cla-signed");
         refUpdateHook = hook(config, hooksPath, "ref-update");
@@ -524,41 +524,6 @@
     }
 
     @Override
-    public void doMergeFailedHook(Change change, Account account,
-          PatchSet patchSet, String reason,
-          ReviewDb db) throws OrmException {
-      MergeFailedEvent event = new MergeFailedEvent(change);
-      Supplier<AccountState> owner = getAccountSupplier(change.getOwner());
-
-      event.change = changeAttributeSupplier(change);
-      event.submitter = accountAttributeSupplier(account);
-      event.patchSet = patchSetAttributeSupplier(change, patchSet);
-      event.reason = reason;
-
-      dispatcher.get().postEvent(change, event, db);
-
-      if (!mergeFailedHook.isPresent()) {
-        return;
-      }
-
-      List<String> args = new ArrayList<>();
-      ChangeAttribute c = event.change.get();
-      PatchSetAttribute ps = event.patchSet.get();
-
-      addArg(args, "--change", c.id);
-      addArg(args, "--change-url", c.url);
-      addArg(args, "--change-owner", getDisplayName(owner.get().getAccount()));
-      addArg(args, "--project", c.project);
-      addArg(args, "--branch", c.branch);
-      addArg(args, "--topic", c.topic);
-      addArg(args, "--submitter", getDisplayName(account));
-      addArg(args, "--commit", ps.revision);
-      addArg(args, "--reason",  reason == null ? "" : reason);
-
-      runHook(change.getProject(), mergeFailedHook, args);
-    }
-
-    @Override
     public void doChangeAbandonedHook(Change change, Account account,
           PatchSet patchSet, String reason, ReviewDb db)
           throws OrmException {
@@ -700,6 +665,70 @@
     }
 
     @Override
+    public void doReviewerDeletedHook(final Change change, Account account,
+      PatchSet patchSet, String comment, final Map<String, Short> approvals,
+      final Map<String, Short> oldApprovals, ReviewDb db) throws OrmException {
+
+      ReviewerDeletedEvent event = new ReviewerDeletedEvent(change);
+      event.change = changeAttributeSupplier(change);
+      event.patchSet = patchSetAttributeSupplier(change, patchSet);
+      event.reviewer = accountAttributeSupplier(account);
+      event.comment = comment;
+      event.approvals = Suppliers.memoize(
+          new Supplier<ApprovalAttribute[]>() {
+            @Override
+            public ApprovalAttribute[] get() {
+              LabelTypes labelTypes = projectCache.get(
+                  change.getProject()).getLabelTypes();
+              if (!approvals.isEmpty()) {
+                ApprovalAttribute[] r = new ApprovalAttribute[approvals.size()];
+                int i = 0;
+                for (Map.Entry<String, Short> approval : approvals.entrySet()) {
+                  r[i++] = getApprovalAttribute(labelTypes, approval,
+                      oldApprovals);
+                }
+                return r;
+              }
+              return null;
+            }
+          });
+
+      dispatcher.get().postEvent(change, event, db);
+
+      if (!reviewerDeletedHook.isPresent()) {
+        return;
+      }
+
+      List<String> args = new ArrayList<>();
+      ChangeAttribute c = event.change.get();
+      AccountState owner = accountCache.get(change.getOwner());
+
+      addArg(args, "--change", c.id);
+      addArg(args, "--change-url", c.url);
+      addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
+      addArg(args, "--project", c.project);
+      addArg(args, "--branch", c.branch);
+      addArg(args, "--reviewer", getDisplayName(account));
+      LabelTypes labelTypes = projectCache.get(
+          change.getProject()).getLabelTypes();
+      // append votes that were removed
+      for (Map.Entry<String, Short> approval : approvals.entrySet()) {
+        LabelType lt = labelTypes.byLabel(approval.getKey());
+        if (lt != null && approval.getValue() != null) {
+          addArg(args, "--" + lt.getName(), Short.toString(approval.getValue()));
+          if (oldApprovals != null && !oldApprovals.isEmpty()) {
+            Short oldValue = oldApprovals.get(approval.getKey());
+            if (oldValue != null) {
+              addArg(args, "--" + lt.getName() + "-oldValue",
+                  Short.toString(oldValue));
+            }
+          }
+        }
+      }
+      runHook(change.getProject(), reviewerDeletedHook, args);
+    }
+
+    @Override
     public void doTopicChangedHook(Change change, Account account,
         String oldTopic, ReviewDb db)
             throws OrmException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
index ee1de65..a7e3583 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
@@ -85,19 +85,6 @@
       PatchSet patchSet, ReviewDb db, String mergeResultRev) throws OrmException;
 
   /**
-   * Fire the Merge Failed Hook.
-   *
-   * @param change The change itself.
-   * @param account The gerrit user who attempted to submit the change.
-   * @param patchSet The patchset that failed to merge.
-   * @param reason The reason that the change failed to merge.
-   * @param db The review database.
-   * @throws OrmException
-   */
-  void doMergeFailedHook(Change change, Account account,
-      PatchSet patchSet, String reason, ReviewDb db) throws OrmException;
-
-  /**
    * Fire the Change Abandoned Hook.
    *
    * @param change The change itself.
@@ -155,7 +142,23 @@
       PatchSet patchSet, ReviewDb db) throws OrmException;
 
   /**
-   * Fire the Topic Changed Hook.
+   * Fire the Reviewer Deleted Hook
+   *
+   * @param change The change itself.
+   * @param account The reviewer that was removed.
+   * @param patchSet The patchset that the reviewer was removed from.
+   * @param comment The comment given.
+   * @param approvals Map of label IDs to scores.
+   * @param oldApprovals Map of label IDs to old approval scores
+   * @param db The review database.
+   * @throws OrmException
+   */
+  void doReviewerDeletedHook(Change change, Account account, PatchSet patchSet,
+      String comment, Map<String, Short> approvals,
+      Map<String, Short> oldApprovals, ReviewDb db) throws OrmException;
+
+  /**
+   * Fire the Topic Changed Hook
    *
    * @param change The change itself.
    * @param account The gerrit user who changed the topic.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java b/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
index bec0b7e..2b44946 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
@@ -45,11 +45,6 @@
   }
 
   @Override
-  public void doMergeFailedHook(Change change, Account account,
-      PatchSet patchSet, String reason, ReviewDb db) {
-  }
-
-  @Override
   public void doChangeRestoredHook(Change change, Account account,
       PatchSet patchSet, String reason, ReviewDb db) {
   }
@@ -91,6 +86,12 @@
   }
 
   @Override
+  public void doReviewerDeletedHook(Change change, Account account,
+      PatchSet patchSet, String comment, Map<String, Short> approvals,
+      Map<String, Short> oldApprovals, ReviewDb db) {
+  }
+
+  @Override
   public void doTopicChangedHook(Change change, Account account, String oldTopic,
       ReviewDb db) {
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/Description.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/Description.java
index 43e5c8a..48358b7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/metrics/Description.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/Description.java
@@ -44,7 +44,7 @@
     }
   }
 
-  public static enum FieldOrdering {
+  public enum FieldOrdering {
     /** Default ordering places fields at end of the parent metric name. */
     AT_END,
 
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 26c0fd0..de54a0b 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
@@ -54,7 +54,7 @@
   private static final Logger log =
       LoggerFactory.getLogger(PrologEnvironment.class);
 
-  public static interface Factory {
+  public interface Factory {
     /**
      * Construct a new Prolog interpreter.
      *
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 f6df335a..068b70d 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
@@ -18,7 +18,6 @@
 
 import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
@@ -61,6 +60,7 @@
 import java.net.URLClassLoader;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.ArrayList;
 import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.List;
@@ -283,7 +283,7 @@
         predicateProviders, cl)));
     ctl.setEnabled(EnumSet.allOf(Prolog.Feature.class), false);
 
-    List<String> packages = Lists.newArrayList();
+    List<String> packages = new ArrayList<>();
     packages.addAll(PACKAGE_LIST);
     for (PredicateProvider predicateProvider : predicateProviders) {
       packages.addAll(predicateProvider.getPackages());
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 27f15e6..206e840 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
@@ -93,4 +93,4 @@
   protected T createValue(Prolog engine) {
     return null;
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
index bc301e3..9034e47 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
@@ -16,7 +16,6 @@
 
 import static com.google.gerrit.rules.StoredValue.create;
 
-import com.google.common.collect.Maps;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
@@ -45,6 +44,7 @@
 import org.eclipse.jgit.lib.Repository;
 
 import java.io.IOException;
+import java.util.HashMap;
 import java.util.Map;
 
 public final class StoredValues {
@@ -150,7 +150,7 @@
       new StoredValue<Map<Account.Id, IdentifiedUser>>() {
         @Override
         protected Map<Account.Id, IdentifiedUser> createValue(Prolog engine) {
-          return Maps.newHashMap();
+          return new HashMap<>();
         }
       };
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/AnonymousUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/AnonymousUser.java
index ff09df5..e916aff 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/AnonymousUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/AnonymousUser.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server;
 
-import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.GroupMembership;
@@ -22,7 +21,6 @@
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.inject.Inject;
 
-import java.util.Collection;
 import java.util.Collections;
 import java.util.Set;
 
@@ -44,11 +42,6 @@
   }
 
   @Override
-  public Collection<AccountProjectWatch> getNotificationFilters() {
-    return Collections.emptySet();
-  }
-
-  @Override
   public String toString() {
     return "ANONYMOUS";
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
index dba723f..f4aa4c2 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
@@ -19,7 +19,6 @@
 
 import com.google.common.collect.HashBasedTable;
 import com.google.common.collect.ListMultimap;
-import com.google.common.collect.Maps;
 import com.google.common.collect.Table;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.reviewdb.client.Account;
@@ -148,7 +147,7 @@
   private static TreeMap<Integer, PatchSet> getPatchSets(ChangeData cd)
       throws OrmException {
     Collection<PatchSet> patchSets = cd.patchSets();
-    TreeMap<Integer, PatchSet> result = Maps.newTreeMap();
+    TreeMap<Integer, PatchSet> result = new TreeMap<>();
     for (PatchSet ps : patchSets) {
       result.put(ps.getId().get(), ps);
     }
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
index af953e0..91647ff 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeMessagesUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeMessagesUtil.java
@@ -76,6 +76,7 @@
         "cannot store change message by %s in update by %s",
         changeMessage.getAuthor(), update.getAccountId());
     update.setChangeMessage(changeMessage.getMessage());
+    update.setTag(changeMessage.getTag());
     db.changeMessages().insert(Collections.singleton(changeMessage));
   }
 }
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 b694cd8..11a3d81 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
@@ -16,7 +16,6 @@
 
 import com.google.common.base.Function;
 import com.google.common.collect.Ordering;
-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.util.IdGenerator;
@@ -28,7 +27,6 @@
 import org.eclipse.jgit.lib.Repository;
 
 import java.io.IOException;
-import java.util.Collections;
 import java.util.Map;
 
 @Singleton
@@ -77,15 +75,6 @@
     return u + '_' + l;
   }
 
-  public static void bumpRowVersionNotLastUpdatedOn(Change.Id id, ReviewDb db)
-      throws OrmException {
-    // Empty update of Change to bump rowVersion, changing its ETag.
-    Change c = db.changes().get(id);
-    if (c != null) {
-      db.changes().update(Collections.singleton(c));
-    }
-  }
-
   public static PatchSet.Id nextPatchSetId(Map<String, Ref> allRefs,
       PatchSet.Id id) {
     PatchSet.Id next = nextPatchSetId(id);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
index 6a8600f..16e868f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
@@ -14,14 +14,13 @@
 
 package com.google.gerrit.server;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.GroupMembership;
 import com.google.inject.servlet.RequestScoped;
 
-import java.util.Collection;
 import java.util.Set;
 
 /**
@@ -33,6 +32,16 @@
  * @see IdentifiedUser
  */
 public abstract class CurrentUser {
+  /** Unique key for plugin/extension specific data on a CurrentUser. */
+  public static final class PropertyKey<T> {
+    public static <T> PropertyKey<T> create() {
+      return new PropertyKey<>();
+    }
+
+    private PropertyKey() {
+    }
+  }
+
   private final CapabilityControl.Factory capabilityControlFactory;
   private AccessPath accessPath = AccessPath.UNKNOWN;
 
@@ -79,11 +88,9 @@
   public abstract GroupMembership getEffectiveGroups();
 
   /** Set of changes starred by this user. */
+  @Deprecated
   public abstract Set<Change.Id> getStarredChanges();
 
-  /** Filters selecting changes the user wants to monitor. */
-  public abstract Collection<AccountProjectWatch> getNotificationFilters();
-
   /** Unique name of the user on this server, if one has been assigned. */
   public String getUserName() {
     return null;
@@ -118,4 +125,24 @@
   public boolean isInternalUser() {
     return false;
   }
+
+  /**
+   * Lookup a previously stored property.
+   *
+   * @param key unique property key.
+   * @return previously stored value, or {@code null}.
+   */
+  @Nullable
+  public <T> T get(PropertyKey<T> key) {
+    return null;
+  }
+
+  /**
+   * Store a property for later retrieval.
+   *
+   * @param key unique property key.
+   * @param value value to store; or {@code null} to clear the value.
+   */
+  public <T> void put(PropertyKey<T> key, @Nullable T value) {
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
index 50b20f0..0493df3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
@@ -19,7 +19,6 @@
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.AccountCache;
@@ -34,28 +33,23 @@
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.DisableReverseDnsLookup;
 import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
-import com.google.inject.OutOfScopeException;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import com.google.inject.util.Providers;
 
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.util.SystemReader;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.MalformedURLException;
 import java.net.SocketAddress;
 import java.net.URL;
-import java.util.Collection;
-import java.util.Collections;
 import java.util.Date;
-import java.util.List;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.Set;
 import java.util.TimeZone;
 
@@ -146,7 +140,7 @@
     @Inject
     RequestFactory(
         CapabilityControl.Factory capabilityControlFactory,
-        StarredChangesUtil starredChangesUtil,
+        @Nullable StarredChangesUtil starredChangesUtil,
         final AuthConfig authConfig,
         Realm realm,
         @AnonymousCowardName final String anonymousCowardName,
@@ -184,9 +178,6 @@
     }
   }
 
-  private static final Logger log =
-      LoggerFactory.getLogger(IdentifiedUser.class);
-
   private static final GroupMembership registeredGroups =
       new ListGroupMembership(ImmutableSet.of(
           SystemGroupBackend.ANONYMOUS_USERS,
@@ -219,8 +210,8 @@
   private GroupMembership effectiveGroups;
   private Set<Change.Id> starredChanges;
   private ResultSet<Change.Id> starredQuery;
-  private Collection<AccountProjectWatch> notificationFilters;
   private CurrentUser realUser;
+  private Map<PropertyKey<Object>, Object> properties;
 
   private IdentifiedUser(
       CapabilityControl.Factory capabilityControlFactory,
@@ -256,7 +247,6 @@
     return realUser;
   }
 
-  // TODO(cranger): maybe get the state through the accountCache instead.
   public AccountState state() {
     if (state == null) {
       state = accountCache.get(getAccountId());
@@ -327,6 +317,7 @@
     return effectiveGroups;
   }
 
+  @SuppressWarnings("deprecation")
   @Override
   public Set<Change.Id> getStarredChanges() {
     if (starredChanges == null) {
@@ -338,7 +329,7 @@
             FluentIterable.from(
               starredQuery != null
               ? starredQuery
-              : starredChangesUtil.query(accountId))
+              : starredChangesUtil.queryFromIndex(accountId))
             .toSet();
       } finally {
         starredQuery = null;
@@ -347,6 +338,7 @@
     return starredChanges;
   }
 
+  @Deprecated
   public void clearStarredChanges() {
     // Async query may have started before an update that the caller expects
     // to see the results of, so we can't trust it.
@@ -354,12 +346,14 @@
     starredChanges = null;
   }
 
+  @Deprecated
   public void asyncStarredChanges() {
     if (starredChanges == null && starredChangesUtil != null) {
-      starredQuery = starredChangesUtil.query(accountId);
+      starredQuery = starredChangesUtil.queryFromIndex(accountId);
     }
   }
 
+  @Deprecated
   public void abortStarredChanges() {
     if (starredQuery != null) {
       try {
@@ -370,29 +364,6 @@
     }
   }
 
-  private void checkRequestScope() {
-    if (dbProvider == null) {
-      throw new OutOfScopeException("Not in request scoped user");
-    }
-  }
-
-  @Override
-  public Collection<AccountProjectWatch> getNotificationFilters() {
-    if (notificationFilters == null) {
-      checkRequestScope();
-      List<AccountProjectWatch> r;
-      try {
-        r = dbProvider.get().accountProjectWatches() //
-            .byAccount(getAccountId()).toList();
-      } catch (OrmException e) {
-        log.warn("Cannot query notification filters of a user", e);
-        r = Collections.emptyList();
-      }
-      notificationFilters = Collections.unmodifiableList(r);
-    }
-    return notificationFilters;
-  }
-
   public PersonIdent newRefLogIdent() {
     return newRefLogIdent(new Date(), TimeZone.getDefault());
   }
@@ -483,6 +454,41 @@
     return true;
   }
 
+  @Override
+  @Nullable
+  public synchronized <T> T get(PropertyKey<T> key) {
+    if (properties != null) {
+      @SuppressWarnings("unchecked")
+      T value = (T) properties.get(key);
+      return value;
+    }
+    return null;
+  }
+
+  /**
+   * Store a property for later retrieval.
+   *
+   * @param key unique property key.
+   * @param value value to store; or {@code null} to clear the value.
+   */
+  @Override
+  public synchronized <T> void put(PropertyKey<T> key, @Nullable T value) {
+    if (properties == null) {
+      if (value == null) {
+        return;
+      }
+      properties = new HashMap<>();
+    }
+
+    @SuppressWarnings("unchecked")
+    PropertyKey<Object> k = (PropertyKey<Object>) key;
+    if (value != null) {
+      properties.put(k, value);
+    } else {
+      properties.remove(k);
+    }
+  }
+
   private String getHost(final InetAddress in) {
     if (Boolean.FALSE.equals(disableReverseDnsLookup)) {
       return in.getCanonicalHostName();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java
index d0c2dc0..3c63bf8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java
@@ -15,13 +15,11 @@
 package com.google.gerrit.server;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.GroupMembership;
 import com.google.inject.Inject;
 
-import java.util.Collection;
 import java.util.Collections;
 import java.util.Set;
 
@@ -57,11 +55,6 @@
   }
 
   @Override
-  public Collection<AccountProjectWatch> getNotificationFilters() {
-    return Collections.emptySet();
-  }
-
-  @Override
   public boolean isInternalUser() {
     return true;
   }
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
index 3423e0b..d990115 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/PatchLineCommentsUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/PatchLineCommentsUtil.java
@@ -23,7 +23,6 @@
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
 import com.google.common.collect.Ordering;
 import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.extensions.common.CommentInfo;
@@ -52,7 +51,6 @@
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefDatabase;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.ReceiveCommand;
@@ -62,8 +60,6 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
-import java.util.Set;
 
 /**
  * Utility functions to manipulate PatchLineComments.
@@ -156,7 +152,7 @@
     }
 
     notes.load();
-    List<PatchLineComment> comments = Lists.newArrayList();
+    List<PatchLineComment> comments = new ArrayList<>();
     comments.addAll(notes.getComments().values());
     return sort(comments);
   }
@@ -168,9 +164,9 @@
           db.patchComments().byChange(notes.getChangeId()), Status.DRAFT));
     }
 
-    List<PatchLineComment> comments = Lists.newArrayList();
-    for (String refSuffix : getDraftRefs(notes.getChangeId()).keySet()) {
-      Account.Id account = Account.Id.fromRefPart(refSuffix);
+    List<PatchLineComment> comments = new ArrayList<>();
+    for (Ref ref : getDraftRefs(notes.getChangeId())) {
+      Account.Id account = Account.Id.fromRefSuffix(ref.getName());
       if (account != null) {
         comments.addAll(draftByChangeAuthor(db, notes, account));
       }
@@ -196,11 +192,11 @@
     if (!migration.readChanges()) {
       return sort(db.patchComments().byPatchSet(psId).toList());
     }
-    List<PatchLineComment> comments = Lists.newArrayList();
+    List<PatchLineComment> comments = new ArrayList<>();
     comments.addAll(publishedByPatchSet(db, notes, psId));
 
-    for (String refSuffix : getDraftRefs(notes.getChangeId()).keySet()) {
-      Account.Id account = Account.Id.fromRefPart(refSuffix);
+    for (Ref ref : getDraftRefs(notes.getChangeId())) {
+      Account.Id account = Account.Id.fromRefSuffix(ref.getName());
       if (account != null) {
         comments.addAll(draftByPatchSetAuthor(db, psId, account, notes));
       }
@@ -266,7 +262,7 @@
             }
           }).toSortedList(PLC_ORDER);
     }
-    List<PatchLineComment> comments = Lists.newArrayList();
+    List<PatchLineComment> comments = new ArrayList<>();
     comments.addAll(notes.getDraftComments(author).values());
     return sort(comments);
   }
@@ -278,17 +274,24 @@
       return sort(db.patchComments().draftByAuthor(author).toList());
     }
 
-    Set<String> refNames =
-        getRefNamesAllUsers(RefNames.refsDraftCommentsPrefix(author));
-    List<PatchLineComment> comments = Lists.newArrayList();
-    for (String refName : refNames) {
-      Change.Id changeId = Change.Id.parse(refName);
-      // Avoid loading notes for all affected changes just to be able to auto-
-      // rebuild. This is only used in a corner case in the search codepath, so
-      // returning slightly stale values is ok.
-      DraftCommentNotes notes =
-          draftFactory.createWithAutoRebuildingDisabled(changeId, author);
-      comments.addAll(notes.load().getComments().values());
+    List<PatchLineComment> comments = new ArrayList<>();
+    try (Repository repo = repoManager.openRepository(allUsers)) {
+      for (String refName : repo.getRefDatabase()
+          .getRefs(RefNames.REFS_DRAFT_COMMENTS).keySet()) {
+        Account.Id accountId = Account.Id.fromRefSuffix(refName);
+        Change.Id changeId = Change.Id.fromRefPart(refName);
+        if (accountId == null || changeId == null) {
+          continue;
+        }
+        // Avoid loading notes for all affected changes just to be able to auto-
+        // rebuild. This is only used in a corner case in the search codepath,
+        // so returning slightly stale values is ok.
+        DraftCommentNotes notes =
+            draftFactory.createWithAutoRebuildingDisabled(changeId, author);
+        comments.addAll(notes.load().getComments().values());
+      }
+    } catch (IOException e) {
+      throw new OrmException(e);
     }
     return sort(comments);
   }
@@ -311,10 +314,10 @@
 
   public void deleteAllDraftsFromAllUsers(Change.Id changeId)
       throws IOException {
-    try (Repository repo = repoManager.openMetadataRepository(allUsers);
+    try (Repository repo = repoManager.openRepository(allUsers);
         RevWalk rw = new RevWalk(repo)) {
       BatchRefUpdate bru = repo.getRefDatabase().newBatchUpdate();
-      for (Ref ref : getDraftRefs(repo, changeId).values()) {
+      for (Ref ref : getDraftRefs(repo, changeId)) {
         bru.addCommand(new ReceiveCommand(
             ref.getObjectId(), ObjectId.zeroId(), ref.getName()));
       }
@@ -372,35 +375,19 @@
     return c.getRevId();
   }
 
-  private Set<String> getRefNamesAllUsers(String prefix) throws OrmException {
-    try (Repository repo = repoManager.openMetadataRepository(allUsers)) {
-      RefDatabase refDb = repo.getRefDatabase();
-      return refDb.getRefs(prefix).keySet();
-    } catch (IOException e) {
-      throw new OrmException(e);
-    }
-  }
-
-  public Map<String, Ref> getDraftRefs(Change.Id changeId)
+  public Collection<Ref> getDraftRefs(Change.Id changeId)
       throws OrmException {
-    try (Repository repo = repoManager.openMetadataRepository(allUsers)) {
+    try (Repository repo = repoManager.openRepository(allUsers)) {
       return getDraftRefs(repo, changeId);
     } catch (IOException e) {
       throw new OrmException(e);
     }
   }
 
-  private Map<String, Ref> getDraftRefs(Repository repo,
-      final Change.Id changeId) throws IOException {
-    final String suffix = "/" + changeId.get();
-    return Maps.filterKeys(
-        repo.getRefDatabase().getRefs(RefNames.REFS_DRAFT_COMMENTS),
-        new Predicate<String>() {
-          @Override
-          public boolean apply(String input) {
-            return input.endsWith(suffix);
-          }
-        });
+  private Collection<Ref> getDraftRefs(Repository repo, Change.Id changeId)
+      throws IOException {
+    return repo.getRefDatabase().getRefs(
+        RefNames.refsDraftCommentsPrefix(changeId)).values();
   }
 
   private static List<PatchLineComment> sort(List<PatchLineComment> comments) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java
index 4d26f02..ea3080d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server;
 
-import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.GroupMembership;
@@ -22,7 +21,6 @@
 import com.google.inject.assistedinject.Assisted;
 
 import java.net.SocketAddress;
-import java.util.Collection;
 import java.util.Collections;
 import java.util.Set;
 
@@ -54,11 +52,6 @@
     return Collections.emptySet();
   }
 
-  @Override
-  public Collection<AccountProjectWatch> getNotificationFilters() {
-    return Collections.emptySet();
-  }
-
   public SocketAddress getRemoteAddress() {
     return peer;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java
index e38f88c..af7affd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java
@@ -129,7 +129,7 @@
       suggestedAccounts = suggestAccount(suggestReviewers, visibilityControl);
     }
 
-    List<SuggestedReviewerInfo> reviewer = Lists.newArrayList();
+    List<SuggestedReviewerInfo> reviewer = new ArrayList<>();
     for (AccountInfo a : suggestedAccounts) {
       SuggestedReviewerInfo info = new SuggestedReviewerInfo();
       info.account = a;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java
index 60c41ef..77064fe 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java
@@ -14,19 +14,32 @@
 
 package com.google.gerrit.server;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.CharMatcher;
 import com.google.common.base.Function;
+import com.google.common.base.Joiner;
 import com.google.common.base.Predicate;
+import com.google.common.base.Splitter;
 import com.google.common.collect.FluentIterable;
-import com.google.common.collect.Iterators;
-import com.google.common.collect.Lists;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.collect.Iterables;
+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.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.client.StarredChange;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.server.index.change.ChangeField;
+import com.google.gerrit.server.index.change.ChangeIndexer;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gwtorm.server.ListResultSet;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
@@ -39,6 +52,8 @@
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefDatabase;
@@ -50,69 +65,126 @@
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.util.Collection;
 import java.util.Collections;
-import java.util.Iterator;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
 
 @Singleton
 public class StarredChangesUtil {
+  @AutoValue
+  public abstract static class StarField {
+    private static final String SEPARATOR = ":";
+
+    public static StarField parse(String s) {
+      int p = s.indexOf(SEPARATOR);
+      if (p >= 0) {
+        Integer id = Ints.tryParse(s.substring(0, p));
+        if (id == null) {
+          return null;
+        }
+        Account.Id accountId = new Account.Id(id);
+        String label = s.substring(p + 1);
+        return create(accountId, label);
+      }
+      return null;
+    }
+
+    public static StarField create(Account.Id accountId, String label) {
+      return new AutoValue_StarredChangesUtil_StarField(accountId, label);
+    }
+
+    public abstract Account.Id accountId();
+    public abstract String label();
+
+    @Override
+    public String toString() {
+      return accountId() + SEPARATOR + label();
+    }
+  }
+
+  public static class IllegalLabelException extends IllegalArgumentException {
+    private static final long serialVersionUID = 1L;
+
+    static IllegalLabelException invalidLabels(Set<String> invalidLabels) {
+      return new IllegalLabelException(
+          String.format("invalid labels: %s",
+              Joiner.on(", ").join(invalidLabels)));
+    }
+
+    IllegalLabelException(String message) {
+      super(message);
+    }
+  }
+
   private static final Logger log =
       LoggerFactory.getLogger(StarredChangesUtil.class);
 
+  public static final String DEFAULT_LABEL = "star";
+  public static final ImmutableSortedSet<String> DEFAULT_LABELS =
+      ImmutableSortedSet.of(DEFAULT_LABEL);
+
   private final GitRepositoryManager repoManager;
   private final AllUsersName allUsers;
-  private final NotesMigration migration;
   private final Provider<ReviewDb> dbProvider;
   private final PersonIdent serverIdent;
+  private final ChangeIndexer indexer;
+  private final Provider<InternalChangeQuery> queryProvider;
 
   @Inject
   StarredChangesUtil(GitRepositoryManager repoManager,
       AllUsersName allUsers,
-      NotesMigration migration,
       Provider<ReviewDb> dbProvider,
-      @GerritPersonIdent PersonIdent serverIdent) {
+      @GerritPersonIdent PersonIdent serverIdent,
+      ChangeIndexer indexer,
+      Provider<InternalChangeQuery> queryProvider) {
     this.repoManager = repoManager;
     this.allUsers = allUsers;
-    this.migration = migration;
     this.dbProvider = dbProvider;
     this.serverIdent = serverIdent;
+    this.indexer = indexer;
+    this.queryProvider = queryProvider;
   }
 
-  public void star(Account.Id accountId, Change.Id changeId)
-      throws OrmException {
-    dbProvider.get().starredChanges()
-        .insert(Collections.singleton(new StarredChange(
-            new StarredChange.Key(accountId, changeId))));
-    if (!migration.writeAccounts()) {
-      return;
+  public ImmutableSortedSet<String> getLabels(Account.Id accountId,
+      Change.Id changeId) throws OrmException {
+    try (Repository repo = repoManager.openRepository(allUsers)) {
+      return ImmutableSortedSet.copyOf(
+          readLabels(repo, RefNames.refsStarredChanges(changeId, accountId)));
+    } catch (IOException e) {
+      throw new OrmException(
+          String.format("Reading stars from change %d for account %d failed",
+              changeId.get(), accountId.get()), e);
     }
-    try (Repository repo = repoManager.openMetadataRepository(allUsers);
-        RevWalk rw = new RevWalk(repo)) {
-      RefUpdate u = repo.updateRef(
-          RefNames.refsStarredChanges(accountId, changeId));
-      u.setExpectedOldObjectId(ObjectId.zeroId());
-      u.setNewObjectId(emptyTree(repo));
-      u.setRefLogIdent(serverIdent);
-      u.setRefLogMessage("Star change " + changeId.get(), false);
-      RefUpdate.Result result = u.update(rw);
-      switch (result) {
-        case NEW:
-          return;
-        case FAST_FORWARD:
-        case FORCED:
-        case IO_FAILURE:
-        case LOCK_FAILURE:
-        case NOT_ATTEMPTED:
-        case NO_CHANGE:
-        case REJECTED:
-        case REJECTED_CURRENT_BRANCH:
-        case RENAMED:
-        default:
-          throw new OrmException(
-              String.format("Star change %d for account %d failed: %s",
-                  changeId.get(), accountId.get(), result.name()));
+  }
+
+  public ImmutableSortedSet<String> star(Account.Id accountId,
+      Project.NameKey project, Change.Id changeId, Set<String> labelsToAdd,
+      Set<String> labelsToRemove) throws OrmException {
+    try (Repository repo = repoManager.openRepository(allUsers)) {
+      String refName = RefNames.refsStarredChanges(changeId, accountId);
+      ObjectId oldObjectId = getObjectId(repo, refName);
+
+      SortedSet<String> labels = readLabels(repo, oldObjectId);
+      if (labelsToAdd != null) {
+        labels.addAll(labelsToAdd);
       }
+      if (labelsToRemove != null) {
+        labels.removeAll(labelsToRemove);
+      }
+
+      if (labels.isEmpty()) {
+        deleteRef(repo, refName, oldObjectId);
+      } else {
+        updateLabels(repo, refName, oldObjectId, labels);
+      }
+
+      indexer.index(dbProvider.get(), project, changeId);
+      return ImmutableSortedSet.copyOf(labels);
     } catch (IOException e) {
       throw new OrmException(
           String.format("Star change %d for account %d failed",
@@ -120,68 +192,16 @@
     }
   }
 
-  private static ObjectId emptyTree(Repository repo) throws IOException {
-    try (ObjectInserter oi = repo.newObjectInserter()) {
-      ObjectId id = oi.insert(Constants.OBJ_TREE, new byte[] {});
-      oi.flush();
-      return id;
-    }
-  }
-
-  public void unstar(Account.Id accountId, Change.Id changeId)
-      throws OrmException {
-    dbProvider.get().starredChanges()
-        .delete(Collections.singleton(new StarredChange(
-            new StarredChange.Key(accountId, changeId))));
-    if (!migration.writeAccounts()) {
-      return;
-    }
-    try (Repository repo = repoManager.openMetadataRepository(allUsers);
-        RevWalk rw = new RevWalk(repo)) {
-      RefUpdate u = repo.updateRef(
-          RefNames.refsStarredChanges(accountId, changeId));
-      u.setForceUpdate(true);
-      u.setRefLogIdent(serverIdent);
-      u.setRefLogMessage("Unstar change " + changeId.get(), true);
-      RefUpdate.Result result = u.delete();
-      switch (result) {
-        case FORCED:
-          return;
-        case FAST_FORWARD:
-        case IO_FAILURE:
-        case LOCK_FAILURE:
-        case NEW:
-        case NOT_ATTEMPTED:
-        case NO_CHANGE:
-        case REJECTED:
-        case REJECTED_CURRENT_BRANCH:
-        case RENAMED:
-        default:
-          throw new OrmException(
-              String.format("Unstar change %d for account %d failed: %s",
-                  changeId.get(), accountId.get(), result.name()));
-      }
-    } catch (IOException e) {
-      throw new OrmException(
-          String.format("Unstar change %d for account %d failed",
-              changeId.get(), accountId.get()), e);
-    }
-  }
-
-  public void unstarAll(Change.Id changeId) throws OrmException {
-    dbProvider.get().starredChanges().delete(
-        dbProvider.get().starredChanges().byChange(changeId));
-    if (!migration.writeAccounts()) {
-      return;
-    }
-    try (Repository repo = repoManager.openMetadataRepository(allUsers);
+  public void unstarAll(Project.NameKey project, Change.Id changeId)
+      throws OrmException, NoSuchChangeException {
+    try (Repository repo = repoManager.openRepository(allUsers);
         RevWalk rw = new RevWalk(repo)) {
       BatchRefUpdate batchUpdate = repo.getRefDatabase().newBatchUpdate();
       batchUpdate.setAllowNonFastForwards(true);
       batchUpdate.setRefLogIdent(serverIdent);
       batchUpdate.setRefLogMessage("Unstar change " + changeId.get(), true);
-      for (Account.Id accountId : byChange(changeId)) {
-        String refName = RefNames.refsStarredChanges(accountId, changeId);
+      for (Account.Id accountId : byChangeFromIndex(changeId).keySet()) {
+        String refName = RefNames.refsStarredChanges(changeId, accountId);
         Ref ref = repo.getRefDatabase().getRef(refName);
         batchUpdate.addCommand(new ReceiveCommand(ref.getObjectId(),
             ObjectId.zeroId(), refName));
@@ -194,52 +214,106 @@
               changeId.get(), command.getRefName(), command.getResult()));
         }
       }
+      indexer.index(dbProvider.get(), project, changeId);
     } catch (IOException e) {
       throw new OrmException(
           String.format("Unstar change %d failed", changeId.get()), e);
     }
   }
 
-  public Iterable<Account.Id> byChange(final Change.Id changeId)
+  public ImmutableMultimap<Account.Id, String> byChange(Change.Id changeId)
       throws OrmException {
-    if (!migration.readAccounts()) {
-      return FluentIterable
-          .from(dbProvider.get().starredChanges().byChange(changeId))
-          .transform(new Function<StarredChange, Account.Id>() {
-            @Override
-            public Account.Id apply(StarredChange in) {
-              return in.getAccountId();
-            }
-          });
+    try (Repository repo = repoManager.openRepository(allUsers)) {
+      ImmutableMultimap.Builder<Account.Id, String> builder =
+          new ImmutableMultimap.Builder<>();
+      for (String refPart : getRefNames(repo,
+          RefNames.refsStarredChangesPrefix(changeId))) {
+        Integer id = Ints.tryParse(refPart);
+        if (id == null) {
+          continue;
+        }
+        Account.Id accountId = new Account.Id(id);
+        builder.putAll(accountId,
+            readLabels(repo, RefNames.refsStarredChanges(changeId, accountId)));
+      }
+      return builder.build();
+    } catch (IOException e) {
+      throw new OrmException(String.format(
+          "Get accounts that starred change %d failed", changeId.get()), e);
     }
-    return FluentIterable.from(getRefNames(RefNames.REFS_STARRED_CHANGES))
-        .filter(new Predicate<String>() {
-          @Override
-          public boolean apply(String refPart) {
-            return refPart.endsWith("/" + changeId.get());
-          }
-        })
-        .transform(new Function<String, Account.Id>() {
-          @Override
-          public Account.Id apply(String refPart) {
-            return Account.Id.fromRefPart(refPart);
-          }
-        });
   }
 
-  public ResultSet<Change.Id> query(Account.Id accountId) {
-    try {
-      if (!migration.readAccounts()) {
-        return new ChangeIdResultSet(
-            dbProvider.get().starredChanges().byAccount(accountId));
-      }
-
-      return new ListResultSet<>(FluentIterable
-          .from(getRefNames(RefNames.refsStarredChangesPrefix(accountId)))
-          .transform(new Function<String, Change.Id>() {
+  public Set<Account.Id> byChange(final Change.Id changeId,
+      final String label) throws OrmException {
+    try (final Repository repo = repoManager.openRepository(allUsers)) {
+      return FluentIterable
+          .from(getRefNames(repo, RefNames.refsStarredChangesPrefix(changeId)))
+          .transform(new Function<String, Account.Id>() {
             @Override
-            public Change.Id apply(String changeId) {
-              return Change.Id.parse(changeId);
+            public Account.Id apply(String refPart) {
+              return Account.Id.parse(refPart);
+            }
+          })
+          .filter(new Predicate<Account.Id>() {
+            @Override
+            public boolean apply(Account.Id accountId) {
+              try {
+                return readLabels(repo,
+                    RefNames.refsStarredChanges(changeId, accountId))
+                        .contains(label);
+              } catch (IOException e) {
+                log.error(String.format(
+                    "Cannot query stars by account %d on change %d",
+                    accountId.get(), changeId.get()), e);
+                return false;
+              }
+            }
+          }).toSet();
+    } catch (IOException e) {
+      throw new OrmException(
+          String.format("Get accounts that starred change %d failed",
+              changeId.get()), e);
+    }
+  }
+
+  public ImmutableMultimap<Account.Id, String> byChangeFromIndex(
+      Change.Id changeId) throws OrmException, NoSuchChangeException {
+    Set<String> fields = ImmutableSet.of(
+        ChangeField.ID.getName(),
+        ChangeField.STAR.getName());
+    List<ChangeData> changeData = queryProvider.get().setRequestedFields(fields)
+        .byLegacyChangeId(changeId);
+    if (changeData.size() != 1) {
+      throw new NoSuchChangeException(changeId);
+    }
+    return changeData.get(0).stars();
+  }
+
+  public Set<Account.Id> byChangeFromIndex(Change.Id changeId, String label)
+      throws OrmException, NoSuchChangeException {
+    Set<Account.Id> accounts = new HashSet<>();
+    for (Map.Entry<Account.Id, Collection<String>> e : byChangeFromIndex(
+        changeId).asMap().entrySet()) {
+      if (e.getValue().contains(label)) {
+        accounts.add(e.getKey());
+      }
+    }
+    return accounts;
+  }
+
+  @Deprecated
+  public ResultSet<Change.Id> queryFromIndex(final Account.Id accountId) {
+    try {
+      Set<String> fields = ImmutableSet.of(
+          ChangeField.ID.getName());
+      List<ChangeData> changeData =
+          queryProvider.get().setRequestedFields(fields).byIsStarred(accountId);
+      return new ListResultSet<>(FluentIterable
+          .from(changeData)
+          .transform(new Function<ChangeData, Change.Id>() {
+            @Override
+            public Change.Id apply(ChangeData cd) {
+              return cd.getId();
             }
           }).toList());
     } catch (OrmException | RuntimeException e) {
@@ -250,46 +324,130 @@
     }
   }
 
-  private Set<String> getRefNames(String prefix) throws OrmException {
-    try (Repository repo = repoManager.openMetadataRepository(allUsers)) {
-      RefDatabase refDb = repo.getRefDatabase();
-      return refDb.getRefs(prefix).keySet();
+  private static Set<String> getRefNames(Repository repo, String prefix)
+      throws IOException {
+    RefDatabase refDb = repo.getRefDatabase();
+    return refDb.getRefs(prefix).keySet();
+  }
+
+  public ObjectId getObjectId(Account.Id accountId, Change.Id changeId) {
+    try (Repository repo = repoManager.openRepository(allUsers)) {
+      return getObjectId(repo,
+          RefNames.refsStarredChanges(changeId, accountId));
     } catch (IOException e) {
-      throw new OrmException(e);
+      log.error(String.format(
+          "Getting star object ID for account %d on change %d failed",
+          accountId.get(), changeId.get()), e);
+      return ObjectId.zeroId();
     }
   }
 
-  private static class ChangeIdResultSet implements ResultSet<Change.Id> {
-    private static final Function<StarredChange, Change.Id>
-        STARRED_CHANGE_TO_CHANGE_ID =
-            new Function<StarredChange, Change.Id>() {
-              @Override
-              public Change.Id apply(StarredChange starredChange) {
-                return starredChange.getChangeId();
-              }
-            };
+  private static ObjectId getObjectId(Repository repo, String refName)
+      throws IOException {
+    Ref ref = repo.exactRef(refName);
+    return ref != null ? ref.getObjectId() : ObjectId.zeroId();
+  }
 
-    private final ResultSet<StarredChange> starredChangesResultSet;
+  private static SortedSet<String> readLabels(Repository repo, String refName)
+      throws IOException {
+    return readLabels(repo, getObjectId(repo, refName));
+  }
 
-    ChangeIdResultSet(ResultSet<StarredChange> starredChangesResultSet) {
-      this.starredChangesResultSet = starredChangesResultSet;
+  private static TreeSet<String> readLabels(Repository repo, ObjectId id)
+      throws IOException {
+    if (ObjectId.zeroId().equals(id)) {
+      return new TreeSet<>();
     }
 
-    @Override
-    public Iterator<Change.Id> iterator() {
-      return Iterators.transform(starredChangesResultSet.iterator(),
-          STARRED_CHANGE_TO_CHANGE_ID);
+    try (ObjectReader reader = repo.newObjectReader()) {
+      ObjectLoader obj = reader.open(id, Constants.OBJ_BLOB);
+      TreeSet<String> labels = new TreeSet<>();
+      Iterables.addAll(labels,
+          Splitter.on(CharMatcher.whitespace()).omitEmptyStrings()
+              .split(new String(obj.getCachedBytes(Integer.MAX_VALUE), UTF_8)));
+      return labels;
+    }
+  }
+
+  public static ObjectId writeLabels(Repository repo, SortedSet<String> labels)
+      throws IOException {
+    validateLabels(labels);
+    try (ObjectInserter oi = repo.newObjectInserter()) {
+      ObjectId id = oi.insert(Constants.OBJ_BLOB,
+          Joiner.on("\n").join(labels).getBytes(UTF_8));
+      oi.flush();
+      return id;
+    }
+  }
+
+  private static void validateLabels(Set<String> labels) {
+    if (labels == null) {
+      return;
     }
 
-    @Override
-    public List<Change.Id> toList() {
-      return Lists.transform(starredChangesResultSet.toList(),
-          STARRED_CHANGE_TO_CHANGE_ID);
+    SortedSet<String> invalidLabels = new TreeSet<>();
+    for (String label : labels) {
+      if (CharMatcher.whitespace().matchesAnyOf(label)) {
+        invalidLabels.add(label);
+      }
     }
+    if (!invalidLabels.isEmpty()) {
+      throw IllegalLabelException.invalidLabels(invalidLabels);
+    }
+  }
 
-    @Override
-    public void close() {
-      starredChangesResultSet.close();
+  private void updateLabels(Repository repo, String refName,
+      ObjectId oldObjectId, SortedSet<String> labels)
+          throws IOException, OrmException {
+    try (RevWalk rw = new RevWalk(repo)) {
+      RefUpdate u = repo.updateRef(refName);
+      u.setExpectedOldObjectId(oldObjectId);
+      u.setForceUpdate(true);
+      u.setNewObjectId(writeLabels(repo, labels));
+      u.setRefLogIdent(serverIdent);
+      u.setRefLogMessage("Update star labels", true);
+      RefUpdate.Result result = u.update(rw);
+      switch (result) {
+        case NEW:
+        case FORCED:
+        case NO_CHANGE:
+        case FAST_FORWARD:
+          return;
+        case IO_FAILURE:
+        case LOCK_FAILURE:
+        case NOT_ATTEMPTED:
+        case REJECTED:
+        case REJECTED_CURRENT_BRANCH:
+        case RENAMED:
+          throw new OrmException(
+              String.format("Update star labels on ref %s failed: %s", refName,
+                  result.name()));
+      }
+    }
+  }
+
+  private void deleteRef(Repository repo, String refName, ObjectId oldObjectId)
+      throws IOException, OrmException {
+    RefUpdate u = repo.updateRef(refName);
+    u.setForceUpdate(true);
+    u.setExpectedOldObjectId(oldObjectId);
+    u.setRefLogIdent(serverIdent);
+    u.setRefLogMessage("Unstar change", true);
+    RefUpdate.Result result = u.delete();
+    switch (result) {
+      case FORCED:
+        return;
+      case NEW:
+      case NO_CHANGE:
+      case FAST_FORWARD:
+      case IO_FAILURE:
+      case LOCK_FAILURE:
+      case NOT_ATTEMPTED:
+      case REJECTED:
+      case REJECTED_CURRENT_BRANCH:
+      case RENAMED:
+        throw new OrmException(String.format("Delete star ref %s failed: %s",
+            refName, result.name()));
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/StringUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/StringUtil.java
index 2133dfb..0aef9e9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/StringUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/StringUtil.java
@@ -26,7 +26,7 @@
     { "\\x00", "\\x01", "\\x02", "\\x03", "\\x04", "\\x05", "\\x06", "\\a",
       "\\b",   "\\t",   "\\n",   "\\v",   "\\f",   "\\r",   "\\x0e", "\\x0f",
       "\\x10", "\\x11", "\\x12", "\\x13", "\\x14", "\\x15", "\\x16", "\\x17",
-      "\\x18", "\\x19", "\\x1a", "\\x1b", "\\x1c", "\\x1d", "\\x1e", "\\x1f" };
+      "\\x18", "\\x19", "\\x1a", "\\x1b", "\\x1c", "\\x1d", "\\x1e", "\\x1f", };
 
   /**
    * Escapes the input string so that all non-printable characters
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
index 351de5e..6057f81 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/WebLinks.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/WebLinks.java
@@ -44,7 +44,7 @@
 
         @Override
         public boolean apply(WebLinkInfo link) {
-          if (link == null){
+          if (link == null) {
             return false;
           } else if (Strings.isNullOrEmpty(link.name)
               || Strings.isNullOrEmpty(link.url)) {
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 04546aa..aeff017 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
@@ -14,285 +14,45 @@
 
 package com.google.gerrit.server.access;
 
-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.common.data.AccessSection;
-import com.google.gerrit.common.data.Permission;
-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.api.access.ProjectAccessInfo;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 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.RefNames;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.account.GroupBackend;
-import com.google.gerrit.server.account.GroupControl;
-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.NoSuchProjectException;
-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.ProjectState;
-import com.google.gerrit.server.project.RefControl;
+import com.google.gerrit.server.project.GetAccess;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.kohsuke.args4j.Option;
 
 import java.io.IOException;
-import java.util.HashMap;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
+import java.util.TreeMap;
 
 public class ListAccess implements RestReadView<TopLevelResource> {
 
   @Option(name = "--project", aliases = {"-p"}, metaVar = "PROJECT",
       usage = "projects for which the access rights should be returned")
-  private List<String> projects = Lists.newArrayList();
+  private List<String> projects = new ArrayList<>();
 
-  private final Provider<CurrentUser> self;
-  private final ProjectControl.GenericFactory projectControlFactory;
-  private final ProjectCache projectCache;
-  private final ProjectJson projectJson;
-  private final MetaDataUpdate.Server metaDataUpdateFactory;
-  private final GroupControl.Factory groupControlFactory;
-  private final GroupBackend groupBackend;
-  private final AllProjectsName allProjectsName;
+  private final GetAccess getAccess;
 
   @Inject
-  public ListAccess(Provider<CurrentUser> self,
-      ProjectControl.GenericFactory projectControlFactory,
-      ProjectCache projectCache, ProjectJson projectJson,
-      MetaDataUpdate.Server metaDataUpdateFactory,
-      GroupControl.Factory groupControlFactory, GroupBackend groupBackend,
-      AllProjectsName allProjectsName) {
-    this.self = self;
-    this.projectControlFactory = projectControlFactory;
-    this.projectCache = projectCache;
-    this.projectJson = projectJson;
-    this.metaDataUpdateFactory = metaDataUpdateFactory;
-    this.groupControlFactory = groupControlFactory;
-    this.groupBackend = groupBackend;
-    this.allProjectsName = allProjectsName;
+  public ListAccess(GetAccess getAccess) {
+    this.getAccess = getAccess;
   }
 
   @Override
   public Map<String, ProjectAccessInfo> apply(TopLevelResource resource)
       throws ResourceNotFoundException, ResourceConflictException, IOException {
-    Map<String, ProjectAccessInfo> access = Maps.newTreeMap();
-    for (String p: projects) {
-      // Load the current configuration from the repository, ensuring it's the most
-      // recent version available. If it differs from what was in the project
-      // state, force a cache flush now.
-      //
+    Map<String, ProjectAccessInfo> access = new TreeMap<>();
+    for (String p : projects) {
       Project.NameKey projectName = new Project.NameKey(p);
-      try (MetaDataUpdate md = metaDataUpdateFactory.create(projectName)) {
-        ProjectControl pc = open(projectName);
-        ProjectConfig config = ProjectConfig.read(md);
-
-        if (config.updateGroupNames(groupBackend)) {
-          md.setMessage("Update group names\n");
-          config.commit(md);
-          projectCache.evict(config.getProject());
-          pc = open(projectName);
-        } else if (config.getRevision() != null
-            && !config.getRevision().equals(
-                pc.getProjectState().getConfig().getRevision())) {
-          projectCache.evict(config.getProject());
-          pc = open(projectName);
-        }
-        access.put(p, new ProjectAccessInfo(pc, config));
-      } catch (ConfigInvalidException e) {
-        throw new ResourceConflictException(e.getMessage());
-      } catch (RepositoryNotFoundException e) {
-        throw new ResourceNotFoundException(p);
-      }
+      access.put(p, getAccess.apply(projectName));
     }
     return access;
   }
 
-  private ProjectControl open(Project.NameKey projectName)
-      throws ResourceNotFoundException, IOException {
-    try {
-      return projectControlFactory.validateFor(projectName,
-          ProjectControl.OWNER | ProjectControl.VISIBLE, self.get());
-    } catch (NoSuchProjectException e) {
-      throw new ResourceNotFoundException(projectName.get());
-    }
-  }
-
-  public class ProjectAccessInfo {
-    public String revision;
-    public ProjectInfo inheritsFrom;
-    public Map<String, AccessSectionInfo> local;
-    public Boolean isOwner;
-    public Set<String> ownerOf;
-    public Boolean canUpload;
-    public Boolean canAdd;
-    public Boolean configVisible;
-
-    public ProjectAccessInfo(ProjectControl pc, ProjectConfig config) {
-      final RefControl metaConfigControl =
-          pc.controlForRef(RefNames.REFS_CONFIG);
-      local = Maps.newHashMap();
-      ownerOf = Sets.newHashSet();
-      Map<AccountGroup.UUID, Boolean> visibleGroups = new HashMap<>();
-
-      for (AccessSection section : config.getAccessSections()) {
-        String name = section.getName();
-        if (AccessSection.GLOBAL_CAPABILITIES.equals(name)) {
-          if (pc.isOwner()) {
-            local.put(name, new AccessSectionInfo(section));
-            ownerOf.add(name);
-
-          } else if (metaConfigControl.isVisible()) {
-            local.put(section.getName(), new AccessSectionInfo(section));
-          }
-
-        } else if (RefConfigSection.isValid(name)) {
-          RefControl rc = pc.controlForRef(name);
-          if (rc.isOwner()) {
-            local.put(name, new AccessSectionInfo(section));
-            ownerOf.add(name);
-
-          } else if (metaConfigControl.isVisible()) {
-            local.put(name, new AccessSectionInfo(section));
-
-          } else if (rc.isVisible()) {
-            // Filter the section to only add rules describing groups that
-            // are visible to the current-user. This includes any group the
-            // user is a member of, as well as groups they own or that
-            // are visible to all users.
-
-            AccessSection dst = null;
-            for (Permission srcPerm : section.getPermissions()) {
-              Permission dstPerm = null;
-
-              for (PermissionRule srcRule : srcPerm.getRules()) {
-                AccountGroup.UUID group = srcRule.getGroup().getUUID();
-                if (group == null) {
-                  continue;
-                }
-
-                Boolean canSeeGroup = visibleGroups.get(group);
-                if (canSeeGroup == null) {
-                  try {
-                    canSeeGroup = groupControlFactory.controlFor(group).isVisible();
-                  } catch (NoSuchGroupException e) {
-                    canSeeGroup = Boolean.FALSE;
-                  }
-                  visibleGroups.put(group, canSeeGroup);
-                }
-
-                if (canSeeGroup) {
-                  if (dstPerm == null) {
-                    if (dst == null) {
-                      dst = new AccessSection(name);
-                      local.put(name, new AccessSectionInfo(dst));
-                    }
-                    dstPerm = dst.getPermission(srcPerm.getName(), true);
-                  }
-                  dstPerm.add(srcRule);
-                }
-              }
-            }
-          }
-        }
-      }
-
-      if (ownerOf.isEmpty() && pc.isOwnerAnyRef()) {
-        // Special case: If the section list is empty, this project has no current
-        // access control information. Rely on what ProjectControl determines
-        // is ownership, which probably means falling back to site administrators.
-        ownerOf.add(AccessSection.ALL);
-      }
-
-
-      if (config.getRevision() != null) {
-        revision = config.getRevision().name();
-      }
-
-      ProjectState parent =
-          Iterables.getFirst(pc.getProjectState().parents(), null);
-      if (parent != null) {
-        inheritsFrom = projectJson.format(parent.getProject());
-      }
-
-      if (pc.getProject().getNameKey().equals(allProjectsName)) {
-        if (pc.isOwner()) {
-          ownerOf.add(AccessSection.GLOBAL_CAPABILITIES);
-        }
-      }
-
-      isOwner = toBoolean(pc.isOwner());
-      canUpload = toBoolean(pc.isOwner()
-          || (metaConfigControl.isVisible() && metaConfigControl.canUpload()));
-      canAdd = toBoolean(pc.canAddRefs());
-      configVisible = pc.isOwner() || metaConfigControl.isVisible();
-    }
-  }
-
-  public static class AccessSectionInfo {
-    public Map<String, PermissionInfo> permissions;
-
-    public AccessSectionInfo(AccessSection section) {
-      permissions = Maps.newHashMap();
-      for (Permission p : section.getPermissions()) {
-        permissions.put(p.getName(), new PermissionInfo(p));
-      }
-    }
-  }
-
-  public static class PermissionInfo {
-    public String label;
-    public Boolean exclusive;
-    public Map<String, PermissionRuleInfo> rules;
-
-    public PermissionInfo(Permission permission) {
-      label = permission.getLabel();
-      exclusive = toBoolean(permission.getExclusiveGroup());
-      rules = Maps.newHashMap();
-      for (PermissionRule r : permission.getRules()) {
-        rules.put(r.getGroup().getUUID().get(), new PermissionRuleInfo(r));
-      }
-    }
-  }
-
-  public static class PermissionRuleInfo {
-    public PermissionRule.Action action;
-    public Boolean force;
-    public Integer min;
-    public Integer max;
-
-
-    public PermissionRuleInfo(PermissionRule rule) {
-      action = rule.getAction();
-      force = toBoolean(rule.getForce());
-      if (hasRange(rule)) {
-        min = rule.getMin();
-        max = rule.getMax();
-      }
-    }
-
-    private boolean hasRange(PermissionRule rule) {
-      return (!(rule.getMin() == null || rule.getMin() == 0))
-          || (!(rule.getMax() == null || rule.getMax() == 0));
-    }
-  }
-
-  private static Boolean toBoolean(boolean value) {
-    return value ? true : null;
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
index 28df97a..57ffd0a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
@@ -17,7 +17,6 @@
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Sets;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -33,6 +32,7 @@
 import org.slf4j.LoggerFactory;
 
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
 
@@ -93,7 +93,7 @@
     @Override
     public Set<Account.Id> load(String email) throws Exception {
       try (ReviewDb db = schema.open()) {
-        Set<Account.Id> r = Sets.newHashSet();
+        Set<Account.Id> r = new HashSet<>();
         for (Account a : db.accounts().byPreferredEmail(email)) {
           r.add(a.getId());
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountLoader.java
index 3e9b575..68e19e4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountLoader.java
@@ -18,8 +18,6 @@
 
 import com.google.common.base.Throwables;
 import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountDirectory.DirectoryException;
@@ -28,9 +26,11 @@
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.EnumSet;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -57,8 +57,8 @@
   AccountLoader(InternalAccountDirectory directory, @Assisted boolean detailed) {
     this.directory = directory;
     options = detailed ? DETAILED_OPTIONS : InternalAccountDirectory.ID_ONLY;
-    created = Maps.newHashMap();
-    provided = Lists.newArrayList();
+    created = new HashMap<>();
+    provided = new ArrayList<>();
   }
 
   public AccountInfo get(Account.Id id) {
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 9ac20f6..596c440 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
@@ -344,8 +344,6 @@
   public AuthResult link(Account.Id to, AuthRequest who)
       throws AccountException, OrmException {
     try (ReviewDb db = schema.open()) {
-      who = realm.link(db, to, who);
-
       AccountExternalId.Key key = id(who);
       AccountExternalId extId = getAccountExternalId(db, key);
       if (extId != null) {
@@ -354,7 +352,7 @@
         }
         try {
           update(db, who, extId);
-        } catch(NameAlreadyUsedException | InvalidUserNameException e) {
+        } catch (NameAlreadyUsedException | InvalidUserNameException e) {
           throw new AccountException("Account update failed", e);
         }
 
@@ -435,8 +433,6 @@
   public AuthResult unlink(Account.Id from, AuthRequest who)
       throws AccountException, OrmException {
     try (ReviewDb db = schema.open()) {
-      who = realm.unlink(db, from, who);
-
       AccountExternalId.Key key = id(who);
       AccountExternalId extId = getAccountExternalId(db, key);
       if (extId != null) {
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 558f8c0..04793c5 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
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.common.collect.Sets;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -154,7 +153,7 @@
 
       // more than one match, try to return the best one
       String name = nameOrEmail.substring(0, lt - 1);
-      Set<Account.Id> nameMatches = Sets.newHashSet();
+      Set<Account.Id> nameMatches = new HashSet<>();
       for (Account.Id id : ids) {
         Account a = byId.get(id).getAccount();
         if (name.equals(a.getFullName())) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResource.java
index 75e5ae5..8bebf52 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResource.java
@@ -22,6 +22,8 @@
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.inject.TypeLiteral;
 
+import java.util.Set;
+
 public class AccountResource implements RestResource {
   public static final TypeLiteral<RestView<AccountResource>> ACCOUNT_KIND =
       new TypeLiteral<RestView<AccountResource>>() {};
@@ -108,4 +110,32 @@
       return change.getChange();
     }
   }
+
+  public static class Star implements RestResource {
+    public static final TypeLiteral<RestView<Star>> STAR_KIND =
+        new TypeLiteral<RestView<Star>>() {};
+
+    private final IdentifiedUser user;
+    private final ChangeResource change;
+    private final Set<String> labels;
+
+    public Star(IdentifiedUser user, ChangeResource change,
+        Set<String> labels) {
+      this.user = user;
+      this.change = change;
+      this.labels = labels;
+    }
+
+    public IdentifiedUser getUser() {
+      return user;
+    }
+
+    public Change getChange() {
+      return change.getChange();
+    }
+
+    public Set<String> getLabels() {
+      return labels;
+    }
+  }
 }
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 815b519..37e36cd 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
@@ -16,9 +16,14 @@
 
 import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME;
 
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.CurrentUser.PropertyKey;
+import com.google.gerrit.server.IdentifiedUser;
 
 import java.util.Collection;
 import java.util.Set;
@@ -27,6 +32,7 @@
   private final Account account;
   private final Set<AccountGroup.UUID> internalGroups;
   private final Collection<AccountExternalId> externalIds;
+  private Cache<IdentifiedUser.PropertyKey<Object>, Object> properties;
 
   public AccountState(final Account account,
       final Set<AccountGroup.UUID> actualGroups,
@@ -81,4 +87,59 @@
     }
     return null;
   }
+
+  /**
+   * Lookup a previously stored property.
+   * <p>
+   * All properties are automatically cleared when the account cache invalidates
+   * the {@code AccountState}. This method is thread-safe.
+   *
+   * @param key unique property key.
+   * @return previously stored value, or {@code null}.
+   */
+  @Nullable
+  public <T> T get(PropertyKey<T> key) {
+    Cache<PropertyKey<Object>, Object> p = properties(false);
+    if (p != null) {
+      @SuppressWarnings("unchecked")
+      T value = (T) p.getIfPresent(key);
+      return value;
+    }
+    return null;
+  }
+
+  /**
+   * Store a property for later retrieval.
+   * <p>
+   * This method is thread-safe.
+   *
+   * @param key unique property key.
+   * @param value value to store; or {@code null} to clear the value.
+   */
+  public <T> void put(PropertyKey<T> key, @Nullable T value) {
+    Cache<PropertyKey<Object>, Object> p = properties(value != null);
+    if (p != null || value != null) {
+      @SuppressWarnings("unchecked")
+      PropertyKey<Object> k = (PropertyKey<Object>) key;
+      if (value != null) {
+        p.put(k, value);
+      } else {
+        p.invalidate(k);
+      }
+    }
+  }
+
+  private synchronized Cache<PropertyKey<Object>, Object> properties(
+      boolean allocate) {
+    if (properties == null && allocate) {
+      properties = CacheBuilder.newBuilder()
+          .concurrencyLevel(1)
+          .initialCapacity(16)
+          // Use weakKeys to ensure plugins that garbage collect will also
+          // eventually release data held in any still live AccountState.
+          .weakKeys()
+          .build();
+    }
+    return properties;
+  }
 }
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 80ea907..d54a999 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
@@ -35,7 +35,7 @@
 @Singleton
 public class AccountsCollection implements
     RestCollection<TopLevelResource, AccountResource>,
-    AcceptsCreate<TopLevelResource>{
+    AcceptsCreate<TopLevelResource> {
   private final Provider<CurrentUser> self;
   private final AccountResolver resolver;
   private final AccountControl.Factory accountControlFactory;
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 e91998e..216672c 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,7 +16,6 @@
 
 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.EmailException;
 import com.google.gerrit.common.errors.InvalidSshKeyException;
@@ -27,24 +26,22 @@
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.client.AccountSshKey;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AddSshKey.Input;
 import com.google.gerrit.server.mail.AddKeySender;
 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 org.eclipse.jgit.errors.ConfigInvalidException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
 import java.io.InputStream;
-import java.util.Collections;
 
 @Singleton
 public class AddSshKey implements RestModifyView<AccountResource, Input> {
@@ -55,22 +52,25 @@
   }
 
   private final Provider<CurrentUser> self;
-  private final Provider<ReviewDb> dbProvider;
+  private final VersionedAuthorizedKeys.Accessor authorizedKeys;
   private final SshKeyCache sshKeyCache;
   private final AddKeySender.Factory addKeyFactory;
 
   @Inject
-  AddSshKey(Provider<CurrentUser> self, Provider<ReviewDb> dbProvider,
-      SshKeyCache sshKeyCache, AddKeySender.Factory addKeyFactory) {
+  AddSshKey(Provider<CurrentUser> self,
+      VersionedAuthorizedKeys.Accessor authorizedKeys,
+      SshKeyCache sshKeyCache,
+      AddKeySender.Factory addKeyFactory) {
     this.self = self;
-    this.dbProvider = dbProvider;
+    this.authorizedKeys = authorizedKeys;
     this.sshKeyCache = sshKeyCache;
     this.addKeyFactory = addKeyFactory;
   }
 
   @Override
   public Response<SshKeyInfo> apply(AccountResource rsrc, Input input)
-      throws AuthException, BadRequestException, OrmException, IOException {
+      throws AuthException, BadRequestException, OrmException, IOException,
+      ConfigInvalidException {
     if (self.get() != rsrc.getUser()
         && !self.get().getCapabilities().canAdministrateServer()) {
       throw new AuthException("not allowed to add SSH keys");
@@ -79,7 +79,8 @@
   }
 
   public Response<SshKeyInfo> apply(IdentifiedUser user, Input input)
-      throws BadRequestException, OrmException, IOException {
+      throws BadRequestException, IOException,
+      ConfigInvalidException {
     if (input == null) {
       input = new Input();
     }
@@ -87,11 +88,6 @@
       throw new BadRequestException("SSH public key missing");
     }
 
-    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() {
       @Override
@@ -102,15 +98,15 @@
 
     try {
       AccountSshKey sshKey =
-          sshKeyCache.create(new AccountSshKey.Id(
-              user.getAccountId(), max + 1), sshPublicKey);
-      dbProvider.get().accountSshKeys().insert(Collections.singleton(sshKey));
+          authorizedKeys.addKey(user.getAccountId(), sshPublicKey);
+
       try {
         addKeyFactory.create(user, sshKey).send();
       } catch (EmailException e) {
         log.error("Cannot send SSH key added message to "
             + user.getAccount().getPreferredEmail(), e);
       }
+
       sshKeyCache.evict(user.getUserName());
       return Response.<SshKeyInfo>created(GetSshKeys.newSshKeyInfo(sshKey));
     } catch (InvalidSshKeyException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthorizedKeys.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthorizedKeys.java
new file mode 100644
index 0000000..0e8c051
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthorizedKeys.java
@@ -0,0 +1,78 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.annotations.VisibleForTesting;
+import com.google.common.base.Optional;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountSshKey;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+public class AuthorizedKeys {
+  public static final String FILE_NAME = "authorized_keys";
+
+  @VisibleForTesting
+  public static final String INVALID_KEY_COMMENT_PREFIX = "# INVALID ";
+
+  @VisibleForTesting
+  public static final String DELETED_KEY_COMMENT = "# DELETED";
+
+  public static List<Optional<AccountSshKey>> parse(
+      Account.Id accountId, String s) {
+    List<Optional<AccountSshKey>> keys = new ArrayList<>();
+    int seq = 1;
+    for (String line : s.split("\\r?\\n")) {
+      line = line.trim();
+      if (line.isEmpty()) {
+        continue;
+      } else if (line.startsWith(INVALID_KEY_COMMENT_PREFIX)) {
+        String pub = line.substring(INVALID_KEY_COMMENT_PREFIX.length());
+        AccountSshKey key =
+            new AccountSshKey(new AccountSshKey.Id(accountId, seq++), pub);
+        key.setInvalid();
+        keys.add(Optional.of(key));
+      } else if (line.startsWith(DELETED_KEY_COMMENT)) {
+        keys.add(Optional.<AccountSshKey> absent());
+        seq++;
+      } else if (line.startsWith("#")) {
+        continue;
+      } else {
+        AccountSshKey key =
+            new AccountSshKey(new AccountSshKey.Id(accountId, seq++), line);
+        keys.add(Optional.of(key));
+      }
+    }
+    return keys;
+  }
+
+  public static String serialize(Collection<Optional<AccountSshKey>> keys) {
+    StringBuilder b = new StringBuilder();
+    for (Optional<AccountSshKey> key : keys) {
+      if (key.isPresent()) {
+        if (!key.get().isValid()) {
+          b.append(INVALID_KEY_COMMENT_PREFIX);
+        }
+        b.append(key.get().getSshPublicKey().trim());
+      } else {
+        b.append(DELETED_KEY_COMMENT);
+      }
+      b.append("\n");
+    }
+    return b.toString();
+  }
+}
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 9d580f5..0f78581 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
@@ -39,7 +39,7 @@
 
 /** Access control management for server-wide capabilities. */
 public class CapabilityControl {
-  public static interface Factory {
+  public interface Factory {
     CapabilityControl create(CurrentUser user);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityUtils.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityUtils.java
index c26b1ab..2bf147d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityUtils.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityUtils.java
@@ -32,6 +32,11 @@
       .getLogger(CapabilityUtils.class);
 
   public static void checkRequiresCapability(Provider<CurrentUser> userProvider,
+      String pluginName, Class<?> clazz) throws AuthException {
+    checkRequiresCapability(userProvider.get(), pluginName, clazz);
+  }
+
+  public static void checkRequiresCapability(CurrentUser user,
       String pluginName, Class<?> clazz)
       throws AuthException {
     RequiresCapability rc = getClassAnnotation(clazz, RequiresCapability.class);
@@ -45,7 +50,6 @@
           RequiresAnyCapability.class.getSimpleName()));
       throw new AuthException("cannot check capability");
     }
-    CurrentUser user = userProvider.get();
     CapabilityControl ctl = user.getCapabilities();
     if (ctl.canAdministrateServer()) {
       return;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
index f16c3ea..3a511ee 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.common.collect.Sets;
 import com.google.gerrit.audit.AuditService;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.GlobalCapability;
@@ -34,7 +33,6 @@
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gerrit.reviewdb.client.AccountSshKey;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.CreateAccount.Input;
@@ -48,8 +46,11 @@
 import com.google.inject.assistedinject.Assisted;
 
 import org.apache.commons.validator.routines.EmailValidator;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 
+import java.io.IOException;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Set;
@@ -66,44 +67,52 @@
     public List<String> groups;
   }
 
-  public static interface Factory {
+  public interface Factory {
     CreateAccount create(String username);
   }
 
   private final ReviewDb db;
   private final Provider<IdentifiedUser> currentUser;
   private final GroupsCollection groupsCollection;
+  private final VersionedAuthorizedKeys.Accessor authorizedKeys;
   private final SshKeyCache sshKeyCache;
   private final AccountCache accountCache;
   private final AccountByEmailCache byEmailCache;
   private final AccountLoader.Factory infoLoader;
   private final DynamicSet<AccountExternalIdCreator> externalIdCreators;
-  private final String username;
   private final AuditService auditService;
+  private final String username;
 
   @Inject
-  CreateAccount(ReviewDb db, Provider<IdentifiedUser> currentUser,
-      GroupsCollection groupsCollection, SshKeyCache sshKeyCache,
-      AccountCache accountCache, AccountByEmailCache byEmailCache,
+  CreateAccount(ReviewDb db,
+      Provider<IdentifiedUser> currentUser,
+      GroupsCollection groupsCollection,
+      VersionedAuthorizedKeys.Accessor authorizedKeys,
+      SshKeyCache sshKeyCache,
+      AccountCache accountCache,
+      AccountByEmailCache byEmailCache,
       AccountLoader.Factory infoLoader,
       DynamicSet<AccountExternalIdCreator> externalIdCreators,
-      @Assisted String username, AuditService auditService) {
+      AuditService auditService,
+      @Assisted String username) {
     this.db = db;
     this.currentUser = currentUser;
     this.groupsCollection = groupsCollection;
+    this.authorizedKeys = authorizedKeys;
     this.sshKeyCache = sshKeyCache;
     this.accountCache = accountCache;
     this.byEmailCache = byEmailCache;
     this.infoLoader = infoLoader;
     this.externalIdCreators = externalIdCreators;
-    this.username = username;
     this.auditService = auditService;
+    this.username = username;
   }
 
   @Override
   public Response<AccountInfo> apply(TopLevelResource rsrc, Input input)
       throws BadRequestException, ResourceConflictException,
-      UnprocessableEntityException, OrmException {
+      UnprocessableEntityException, OrmException, IOException,
+      ConfigInvalidException {
     if (input == null) {
       input = new Input();
     }
@@ -119,7 +128,6 @@
     Set<AccountGroup.Id> groups = parseGroups(input.groups);
 
     Account.Id id = new Account.Id(db.nextAccountId());
-    AccountSshKey key = createSshKey(id, input.sshKey);
 
     AccountExternalId extUser =
         new AccountExternalId(id, new AccountExternalId.Key(
@@ -178,10 +186,6 @@
     a.setPreferredEmail(input.email);
     db.accounts().insert(Collections.singleton(a));
 
-    if (key != null) {
-      db.accountSshKeys().insert(Collections.singleton(key));
-    }
-
     for (AccountGroup.Id groupId : groups) {
       AccountGroupMember m =
           new AccountGroupMember(new AccountGroupMember.Key(id, groupId));
@@ -190,7 +194,15 @@
       db.accountGroupMembers().insert(Collections.singleton(m));
     }
 
-    sshKeyCache.evict(username);
+    if (input.sshKey != null) {
+      try {
+        authorizedKeys.addKey(id, input.sshKey);
+        sshKeyCache.evict(username);
+      } catch (InvalidSshKeyException e) {
+        throw new BadRequestException(e.getMessage());
+      }
+    }
+
     accountCache.evictByUsername(username);
     byEmailCache.evict(input.email);
 
@@ -202,7 +214,7 @@
 
   private Set<AccountGroup.Id> parseGroups(List<String> groups)
       throws UnprocessableEntityException {
-    Set<AccountGroup.Id> groupIds = Sets.newHashSet();
+    Set<AccountGroup.Id> groupIds = new HashSet<>();
     if (groups != null) {
       for (String g : groups) {
         groupIds.add(GroupDescriptions.toAccountGroup(
@@ -212,18 +224,6 @@
     return groupIds;
   }
 
-  private AccountSshKey createSshKey(Account.Id id, String sshKey)
-      throws BadRequestException {
-    if (sshKey == null) {
-      return null;
-    }
-    try {
-      return sshKeyCache.create(new AccountSshKey.Id(id, 1), sshKey.trim());
-    } catch (InvalidSshKeyException e) {
-      throw new BadRequestException(e.getMessage());
-    }
-  }
-
   private AccountExternalId.Key getEmailKey(String email) {
     return new AccountExternalId.Key(AccountExternalId.SCHEME_MAILTO, email);
   }
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 a78c23c..45dd971 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
@@ -42,7 +42,7 @@
 public class CreateEmail implements RestModifyView<AccountResource, EmailInput> {
   private static final Logger log = LoggerFactory.getLogger(CreateEmail.class);
 
-  public static interface Factory {
+  public interface Factory {
     CreateEmail create(String email);
   }
 
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 96dc4b8..5b7a9a2 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
@@ -17,7 +17,6 @@
 import com.google.common.base.Strings;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AuthType;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -75,16 +74,6 @@
   }
 
   @Override
-  public AuthRequest link(ReviewDb db, Account.Id to, AuthRequest who) {
-    return who;
-  }
-
-  @Override
-  public AuthRequest unlink(ReviewDb db, Account.Id from, AuthRequest who) {
-    return who;
-  }
-
-  @Override
   public void onCreateAccount(final AuthRequest who, final Account account) {
   }
 
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 9066858..9212002 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
@@ -17,7 +17,6 @@
 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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.DeleteSshKey.Input;
 import com.google.gerrit.server.ssh.SshKeyCache;
@@ -26,7 +25,10 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
-import java.util.Collections;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+
+import java.io.IOException;
 
 @Singleton
 public class DeleteSshKey implements
@@ -35,28 +37,31 @@
   }
 
   private final Provider<CurrentUser> self;
-  private final Provider<ReviewDb> dbProvider;
+  private final VersionedAuthorizedKeys.Accessor authorizedKeys;
   private final SshKeyCache sshKeyCache;
 
   @Inject
-  DeleteSshKey(Provider<ReviewDb> dbProvider,
-      Provider<CurrentUser> self,
+  DeleteSshKey(Provider<CurrentUser> self,
+      VersionedAuthorizedKeys.Accessor authorizedKeys,
       SshKeyCache sshKeyCache) {
     this.self = self;
-    this.dbProvider = dbProvider;
+    this.authorizedKeys = authorizedKeys;
     this.sshKeyCache = sshKeyCache;
   }
 
   @Override
   public Response<?> apply(AccountResource.SshKey rsrc, Input input)
-      throws AuthException, OrmException {
+      throws AuthException, OrmException, RepositoryNotFoundException,
+      IOException, ConfigInvalidException {
     if (self.get() != rsrc.getUser()
         && !self.get().getCapabilities().canAdministrateServer()) {
       throw new AuthException("not allowed to delete SSH keys");
     }
-    dbProvider.get().accountSshKeys()
-        .deleteKeys(Collections.singleton(rsrc.getSshKey().getKey()));
+
+    authorizedKeys.deleteKey(rsrc.getUser().getAccountId(),
+        rsrc.getSshKey().getKey().get());
     sshKeyCache.evict(rsrc.getUser().getUserName());
+
     return Response.none();
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteWatchedProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteWatchedProjects.java
new file mode 100644
index 0000000..b91378e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteWatchedProjects.java
@@ -0,0 +1,78 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.gerrit.server.account;
+
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+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.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+
+@Singleton
+public class DeleteWatchedProjects
+    implements RestModifyView<AccountResource, List<String>> {
+
+  private final Provider<ReviewDb> dbProvider;
+  private final Provider<IdentifiedUser> self;
+
+  @Inject
+  DeleteWatchedProjects(Provider<ReviewDb> dbProvider,
+      Provider<IdentifiedUser> self) {
+    this.dbProvider = dbProvider;
+    this.self = self;
+  }
+
+  @Override
+  public Response<?> apply(
+      AccountResource rsrc, List<String> input)
+      throws UnprocessableEntityException, OrmException, AuthException {
+    if (self.get() != rsrc.getUser()) {
+      throw new AuthException("It is not allowed to edit project watches "
+          + "of other users");
+    }
+    ResultSet<AccountProjectWatch> watchedProjects =
+        dbProvider.get().accountProjectWatches()
+            .byAccount(rsrc.getUser().getAccountId());
+    HashMap<String, AccountProjectWatch> watchedProjectsMap = new HashMap<>();
+    for (AccountProjectWatch watchedProject : watchedProjects) {
+      watchedProjectsMap
+          .put(watchedProject.getProjectNameKey().get(), watchedProject);
+    }
+
+    if (input != null) {
+      List<AccountProjectWatch.Key> keysToDelete = new LinkedList<>();
+      for (String projectKeyToDelete : input) {
+        if (!watchedProjectsMap.containsKey(projectKeyToDelete)) {
+          throw new UnprocessableEntityException(projectKeyToDelete
+              + " is not currently watched by this user.");
+        }
+        keysToDelete.add(watchedProjectsMap.get(projectKeyToDelete).getKey());
+      }
+      dbProvider.get().accountProjectWatches().deleteKeys(keysToDelete);
+    }
+
+    return Response.none();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/EmailExpander.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/EmailExpander.java
index 166f97e..75408c8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/EmailExpander.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/EmailExpander.java
@@ -23,7 +23,7 @@
 
   String expand(String user);
 
-  public static class None implements EmailExpander {
+  class None implements EmailExpander {
     public static final None INSTANCE = new None();
 
     public static boolean canHandle(final String fmt) {
@@ -44,7 +44,7 @@
     }
   }
 
-  public static class Simple implements EmailExpander {
+  class Simple implements EmailExpander {
     private static final String PLACEHOLDER = "{0}";
 
     public static boolean canHandle(final String fmt) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/FakeRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/FakeRealm.java
index f8d6bd1..d3b938f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/FakeRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/FakeRealm.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Account.FieldName;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 
 /** Fake implementation of {@link Realm} that does not communicate. */
 public class FakeRealm extends AbstractRealm {
@@ -31,16 +30,6 @@
   }
 
   @Override
-  public AuthRequest link(ReviewDb db, Account.Id to, AuthRequest who) {
-    return who;
-  }
-
-  @Override
-  public AuthRequest unlink(ReviewDb db, Account.Id to, AuthRequest who) {
-    return who;
-  }
-
-  @Override
   public void onCreateAccount(AuthRequest who, Account account) {
     // Do nothing.
   }
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 d231767..cbd0e32 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
@@ -33,8 +33,6 @@
 import static com.google.gerrit.common.data.GlobalCapability.VIEW_QUEUE;
 
 import com.google.common.collect.Iterables;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Sets;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.extensions.config.CapabilityDefinition;
@@ -54,7 +52,9 @@
 
 import org.kohsuke.args4j.Option;
 
+import java.util.HashSet;
 import java.util.Iterator;
+import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Set;
 
@@ -62,7 +62,7 @@
   @Option(name = "-q", metaVar = "CAP", usage = "Capability to inspect")
   void addQuery(String name) {
     if (query == null) {
-      query = Sets.newHashSet();
+      query = new HashSet<>();
     }
     Iterables.addAll(query, OptionUtil.splitOptionValue(name));
   }
@@ -86,7 +86,7 @@
     }
 
     CapabilityControl cc = resource.getUser().getCapabilities();
-    Map<String, Object> have = Maps.newLinkedHashMap();
+    Map<String, Object> have = new LinkedHashMap<>();
     for (String name : GlobalCapability.getAllNames()) {
       if (!name.equals(PRIORITY) && want(name) && cc.canPerform(name)) {
         if (GlobalCapability.hasRange(name)) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEditPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEditPreferences.java
index cdc2044..02cfaa0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEditPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEditPreferences.java
@@ -74,4 +74,4 @@
           new EditPreferencesInfo(), EditPreferencesInfo.defaults(), in);
     }
   }
-}
\ No newline at end of file
+}
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 22ce4b2..14cc74e 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
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.inject.Singleton;
 
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
@@ -27,7 +27,7 @@
 
   @Override
   public List<EmailInfo> apply(AccountResource rsrc) {
-    List<EmailInfo> emails = Lists.newArrayList();
+    List<EmailInfo> emails = new ArrayList<>();
     for (String email : rsrc.getUser().getEmailAddresses()) {
       if (email != null) {
         EmailInfo e = new EmailInfo();
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 f777145..5b71e0b 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
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.common.collect.Lists;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.RestReadView;
@@ -26,6 +25,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
+import java.util.ArrayList;
 import java.util.List;
 
 @Singleton
@@ -43,7 +43,7 @@
   public List<GroupInfo> apply(AccountResource resource) throws OrmException {
     IdentifiedUser user = resource.getUser();
     Account.Id userId = user.getAccountId();
-    List<GroupInfo> groups = Lists.newArrayList();
+    List<GroupInfo> groups = new ArrayList<>();
     for (AccountGroup.UUID uuid : user.getEffectiveGroups().getKnownGroups()) {
       GroupControl ctl;
       try {
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 e1668f2..bf1a3af 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
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.common.base.Function;
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.common.SshKeyInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.AccountSshKey;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gwtorm.server.OrmException;
@@ -28,23 +28,29 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+
+import java.io.IOException;
 import java.util.List;
 
 @Singleton
 public class GetSshKeys implements RestReadView<AccountResource> {
 
   private final Provider<CurrentUser> self;
-  private final Provider<ReviewDb> dbProvider;
+  private final VersionedAuthorizedKeys.Accessor authorizedKeys;
 
   @Inject
-  GetSshKeys(Provider<CurrentUser> self, Provider<ReviewDb> dbProvider) {
+  GetSshKeys(Provider<CurrentUser> self,
+      VersionedAuthorizedKeys.Accessor authorizedKeys) {
     this.self = self;
-    this.dbProvider = dbProvider;
+    this.authorizedKeys = authorizedKeys;
   }
 
   @Override
-  public List<SshKeyInfo> apply(AccountResource rsrc) throws AuthException,
-      OrmException {
+  public List<SshKeyInfo> apply(AccountResource rsrc)
+      throws AuthException, OrmException, RepositoryNotFoundException,
+      IOException, ConfigInvalidException {
     if (self.get() != rsrc.getUser()
         && !self.get().getCapabilities().canModifyAccount()) {
       throw new AuthException("not allowed to get SSH keys");
@@ -52,13 +58,15 @@
     return apply(rsrc.getUser());
   }
 
-  public List<SshKeyInfo> apply(IdentifiedUser user) throws OrmException {
-    List<SshKeyInfo> sshKeys = Lists.newArrayList();
-    for (AccountSshKey sshKey : dbProvider.get().accountSshKeys()
-        .byAccount(user.getAccountId()).toList()) {
-      sshKeys.add(newSshKeyInfo(sshKey));
-    }
-    return sshKeys;
+  public List<SshKeyInfo> apply(IdentifiedUser user)
+      throws RepositoryNotFoundException, IOException, ConfigInvalidException {
+    return Lists.transform(authorizedKeys.getKeys(user.getAccountId()),
+        new Function<AccountSshKey, SshKeyInfo>() {
+          @Override
+          public SshKeyInfo apply(AccountSshKey key) {
+            return newSshKeyInfo(key);
+          }
+        });
   }
 
   public static SshKeyInfo newSshKeyInfo(AccountSshKey sshKey) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetWatchedProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetWatchedProjects.java
new file mode 100644
index 0000000..fd00b7a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetWatchedProjects.java
@@ -0,0 +1,78 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.gerrit.server.account;
+
+import com.google.gerrit.extensions.client.ProjectWatchInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import java.util.LinkedList;
+import java.util.List;
+
+@Singleton
+public class GetWatchedProjects implements RestReadView<AccountResource> {
+
+  private final Provider<ReviewDb> dbProvider;
+  private final Provider<IdentifiedUser> self;
+
+  @Inject
+  public GetWatchedProjects(Provider<ReviewDb> dbProvider,
+      Provider<IdentifiedUser> self) {
+    this.dbProvider = dbProvider;
+    this.self = self;
+  }
+
+  @Override
+  public List<ProjectWatchInfo> apply(AccountResource rsrc)
+      throws OrmException, AuthException {
+    if (self.get() != rsrc.getUser()) {
+      throw new AuthException("It is not allowed to list project watches "
+          + "of other users");
+    }
+    List<ProjectWatchInfo> projectWatchInfos = new LinkedList<>();
+    Iterable<AccountProjectWatch> projectWatches =
+        dbProvider.get().accountProjectWatches()
+            .byAccount(rsrc.getUser().getAccountId());
+    for (AccountProjectWatch a : projectWatches) {
+      ProjectWatchInfo pwi = new ProjectWatchInfo();
+      pwi.filter = a.getFilter();
+      pwi.project = a.getProjectNameKey().get();
+      pwi.notifyAbandonedChanges =
+          toBoolean(
+              a.isNotify(AccountProjectWatch.NotifyType.ABANDONED_CHANGES));
+      pwi.notifyNewChanges =
+          toBoolean(a.isNotify(AccountProjectWatch.NotifyType.NEW_CHANGES));
+      pwi.notifyNewPatchSets =
+          toBoolean(a.isNotify(AccountProjectWatch.NotifyType.NEW_PATCHSETS));
+      pwi.notifySubmittedChanges =
+          toBoolean(
+              a.isNotify(AccountProjectWatch.NotifyType.SUBMITTED_CHANGES));
+      pwi.notifyAllComments =
+          toBoolean(a.isNotify(AccountProjectWatch.NotifyType.ALL_COMMENTS));
+      projectWatchInfos.add(pwi);
+    }
+    return projectWatchInfos;
+  }
+
+  private static Boolean toBoolean(boolean value) {
+    return value ? true : null;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
index bf04234..e5e2f99 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
@@ -95,7 +95,7 @@
       Optional<AccountGroup> g = byId.get(groupId);
       return g.isPresent() ? g.get() : missing(groupId);
     } catch (ExecutionException e) {
-      log.warn("Cannot load group "+groupId, e);
+      log.warn("Cannot load group " + groupId, e);
       return missing(groupId);
     }
   }
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 713cbda..94feb7d 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
@@ -23,7 +23,7 @@
 import com.google.gerrit.reviewdb.client.AccountGroupById;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.group.GroupInfoCacheFactory;
+import com.google.gerrit.server.group.GroupInfoCache;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -44,18 +44,19 @@
   private final GroupCache groupCache;
   private final GroupBackend groupBackend;
   private final AccountInfoCacheFactory aic;
-  private final GroupInfoCacheFactory gic;
+  private final GroupInfoCache gic;
 
   private final AccountGroup.Id groupId;
   private GroupControl control;
 
   @Inject
-  GroupDetailFactory(final ReviewDb db,
-      final GroupControl.Factory groupControl, final GroupCache groupCache,
-      final GroupBackend groupBackend,
-      final AccountInfoCacheFactory.Factory accountInfoCacheFactory,
-      final GroupInfoCacheFactory.Factory groupInfoCacheFactory,
-      @Assisted final AccountGroup.Id groupId) {
+  GroupDetailFactory(ReviewDb db,
+      GroupControl.Factory groupControl,
+      GroupCache groupCache,
+      GroupBackend groupBackend,
+      AccountInfoCacheFactory.Factory accountInfoCacheFactory,
+      GroupInfoCache.Factory groupInfoCacheFactory,
+      @Assisted AccountGroup.Id groupId) {
     this.db = db;
     this.groupControl = groupControl;
     this.groupCache = groupCache;
@@ -69,8 +70,8 @@
   @Override
   public GroupDetail call() throws OrmException, NoSuchGroupException {
     control = groupControl.validateFor(groupId);
-    final AccountGroup group = groupCache.get(groupId);
-    final GroupDetail detail = new GroupDetail();
+    AccountGroup group = groupCache.get(groupId);
+    GroupDetail detail = new GroupDetail();
     detail.setGroup(group);
     GroupDescription.Basic ownerGroup = groupBackend.get(group.getOwnerGroupUUID());
     if (ownerGroup != null) {
@@ -85,7 +86,7 @@
 
   private List<AccountGroupMember> loadMembers() throws OrmException {
     List<AccountGroupMember> members = new ArrayList<>();
-    for (final AccountGroupMember m : db.accountGroupMembers().byGroup(groupId)) {
+    for (AccountGroupMember m : db.accountGroupMembers().byGroup(groupId)) {
       if (control.canSeeMember(m.getAccountId())) {
         aic.want(m.getAccountId());
         members.add(m);
@@ -94,10 +95,9 @@
 
     Collections.sort(members, new Comparator<AccountGroupMember>() {
       @Override
-      public int compare(final AccountGroupMember o1,
-          final AccountGroupMember o2) {
-        final Account a = aic.get(o1.getAccountId());
-        final Account b = aic.get(o2.getAccountId());
+      public int compare(AccountGroupMember o1, AccountGroupMember o2) {
+        Account a = aic.get(o1.getAccountId());
+        Account b = aic.get(o2.getAccountId());
         return n(a).compareTo(n(b));
       }
 
@@ -121,7 +121,7 @@
   private List<AccountGroupById> loadIncludes() throws OrmException {
     List<AccountGroupById> groups = new ArrayList<>();
 
-    for (final AccountGroupById m : db.accountGroupById().byGroup(groupId)) {
+    for (AccountGroupById m : db.accountGroupById().byGroup(groupId)) {
       if (control.canSeeGroup()) {
         gic.want(m.getIncludeUUID());
         groups.add(m);
@@ -130,8 +130,7 @@
 
     Collections.sort(groups, new Comparator<AccountGroupById>() {
       @Override
-      public int compare(final AccountGroupById o1,
-          final AccountGroupById o2) {
+      public int compare(AccountGroupById o1, AccountGroupById o2) {
         GroupDescription.Basic a = gic.get(o1.getIncludeUUID());
         GroupDescription.Basic b = gic.get(o2.getIncludeUUID());
         return n(a).compareTo(n(b));
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 4b56b81..9bd6b30 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
@@ -17,7 +17,6 @@
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Sets;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupById;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -33,6 +32,7 @@
 import org.slf4j.LoggerFactory;
 
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
@@ -150,7 +150,7 @@
           return Collections.emptySet();
         }
 
-        Set<AccountGroup.UUID> ids = Sets.newHashSet();
+        Set<AccountGroup.UUID> ids = new HashSet<>();
         for (AccountGroupById agi : db.accountGroupById()
             .byGroup(group.get(0).getId())) {
           ids.add(agi.getIncludeUUID());
@@ -172,13 +172,13 @@
     @Override
     public Set<AccountGroup.UUID> load(AccountGroup.UUID key) throws Exception {
       try (ReviewDb db = schema.open()) {
-        Set<AccountGroup.Id> ids = Sets.newHashSet();
+        Set<AccountGroup.Id> ids = new HashSet<>();
         for (AccountGroupById agi : db.accountGroupById()
             .byIncludeUUID(key)) {
           ids.add(agi.getGroupId());
         }
 
-        Set<AccountGroup.UUID> groupArray = Sets.newHashSet();
+        Set<AccountGroup.UUID> groupArray = new HashSet<>();
         for (AccountGroup g : db.accountGroups().get(ids)) {
           groupArray.add(g.getGroupUUID());
         }
@@ -199,7 +199,7 @@
     @Override
     public Set<AccountGroup.UUID> load(String key) throws Exception {
       try (ReviewDb db = schema.open()) {
-        Set<AccountGroup.UUID> ids = Sets.newHashSet();
+        Set<AccountGroup.UUID> ids = new HashSet<>();
         for (AccountGroupById agi : db.accountGroupById().all()) {
           if (!AccountGroup.isInternalGroup(agi.getIncludeUUID())) {
             ids.add(agi.getIncludeUUID());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembership.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembership.java
index d7a97fb..c45b7b7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembership.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembership.java
@@ -24,7 +24,7 @@
  * the presence of a user in a particular group.
  */
 public interface GroupMembership {
-  public static final GroupMembership EMPTY =
+  GroupMembership EMPTY =
       new ListGroupMembership(Collections.<AccountGroup.UUID>emptySet());
 
   /**
@@ -46,7 +46,7 @@
    * Implementors may implement the method as:
    *
    * <pre>
-   * Set&lt;AccountGroup.UUID&gt; r = Sets.newHashSet();
+   * Set&lt;AccountGroup.UUID&gt; r = new HashSet&lt;&gt;();
    * for (AccountGroup.UUID id : groupIds)
    *   if (contains(id)) r.add(id);
    * </pre>
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 b8a67ff..3eaeebe 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
@@ -22,6 +22,7 @@
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -102,7 +103,7 @@
 
   @Override
   public Set<AccountGroup.UUID> intersection(Iterable<AccountGroup.UUID> groupIds) {
-    Set<AccountGroup.UUID> r = Sets.newHashSet();
+    Set<AccountGroup.UUID> r = new HashSet<>();
     for (AccountGroup.UUID id : groupIds) {
       if (contains(id)) {
         r.add(id);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalAccountDirectory.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalAccountDirectory.java
index 4f8eacd..961d554 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalAccountDirectory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalAccountDirectory.java
@@ -12,12 +12,10 @@
 // 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.common.collect.ArrayListMultimap;
-import com.google.common.collect.Lists;
 import com.google.common.collect.Multimap;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.AvatarInfo;
@@ -32,6 +30,7 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.EnumSet;
 import java.util.Set;
@@ -123,17 +122,36 @@
     if (options.contains(FillOptions.AVATARS)) {
       AvatarProvider ap = avatar.get();
       if (ap != null) {
-        info.avatars = Lists.newArrayListWithCapacity(1);
-        String u = ap.getUrl(
-            userFactory.create(account.getId()),
-            AvatarInfo.DEFAULT_SIZE);
-        if (u != null) {
-          AvatarInfo a = new AvatarInfo();
-          a.url = u;
-          a.height = AvatarInfo.DEFAULT_SIZE;
-          info.avatars.add(a);
+        info.avatars = new ArrayList<>(3);
+        IdentifiedUser user = userFactory.create(account.getId());
+
+        // GWT UI uses DEFAULT_SIZE (26px).
+        addAvatar(ap, info, user, AvatarInfo.DEFAULT_SIZE);
+
+        // PolyGerrit UI prefers 32px and 100px.
+        if (!info.avatars.isEmpty()) {
+          if (32 != AvatarInfo.DEFAULT_SIZE) {
+            addAvatar(ap, info, user, 32);
+          }
+          if (100 != AvatarInfo.DEFAULT_SIZE) {
+            addAvatar(ap, info, user, 100);
+          }
         }
       }
     }
   }
+
+  private static void addAvatar(
+      AvatarProvider provider,
+      AccountInfo account,
+      IdentifiedUser user,
+      int size) {
+    String url = provider.getUrl(user, size);
+    if (url != null) {
+      AvatarInfo avatar = new AvatarInfo();
+      avatar.url = url;
+      avatar.height = size;
+      account.avatars.add(avatar);
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java
index 54d4cc0..52e9d4c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java
@@ -19,6 +19,7 @@
 import static com.google.gerrit.server.account.AccountResource.EMAIL_KIND;
 import static com.google.gerrit.server.account.AccountResource.SSH_KEY_KIND;
 import static com.google.gerrit.server.account.AccountResource.STARRED_CHANGE_KIND;
+import static com.google.gerrit.server.account.AccountResource.Star.STAR_KIND;
 
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.RestApiModule;
@@ -34,6 +35,7 @@
     DynamicMap.mapOf(binder(), EMAIL_KIND);
     DynamicMap.mapOf(binder(), SSH_KEY_KIND);
     DynamicMap.mapOf(binder(), STARRED_CHANGE_KIND);
+    DynamicMap.mapOf(binder(), STAR_KIND);
 
     put(ACCOUNT_KIND).to(PutAccount.class);
     get(ACCOUNT_KIND).to(GetAccount.class);
@@ -56,6 +58,10 @@
     delete(ACCOUNT_KIND, "password.http").to(PutHttpPassword.class);
     child(ACCOUNT_KIND, "sshkeys").to(SshKeys.class);
     post(ACCOUNT_KIND, "sshkeys").to(AddSshKey.class);
+    get(ACCOUNT_KIND, "watched.projects").to(GetWatchedProjects.class);
+    post(ACCOUNT_KIND, "watched.projects").to(PostWatchedProjects.class);
+    post(ACCOUNT_KIND, "watched.projects:delete")
+        .to(DeleteWatchedProjects.class);
 
     get(SSH_KEY_KIND).to(GetSshKey.class);
     delete(SSH_KEY_KIND).to(DeleteSshKey.class);
@@ -79,6 +85,10 @@
     delete(STARRED_CHANGE_KIND).to(StarredChanges.Delete.class);
     bind(StarredChanges.Create.class);
 
+    child(ACCOUNT_KIND, "stars.changes").to(Stars.class);
+    get(STAR_KIND).to(Stars.Get.class);
+    post(STAR_KIND).to(Stars.Post.class);
+
     factory(CreateAccount.Factory.class);
     factory(CreateEmail.Factory.class);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PostWatchedProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PostWatchedProjects.java
new file mode 100644
index 0000000..cf67ad9
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PostWatchedProjects.java
@@ -0,0 +1,102 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.gerrit.server.account;
+
+import com.google.gerrit.extensions.client.ProjectWatchInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.project.ProjectsCollection;
+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.LinkedList;
+import java.util.List;
+
+@Singleton
+public class PostWatchedProjects
+    implements RestModifyView<AccountResource, List<ProjectWatchInfo>> {
+  private final Provider<IdentifiedUser> self;
+  private GetWatchedProjects getWatchedProjects;
+  private Provider<ReviewDb> dbProvider;
+  private ProjectsCollection projectsCollection;
+
+  @Inject
+  public PostWatchedProjects(GetWatchedProjects getWatchedProjects,
+      Provider<ReviewDb> dbProvider,
+      ProjectsCollection projectsCollection,
+      Provider<IdentifiedUser> self) {
+    this.getWatchedProjects = getWatchedProjects;
+    this.dbProvider = dbProvider;
+    this.projectsCollection = projectsCollection;
+    this.self = self;
+  }
+
+  @Override
+  public List<ProjectWatchInfo> apply(AccountResource rsrc,
+      List<ProjectWatchInfo> input)
+      throws OrmException, RestApiException, IOException {
+    if (self.get() != rsrc.getUser()) {
+      throw new AuthException("not allowed to edit project watches");
+    }
+    List<AccountProjectWatch> accountProjectWatchList =
+        getAccountProjectWatchList(input, rsrc.getUser().getAccountId());
+    dbProvider.get().accountProjectWatches().upsert(accountProjectWatchList);
+    return getWatchedProjects.apply(rsrc);
+  }
+
+  private List<AccountProjectWatch> getAccountProjectWatchList(
+      List<ProjectWatchInfo> input, Account.Id accountId)
+      throws UnprocessableEntityException, BadRequestException, IOException {
+    List<AccountProjectWatch> watchedProjects = new LinkedList<>();
+    for (ProjectWatchInfo a : input) {
+      if (a.project == null) {
+        throw new BadRequestException("project name must be specified");
+      }
+
+      Project.NameKey projectKey =
+          projectsCollection.parse(a.project).getNameKey();
+
+      AccountProjectWatch.Key key =
+          new AccountProjectWatch.Key(accountId, projectKey, a.filter);
+      AccountProjectWatch apw = new AccountProjectWatch(key);
+      apw.setNotify(AccountProjectWatch.NotifyType.ABANDONED_CHANGES,
+          toBoolean(a.notifyAbandonedChanges));
+      apw.setNotify(AccountProjectWatch.NotifyType.ALL_COMMENTS,
+          toBoolean(a.notifyAllComments));
+      apw.setNotify(AccountProjectWatch.NotifyType.NEW_CHANGES,
+          toBoolean(a.notifyNewChanges));
+      apw.setNotify(AccountProjectWatch.NotifyType.NEW_PATCHSETS,
+          toBoolean(a.notifyNewPatchSets));
+      apw.setNotify(AccountProjectWatch.NotifyType.SUBMITTED_CHANGES,
+          toBoolean(a.notifySubmittedChanges));
+      watchedProjects.add(apw);
+    }
+    return watchedProjects;
+  }
+
+  private boolean toBoolean(Boolean b) {
+    return b == null ? false : b;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
index 6fd76d4..85fde4e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.account;
 
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 
 import java.util.Set;
@@ -29,12 +28,6 @@
 
   AuthRequest authenticate(AuthRequest who) throws AccountException;
 
-  AuthRequest link(ReviewDb db, Account.Id to, AuthRequest who)
-      throws AccountException;
-
-  AuthRequest unlink(ReviewDb db, Account.Id to, AuthRequest who)
-      throws AccountException;
-
   void onCreateAccount(AuthRequest who, Account account);
 
   /** @return true if the user has the given email address. */
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 b70cabd..eb32e5a 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
@@ -140,7 +140,7 @@
     }
   }
 
-  public static void storeUrlAliases(VersionedAccountPreferences prefs,
+  private static void storeUrlAliases(VersionedAccountPreferences prefs,
       Map<String, String> urlAliases) {
     if (urlAliases != null) {
       Config cfg = prefs.getConfig();
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 b35c03e..44a3192 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
@@ -20,7 +20,6 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.reviewdb.client.AccountSshKey;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gwtorm.server.OrmException;
@@ -28,22 +27,26 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+import java.io.IOException;
+
 @Singleton
 public class SshKeys implements
     ChildCollection<AccountResource, AccountResource.SshKey> {
   private final DynamicMap<RestView<AccountResource.SshKey>> views;
   private final GetSshKeys list;
   private final Provider<CurrentUser> self;
-  private final Provider<ReviewDb> dbProvider;
+  private final VersionedAuthorizedKeys.Accessor authorizedKeys;
 
   @Inject
   SshKeys(DynamicMap<RestView<AccountResource.SshKey>> views,
       GetSshKeys list, Provider<CurrentUser> self,
-      Provider<ReviewDb> dbProvider) {
+      VersionedAuthorizedKeys.Accessor authorizedKeys) {
     this.views = views;
     this.list = list;
     this.self = self;
-    this.dbProvider = dbProvider;
+    this.authorizedKeys = authorizedKeys;
   }
 
   @Override
@@ -53,7 +56,8 @@
 
   @Override
   public AccountResource.SshKey parse(AccountResource rsrc, IdString id)
-      throws ResourceNotFoundException, OrmException {
+      throws ResourceNotFoundException, OrmException, IOException,
+      ConfigInvalidException {
     if (self.get() != rsrc.getUser()
         && !self.get().getCapabilities().canModifyAccount()) {
       throw new ResourceNotFoundException();
@@ -62,12 +66,10 @@
   }
 
   public AccountResource.SshKey parse(IdentifiedUser user, IdString id)
-      throws ResourceNotFoundException, OrmException {
+      throws ResourceNotFoundException, IOException, ConfigInvalidException {
     try {
       int seq = Integer.parseInt(id.get(), 10);
-      AccountSshKey sshKey =
-          dbProvider.get().accountSshKeys()
-              .get(new AccountSshKey.Id(user.getAccountId(), seq));
+      AccountSshKey sshKey = authorizedKeys.getKey(user.getAccountId(), seq);
       if (sshKey == null) {
         throw new ResourceNotFoundException(id);
       }
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 e69bc0f..b71fc68 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
@@ -42,6 +42,8 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.IOException;
+
 @Singleton
 public class StarredChanges implements
     ChildCollection<AccountResource, AccountResource.StarredChange>,
@@ -51,31 +53,29 @@
   private final ChangesCollection changes;
   private final DynamicMap<RestView<AccountResource.StarredChange>> views;
   private final Provider<Create> createProvider;
+  private final StarredChangesUtil starredChangesUtil;
 
   @Inject
   StarredChanges(ChangesCollection changes,
       DynamicMap<RestView<AccountResource.StarredChange>> views,
-      Provider<Create> createProvider) {
+      Provider<Create> createProvider,
+      StarredChangesUtil starredChangesUtil) {
     this.changes = changes;
     this.views = views;
     this.createProvider = createProvider;
+    this.starredChangesUtil = starredChangesUtil;
   }
 
   @Override
   public AccountResource.StarredChange parse(AccountResource parent, IdString id)
       throws ResourceNotFoundException, OrmException {
     IdentifiedUser user = parent.getUser();
-    try {
-      user.asyncStarredChanges();
-
-      ChangeResource change = changes.parse(TopLevelResource.INSTANCE, id);
-      if (user.getStarredChanges().contains(change.getId())) {
-        return new AccountResource.StarredChange(user, change);
-      }
-      throw new ResourceNotFoundException(id);
-    } finally {
-      user.abortStarredChanges();
+    ChangeResource change = changes.parse(TopLevelResource.INSTANCE, id);
+    if (starredChangesUtil.getLabels(user.getAccountId(), change.getId())
+        .contains(StarredChangesUtil.DEFAULT_LABEL)) {
+      return new AccountResource.StarredChange(user, change);
     }
+    throw new ResourceNotFoundException(id);
   }
 
   @Override
@@ -99,7 +99,7 @@
   @SuppressWarnings("unchecked")
   @Override
   public RestModifyView<AccountResource, EmptyInput> create(
-      AccountResource parent, IdString id) throws UnprocessableEntityException{
+      AccountResource parent, IdString id) throws UnprocessableEntityException {
     try {
       return createProvider.get()
           .setChange(changes.parse(TopLevelResource.INSTANCE, id));
@@ -130,12 +130,13 @@
 
     @Override
     public Response<?> apply(AccountResource rsrc, EmptyInput in)
-        throws AuthException, OrmException {
+        throws AuthException, OrmException, IOException {
       if (self.get() != rsrc.getUser()) {
         throw new AuthException("not allowed to add starred change");
       }
       try {
-        starredChangesUtil.star(self.get().getAccountId(), change.getId());
+        starredChangesUtil.star(self.get().getAccountId(), change.getProject(),
+            change.getId(), StarredChangesUtil.DEFAULT_LABELS, null);
       } catch (OrmDuplicateKeyException e) {
         return Response.none();
       }
@@ -177,12 +178,13 @@
 
     @Override
     public Response<?> apply(AccountResource.StarredChange rsrc,
-        EmptyInput in) throws AuthException, OrmException {
+        EmptyInput in) throws AuthException, OrmException, IOException {
       if (self.get() != rsrc.getUser()) {
         throw new AuthException("not allowed remove starred change");
       }
-      starredChangesUtil.unstar(self.get().getAccountId(),
-          rsrc.getChange().getId());
+      starredChangesUtil.star(self.get().getAccountId(),
+          rsrc.getChange().getProject(), rsrc.getChange().getId(), null,
+          StarredChangesUtil.DEFAULT_LABELS);
       return Response.none();
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Stars.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Stars.java
new file mode 100644
index 0000000..fddbb6a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Stars.java
@@ -0,0 +1,167 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.gerrit.extensions.api.changes.StarsInput;
+import com.google.gerrit.extensions.common.ChangeInfo;
+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.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.StarredChangesUtil;
+import com.google.gerrit.server.StarredChangesUtil.IllegalLabelException;
+import com.google.gerrit.server.account.AccountResource.Star;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.ChangesCollection;
+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.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.SortedSet;
+
+@Singleton
+public class Stars implements
+    ChildCollection<AccountResource, AccountResource.Star> {
+
+  private final ChangesCollection changes;
+  private final ListStarredChanges listStarredChanges;
+  private final StarredChangesUtil starredChangesUtil;
+  private final DynamicMap<RestView<AccountResource.Star>> views;
+
+  @Inject
+  Stars(ChangesCollection changes,
+      ListStarredChanges listStarredChanges,
+      StarredChangesUtil starredChangesUtil,
+      DynamicMap<RestView<AccountResource.Star>> views) {
+    this.changes = changes;
+    this.listStarredChanges = listStarredChanges;
+    this.starredChangesUtil = starredChangesUtil;
+    this.views = views;
+  }
+
+  @Override
+  public Star parse(AccountResource parent, IdString id)
+      throws ResourceNotFoundException, OrmException {
+    IdentifiedUser user = parent.getUser();
+    ChangeResource change = changes.parse(TopLevelResource.INSTANCE, id);
+    Set<String> labels =
+        starredChangesUtil.getLabels(user.getAccountId(), change.getId());
+    return new AccountResource.Star(user, change, labels);
+  }
+
+  @Override
+  public DynamicMap<RestView<Star>> views() {
+    return views;
+  }
+
+  @Override
+  public ListStarredChanges list() {
+    return listStarredChanges;
+  }
+
+  @Singleton
+  public static class ListStarredChanges
+      implements RestReadView<AccountResource> {
+    private final Provider<CurrentUser> self;
+    private final ChangesCollection changes;
+
+    @Inject
+    ListStarredChanges(Provider<CurrentUser> self,
+        ChangesCollection changes) {
+      this.self = self;
+      this.changes = changes;
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public List<ChangeInfo> apply(AccountResource rsrc)
+        throws BadRequestException, AuthException, OrmException {
+      if (self.get() != rsrc.getUser()) {
+        throw new AuthException(
+            "not allowed to list stars of another account");
+      }
+      QueryChanges query = changes.list();
+      query.addQuery("has:stars");
+      return (List<ChangeInfo>) query.apply(TopLevelResource.INSTANCE);
+    }
+  }
+
+  @Singleton
+  public static class Get implements
+      RestReadView<AccountResource.Star> {
+    private final Provider<CurrentUser> self;
+    private final StarredChangesUtil starredChangesUtil;
+
+    @Inject
+    Get(Provider<CurrentUser> self,
+        StarredChangesUtil starredChangesUtil) {
+      this.self = self;
+      this.starredChangesUtil = starredChangesUtil;
+    }
+
+    @Override
+    public SortedSet<String> apply(AccountResource.Star rsrc)
+        throws AuthException, OrmException {
+      if (self.get() != rsrc.getUser()) {
+        throw new AuthException("not allowed to get stars of another account");
+      }
+      return starredChangesUtil.getLabels(self.get().getAccountId(),
+          rsrc.getChange().getId());
+    }
+  }
+
+  @Singleton
+  public static class Post implements
+      RestModifyView<AccountResource.Star, StarsInput> {
+    private final Provider<CurrentUser> self;
+    private final StarredChangesUtil starredChangesUtil;
+
+    @Inject
+    Post(Provider<CurrentUser> self,
+        StarredChangesUtil starredChangesUtil) {
+      this.self = self;
+      this.starredChangesUtil = starredChangesUtil;
+    }
+
+    @Override
+    public Collection<String> apply(AccountResource.Star rsrc, StarsInput in)
+        throws AuthException, BadRequestException, OrmException {
+      if (self.get() != rsrc.getUser()) {
+        throw new AuthException(
+            "not allowed to update stars of another account");
+      }
+      try {
+        return starredChangesUtil.star(self.get().getAccountId(),
+            rsrc.getChange().getProject(), rsrc.getChange().getId(), in.add,
+            in.remove);
+      } catch (IllegalLabelException e) {
+        throw new BadRequestException(e.getMessage());
+      }
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java
index 3450aff..3fccacce 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java
@@ -35,6 +35,7 @@
 import org.slf4j.LoggerFactory;
 
 import java.util.Collection;
+import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 
@@ -180,7 +181,7 @@
         }
         lookups.put(m, uuid);
       }
-      Set<AccountGroup.UUID> groups = Sets.newHashSet();
+      Set<AccountGroup.UUID> groups = new HashSet<>();
       for (Map.Entry<GroupMembership, Collection<AccountGroup.UUID>> entry
           : lookups.asMap().entrySet()) {
         groups.addAll(entry.getKey().intersection(entry.getValue()));
@@ -190,7 +191,7 @@
 
     @Override
     public Set<AccountGroup.UUID> getKnownGroups() {
-      Set<AccountGroup.UUID> groups = Sets.newHashSet();
+      Set<AccountGroup.UUID> groups = new HashSet<>();
       for (GroupMembership m : memberships.values()) {
         groups.addAll(m.getKnownGroups());
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
new file mode 100644
index 0000000..dc96d49
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
@@ -0,0 +1,327 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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 static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.base.Function;
+import com.google.common.base.Optional;
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Ordering;
+import com.google.gerrit.common.errors.InvalidSshKeyException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountSshKey;
+import com.google.gerrit.reviewdb.client.AccountSshKey.Id;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.AllUsersName;
+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.ssh.SshKeyCreator;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 'authorized_keys' file in the refs/users/CD/ABCD branches of the All-Users
+ * repository.
+ *
+ * The `authorized_keys' files stores the public SSH keys of the user. The file
+ * format matches the standard SSH file format, which means that each key is
+ * stored on a separate line (see
+ * https://en.wikibooks.org/wiki/OpenSSH/Client_Configuration_Files#.7E.2F.ssh.2Fauthorized_keys).
+ *
+ * The order of the keys in the file determines the sequence numbers of the
+ * keys. The first line corresponds to sequence number 1.
+ *
+ * Invalid keys are marked with the prefix <code># INVALID</code>.
+ *
+ * To keep the sequence numbers intact when a key is deleted, a
+ * <code># DELETED</code> line is inserted at the position where the key was
+ * deleted.
+ *
+ * Other comment lines are ignored on read, and are not written back when the
+ * file is modified.
+ */
+public class VersionedAuthorizedKeys extends VersionedMetaData
+    implements AutoCloseable {
+  @Singleton
+  public static class Accessor {
+    private final GitRepositoryManager repoManager;
+    private final AllUsersName allUsersName;
+    private final VersionedAuthorizedKeys.Factory authorizedKeysFactory;
+    private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
+    private final IdentifiedUser.GenericFactory userFactory;
+
+    @Inject
+    Accessor(
+        GitRepositoryManager repoManager,
+        AllUsersName allUsersName,
+        VersionedAuthorizedKeys.Factory authorizedKeysFactory,
+        Provider<MetaDataUpdate.User> metaDataUpdateFactory,
+        IdentifiedUser.GenericFactory userFactory) {
+      this.repoManager = repoManager;
+      this.allUsersName = allUsersName;
+      this.authorizedKeysFactory = authorizedKeysFactory;
+      this.metaDataUpdateFactory = metaDataUpdateFactory;
+      this.userFactory = userFactory;
+    }
+
+    public List<AccountSshKey> getKeys(Account.Id accountId)
+        throws IOException, ConfigInvalidException {
+      return read(accountId).getKeys();
+    }
+
+    public AccountSshKey getKey(Account.Id accountId, int seq)
+        throws IOException, ConfigInvalidException {
+      return read(accountId).getKey(seq);
+    }
+
+    public AccountSshKey addKey(Account.Id accountId, String pub)
+        throws IOException, ConfigInvalidException, InvalidSshKeyException {
+      try (VersionedAuthorizedKeys authorizedKeys = open(accountId)) {
+        AccountSshKey key = authorizedKeys.addKey(pub);
+        commit(authorizedKeys);
+        return key;
+      }
+    }
+
+    public void deleteKey(Account.Id accountId, int seq)
+        throws IOException, ConfigInvalidException {
+      try (VersionedAuthorizedKeys authorizedKeys = open(accountId)) {
+        if (authorizedKeys.deleteKey(seq)) {
+          commit(authorizedKeys);
+        }
+      }
+    }
+
+    public void markKeyInvalid(Account.Id accountId, int seq)
+        throws IOException, ConfigInvalidException {
+      try (VersionedAuthorizedKeys authorizedKeys = open(accountId)) {
+        if (authorizedKeys.markKeyInvalid(seq)) {
+          commit(authorizedKeys);
+        }
+      }
+    }
+
+    private VersionedAuthorizedKeys read(Account.Id accountId)
+        throws IOException, ConfigInvalidException {
+      try (Repository git = repoManager.openRepository(allUsersName)) {
+        VersionedAuthorizedKeys authorizedKeys =
+            authorizedKeysFactory.create(accountId);
+        authorizedKeys.load(git);
+        return authorizedKeys;
+      }
+    }
+
+    private VersionedAuthorizedKeys open(Account.Id accountId)
+        throws IOException, ConfigInvalidException {
+      Repository git = repoManager.openRepository(allUsersName);
+      VersionedAuthorizedKeys authorizedKeys =
+          authorizedKeysFactory.create(accountId);
+      authorizedKeys.load(git);
+      return authorizedKeys;
+    }
+
+    private void commit(VersionedAuthorizedKeys authorizedKeys)
+        throws IOException {
+      try (MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsersName,
+          userFactory.create(authorizedKeys.accountId))) {
+        authorizedKeys.commit(md);
+      }
+    }
+  }
+
+  public static class SimpleSshKeyCreator implements SshKeyCreator {
+    @Override
+    public AccountSshKey create(Id id, String encoded) {
+      return new AccountSshKey(id, encoded);
+    }
+  }
+
+  public interface Factory {
+    VersionedAuthorizedKeys create(Account.Id accountId);
+  }
+
+  private final SshKeyCreator sshKeyCreator;
+  private final Account.Id accountId;
+  private final String ref;
+  private Repository git;
+  private List<Optional<AccountSshKey>> keys;
+
+  @Inject
+  public VersionedAuthorizedKeys(
+      SshKeyCreator sshKeyCreator,
+      @Assisted Account.Id accountId) {
+    this.sshKeyCreator = sshKeyCreator;
+    this.accountId = accountId;
+    this.ref = RefNames.refsUsers(accountId);
+  }
+
+  @Override
+  protected String getRefName() {
+    return ref;
+  }
+
+  @Override
+  public void load(Repository git) throws IOException, ConfigInvalidException {
+    checkState(this.git == null);
+    this.git = git;
+    super.load(git);
+  }
+
+  @Override
+  protected void onLoad() throws IOException {
+    keys = AuthorizedKeys.parse(accountId, readUTF8(AuthorizedKeys.FILE_NAME));
+  }
+
+  @Override
+  protected boolean onSave(CommitBuilder commit) throws IOException {
+    if (Strings.isNullOrEmpty(commit.getMessage())) {
+      commit.setMessage("Updated SSH keys\n");
+    }
+
+    saveUTF8(AuthorizedKeys.FILE_NAME, AuthorizedKeys.serialize(keys));
+    return true;
+  }
+
+  /** Returns all SSH keys. */
+  private List<AccountSshKey> getKeys() {
+    checkLoaded();
+    return Lists.newArrayList(Optional.presentInstances(keys));
+  }
+
+  /**
+   * Returns the SSH key with the given sequence number.
+   *
+   * @param seq sequence number
+   * @return the SSH key, <code>null</code> if there is no SSH key with this
+   *         sequence number, or if the SSH key with this sequence number has
+   *         been deleted
+   */
+  private AccountSshKey getKey(int seq) {
+    checkLoaded();
+    Optional<AccountSshKey> key = keys.get(seq - 1);
+    return key.orNull();
+  }
+
+  /**
+   * Adds a new public SSH key.
+   *
+   * If the specified public key exists already, the existing key is returned.
+   *
+   * @param pub the public SSH key to be added
+   * @return the new SSH key
+   * @throws InvalidSshKeyException
+   */
+  private AccountSshKey addKey(String pub) throws InvalidSshKeyException {
+    checkLoaded();
+
+    for (Optional<AccountSshKey> key : keys) {
+      if (key.isPresent()
+          && key.get().getSshPublicKey().trim().equals(pub.trim())) {
+        return key.get();
+      }
+    }
+
+    int seq = keys.size() + 1;
+    AccountSshKey.Id keyId = new AccountSshKey.Id(accountId, seq);
+    AccountSshKey key = sshKeyCreator.create(keyId, pub);
+    keys.add(Optional.of(key));
+    return key;
+  }
+
+  /**
+   * Deletes the SSH key with the given sequence number.
+   *
+   * @param seq the sequence number
+   * @return <code>true</code> if a key with this sequence number was found and
+   *         deleted, <code>false</code> if no key with the given sequence
+   *         number exists
+   */
+  private boolean deleteKey(int seq) {
+    checkLoaded();
+    if (seq <= keys.size() && keys.get(seq - 1).isPresent()) {
+      keys.set(seq - 1, Optional.<AccountSshKey> absent());
+      return true;
+    }
+    return false;
+  }
+
+  /**
+   * Marks the SSH key with the given sequence number as invalid.
+   *
+   * @param seq the sequence number
+   * @return <code>true</code> if a key with this sequence number was found and
+   *         marked as invalid, <code>false</code> if no key with the given
+   *         sequence number exists or if the key was already marked as invalid
+   */
+  private boolean markKeyInvalid(int seq) {
+    checkLoaded();
+    AccountSshKey key = getKey(seq);
+    if (key != null && key.isValid()) {
+      key.setInvalid();
+      return true;
+    }
+    return false;
+  }
+
+  /**
+   * Sets new SSH keys.
+   *
+   * The existing SSH keys are overwritten.
+   *
+   * @param newKeys the new public SSH keys
+   */
+  public void setKeys(Collection<AccountSshKey> newKeys) {
+    Ordering<AccountSshKey> o =
+        Ordering.natural().onResultOf(new Function<AccountSshKey, Integer>() {
+          @Override
+          public Integer apply(AccountSshKey sshKey) {
+            return sshKey.getKey().get();
+          }
+        });
+    keys = new ArrayList<>(Collections.nCopies(o.max(newKeys).getKey().get(),
+        Optional.<AccountSshKey> absent()));
+    for (AccountSshKey key : newKeys) {
+      keys.set(key.getKey().get() - 1, Optional.of(key));
+    }
+  }
+
+  @Override
+  public void close() {
+    if (git != null) {
+      git.close();
+    }
+  }
+
+  private void checkLoaded() {
+    checkNotNull(keys, "SSH keys not loaded yet");
+  }
+}
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
index 55d077f..154780a 100644
--- 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
@@ -19,10 +19,13 @@
 import com.google.gerrit.extensions.api.accounts.AccountApi;
 import com.google.gerrit.extensions.api.accounts.EmailInput;
 import com.google.gerrit.extensions.api.accounts.GpgKeyApi;
+import com.google.gerrit.extensions.api.changes.StarsInput;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.client.EditPreferencesInfo;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
+import com.google.gerrit.extensions.client.ProjectWatchInfo;
 import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.GpgKeyInfo;
 import com.google.gerrit.extensions.common.SshKeyInfo;
 import com.google.gerrit.extensions.restapi.IdString;
@@ -33,20 +36,25 @@
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.AddSshKey;
 import com.google.gerrit.server.account.CreateEmail;
+import com.google.gerrit.server.account.DeleteSshKey;
+import com.google.gerrit.server.account.DeleteWatchedProjects;
 import com.google.gerrit.server.account.GetAvatar;
 import com.google.gerrit.server.account.GetDiffPreferences;
 import com.google.gerrit.server.account.GetEditPreferences;
 import com.google.gerrit.server.account.GetPreferences;
 import com.google.gerrit.server.account.GetSshKeys;
+import com.google.gerrit.server.account.GetWatchedProjects;
+import com.google.gerrit.server.account.PostWatchedProjects;
 import com.google.gerrit.server.account.SetDiffPreferences;
 import com.google.gerrit.server.account.SetEditPreferences;
 import com.google.gerrit.server.account.SetPreferences;
+import com.google.gerrit.server.account.SshKeys;
 import com.google.gerrit.server.account.StarredChanges;
+import com.google.gerrit.server.account.Stars;
 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.Provider;
 import com.google.inject.assistedinject.Assisted;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -54,6 +62,7 @@
 import java.io.IOException;
 import java.util.List;
 import java.util.Map;
+import java.util.SortedSet;
 
 public class AccountApiImpl implements AccountApi {
   interface Factory {
@@ -63,36 +72,52 @@
   private final AccountResource account;
   private final ChangesCollection changes;
   private final AccountLoader.Factory accountLoaderFactory;
-  private final Provider<GetAvatar> getAvatar;
+  private final GetAvatar getAvatar;
   private final GetPreferences getPreferences;
   private final SetPreferences setPreferences;
   private final GetDiffPreferences getDiffPreferences;
   private final SetDiffPreferences setDiffPreferences;
   private final GetEditPreferences getEditPreferences;
   private final SetEditPreferences setEditPreferences;
+  private final GetWatchedProjects getWatchedProjects;
+  private final PostWatchedProjects postWatchedProjects;
+  private final DeleteWatchedProjects deleteWatchedProjects;
   private final StarredChanges.Create starredChangesCreate;
   private final StarredChanges.Delete starredChangesDelete;
+  private final Stars stars;
+  private final Stars.Get starsGet;
+  private final Stars.Post starsPost;
   private final CreateEmail.Factory createEmailFactory;
   private final GpgApiAdapter gpgApiAdapter;
   private final GetSshKeys getSshKeys;
   private final AddSshKey addSshKey;
+  private final DeleteSshKey deleteSshKey;
+  private final SshKeys sshKeys;
 
   @Inject
   AccountApiImpl(AccountLoader.Factory ailf,
       ChangesCollection changes,
-      Provider<GetAvatar> getAvatar,
+      GetAvatar getAvatar,
       GetPreferences getPreferences,
       SetPreferences setPreferences,
       GetDiffPreferences getDiffPreferences,
       SetDiffPreferences setDiffPreferences,
       GetEditPreferences getEditPreferences,
       SetEditPreferences setEditPreferences,
+      GetWatchedProjects getWatchedProjects,
+      PostWatchedProjects postWatchedProjects,
+      DeleteWatchedProjects deleteWatchedProjects,
       StarredChanges.Create starredChangesCreate,
       StarredChanges.Delete starredChangesDelete,
+      Stars stars,
+      Stars.Get starsGet,
+      Stars.Post starsPost,
       CreateEmail.Factory createEmailFactory,
       GpgApiAdapter gpgApiAdapter,
       GetSshKeys getSshKeys,
       AddSshKey addSshKey,
+      DeleteSshKey deleteSshKey,
+      SshKeys sshKeys,
       @Assisted AccountResource account) {
     this.account = account;
     this.accountLoaderFactory = ailf;
@@ -104,11 +129,19 @@
     this.setDiffPreferences = setDiffPreferences;
     this.getEditPreferences = getEditPreferences;
     this.setEditPreferences = setEditPreferences;
+    this.getWatchedProjects = getWatchedProjects;
+    this.postWatchedProjects = postWatchedProjects;
+    this.deleteWatchedProjects = deleteWatchedProjects;
     this.starredChangesCreate = starredChangesCreate;
     this.starredChangesDelete = starredChangesDelete;
+    this.stars = stars;
+    this.starsGet = starsGet;
+    this.starsPost = starsPost;
     this.createEmailFactory = createEmailFactory;
     this.getSshKeys = getSshKeys;
     this.addSshKey = addSshKey;
+    this.deleteSshKey = deleteSshKey;
+    this.sshKeys = sshKeys;
     this.gpgApiAdapter = gpgApiAdapter;
   }
 
@@ -127,9 +160,8 @@
 
   @Override
   public String getAvatarUrl(int size) throws RestApiException {
-    GetAvatar myGetAvatar = getAvatar.get();
-    myGetAvatar.setSize(size);
-    return myGetAvatar.apply(account).location();
+    getAvatar.setSize(size);
+    return getAvatar.apply(account).location();
   }
 
   @Override
@@ -186,33 +218,94 @@
   }
 
   @Override
-  public void starChange(String id) throws RestApiException {
+  public List<ProjectWatchInfo> getWatchedProjects() throws RestApiException {
+    try {
+      return getWatchedProjects.apply(account);
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot get watched projects", e);
+    }
+  }
+
+  @Override
+  public List<ProjectWatchInfo> setWatchedProjects(
+      List<ProjectWatchInfo> in) throws RestApiException {
+    try {
+      return postWatchedProjects.apply(account, in);
+    } catch (OrmException | IOException e) {
+      throw new RestApiException("Cannot update watched projects", e);
+    }
+  }
+
+  @Override
+  public void deleteWatchedProjects(List<String> in)
+      throws RestApiException {
+    try {
+      deleteWatchedProjects.apply(account, in);
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot delete watched projects", e);
+    }
+  }
+
+  @Override
+  public void starChange(String changeId) throws RestApiException {
     try {
       ChangeResource rsrc = changes.parse(
         TopLevelResource.INSTANCE,
-        IdString.fromUrl(id));
+        IdString.fromUrl(changeId));
       starredChangesCreate.setChange(rsrc);
       starredChangesCreate.apply(account, new StarredChanges.EmptyInput());
-    } catch (OrmException e) {
+    } catch (OrmException | IOException e) {
       throw new RestApiException("Cannot star change", e);
     }
   }
 
   @Override
-  public void unstarChange(String id) throws RestApiException {
+  public void unstarChange(String changeId) throws RestApiException {
     try {
       ChangeResource rsrc =
-          changes.parse(TopLevelResource.INSTANCE, IdString.fromUrl(id));
+          changes.parse(TopLevelResource.INSTANCE, IdString.fromUrl(changeId));
       AccountResource.StarredChange starredChange =
           new AccountResource.StarredChange(account.getUser(), rsrc);
       starredChangesDelete.apply(starredChange,
           new StarredChanges.EmptyInput());
-    } catch (OrmException e) {
+    } catch (OrmException | IOException e) {
       throw new RestApiException("Cannot unstar change", e);
     }
   }
 
   @Override
+  public void setStars(String changeId, StarsInput input)
+      throws RestApiException {
+    try {
+      AccountResource.Star rsrc =
+          stars.parse(account, IdString.fromUrl(changeId));
+      starsPost.apply(rsrc, input);
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot post stars", e);
+    }
+  }
+
+  @Override
+  public SortedSet<String> getStars(String changeId) throws RestApiException {
+    try {
+      AccountResource.Star rsrc =
+          stars.parse(account, IdString.fromUrl(changeId));
+      return starsGet.apply(rsrc);
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot get stars", e);
+    }
+  }
+
+  @Override
+  public List<ChangeInfo> getStarredChanges() throws RestApiException {
+    try {
+      return stars.list().apply(account);
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot get starred changes", e);
+    }
+  }
+
+  @Override
   public void addEmail(EmailInput input) throws RestApiException {
     AccountResource.Email rsrc =
         new AccountResource.Email(account.getUser(), input.email);
@@ -227,7 +320,7 @@
   public List<SshKeyInfo> listSshKeys() throws RestApiException {
     try {
       return getSshKeys.apply(account);
-    } catch (OrmException e) {
+    } catch (OrmException | IOException | ConfigInvalidException e) {
       throw new RestApiException("Cannot list SSH keys", e);
     }
   }
@@ -238,12 +331,23 @@
     in.raw = RawInputUtil.create(key);
     try {
       return addSshKey.apply(account, in).value();
-    } catch (OrmException | IOException e) {
+    } catch (OrmException | IOException | ConfigInvalidException e) {
       throw new RestApiException("Cannot add SSH key", e);
     }
   }
 
   @Override
+  public void deleteSshKey(int seq) throws RestApiException {
+    try {
+      AccountResource.SshKey sshKeyRes =
+          sshKeys.parse(account, IdString.fromDecoded(Integer.toString(seq)));
+      deleteSshKey.apply(sshKeyRes, null);
+    } catch (OrmException | IOException | ConfigInvalidException e) {
+      throw new RestApiException("Cannot delete SSH key", e);
+    }
+  }
+
+  @Override
   public Map<String, GpgKeyInfo> listGpgKeys() throws RestApiException {
     try {
       return gpgApiAdapter.listGpgKeys(account);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountExternalIdCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountExternalIdCreator.java
index 9c500b2..a0b9b4e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountExternalIdCreator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountExternalIdCreator.java
@@ -32,6 +32,6 @@
    *
    * @return a list of external identifiers, or an empty list.
    */
-  public List<AccountExternalId> create(Account.Id id, String username,
+  List<AccountExternalId> create(Account.Id id, String username,
       String email);
 }
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 274826a..f143bd6 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
@@ -42,6 +42,7 @@
 import com.google.gerrit.server.change.DeleteDraftChange;
 import com.google.gerrit.server.change.GetHashtags;
 import com.google.gerrit.server.change.GetTopic;
+import com.google.gerrit.server.change.Index;
 import com.google.gerrit.server.change.ListChangeComments;
 import com.google.gerrit.server.change.ListChangeDrafts;
 import com.google.gerrit.server.change.Move;
@@ -59,7 +60,6 @@
 import com.google.gerrit.server.project.NoSuchChangeException;
 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;
@@ -73,13 +73,13 @@
     ChangeApiImpl create(ChangeResource change);
   }
 
-  private final Provider<CurrentUser> user;
+  private final CurrentUser user;
   private final Changes changeApi;
   private final Reviewers reviewers;
   private final Revisions revisions;
   private final ReviewerApiImpl.Factory reviewerApi;
   private final RevisionApiImpl.Factory revisionApi;
-  private final Provider<SuggestChangeReviewers> suggestReviewers;
+  private final SuggestChangeReviewers suggestReviewers;
   private final ChangeResource change;
   private final Abandon abandon;
   private final Revert revert;
@@ -97,17 +97,18 @@
   private final ListChangeComments listComments;
   private final ListChangeDrafts listDrafts;
   private final Check check;
+  private final Index index;
   private final ChangeEdits.Detail editDetail;
   private final Move move;
 
   @Inject
-  ChangeApiImpl(Provider<CurrentUser> user,
+  ChangeApiImpl(CurrentUser user,
       Changes changeApi,
       Reviewers reviewers,
       Revisions revisions,
       ReviewerApiImpl.Factory reviewerApi,
       RevisionApiImpl.Factory revisionApi,
-      Provider<SuggestChangeReviewers> suggestReviewers,
+      SuggestChangeReviewers suggestReviewers,
       Abandon abandon,
       Revert revert,
       Restore restore,
@@ -123,6 +124,7 @@
       ListChangeComments listComments,
       ListChangeDrafts listDrafts,
       Check check,
+      Index index,
       ChangeEdits.Detail editDetail,
       Move move,
       @Assisted ChangeResource change) {
@@ -148,6 +150,7 @@
     this.listComments = listComments;
     this.listDrafts = listDrafts;
     this.check = check;
+    this.index = index;
     this.editDetail = editDetail;
     this.move = move;
     this.change = change;
@@ -219,7 +222,7 @@
   @Override
   public void move(String destination) throws RestApiException {
     MoveInput in = new MoveInput();
-    in.destination_branch = destination;
+    in.destinationBranch = destination;
     move(in);
   }
 
@@ -325,22 +328,21 @@
   private List<SuggestedReviewerInfo> suggestReviewers(SuggestedReviewersRequest r)
       throws RestApiException {
     try {
-      SuggestChangeReviewers mySuggestReviewers = suggestReviewers.get();
-      mySuggestReviewers.setQuery(r.getQuery());
-      mySuggestReviewers.setLimit(r.getLimit());
-      return mySuggestReviewers.apply(change);
+      suggestReviewers.setQuery(r.getQuery());
+      suggestReviewers.setLimit(r.getLimit());
+      return suggestReviewers.apply(change);
     } catch (OrmException | IOException e) {
       throw new RestApiException("Cannot retrieve suggested reviewers", e);
     }
   }
 
+  @SuppressWarnings("deprecation")
   @Override
   public ChangeInfo get(EnumSet<ListChangesOption> s)
       throws RestApiException {
     try {
-      CurrentUser u = user.get();
-      if (u.isIdentifiedUser()) {
-        u.asIdentifiedUser().clearStarredChanges();
+      if (user.isIdentifiedUser()) {
+        user.asIdentifiedUser().clearStarredChanges();
       }
       return changeJson.create(s).format(change);
     } catch (OrmException e) {
@@ -421,4 +423,13 @@
       throw new RestApiException("Cannot check change", e);
     }
   }
+
+  @Override
+  public void index() throws RestApiException {
+    try {
+      index.apply(change, new Index.Input());
+    } catch (IOException e) {
+      throw new RestApiException("Cannot index change", e);
+    }
+  }
 }
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 5485052..f4c671f 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
@@ -117,6 +117,7 @@
     return query().withQuery(query);
   }
 
+  @SuppressWarnings("deprecation")
   private List<ChangeInfo> get(final QueryRequest q) throws RestApiException {
     QueryChanges qc = queryProvider.get();
     if (q.getQuery() != null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/FileApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/FileApiImpl.java
index 46204d3..3d40373 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/FileApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/FileApiImpl.java
@@ -25,7 +25,6 @@
 import com.google.gerrit.server.project.NoSuchChangeException;
 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;
@@ -36,12 +35,12 @@
   }
 
   private final GetContent getContent;
-  private final Provider<GetDiff> getDiff;
+  private final GetDiff getDiff;
   private final FileResource file;
 
   @Inject
   FileApiImpl(GetContent getContent,
-      Provider<GetDiff> getDiff,
+      GetDiff getDiff,
       @Assisted FileResource file) {
     this.getContent = getContent;
     this.getDiff = getDiff;
@@ -60,7 +59,7 @@
   @Override
   public DiffInfo diff() throws RestApiException {
     try {
-      return getDiff.get().apply(file).value();
+      return getDiff.apply(file).value();
     } catch (IOException | InvalidChangeOperationException | OrmException e) {
       throw new RestApiException("Cannot retrieve diff", e);
     }
@@ -69,7 +68,7 @@
   @Override
   public DiffInfo diff(String base) throws RestApiException {
     try {
-      return getDiff.get().setBase(base).apply(file).value();
+      return getDiff.setBase(base).apply(file).value();
     } catch (IOException | InvalidChangeOperationException | OrmException e) {
       throw new RestApiException("Cannot retrieve diff", e);
     }
@@ -86,21 +85,20 @@
   }
 
   private DiffInfo get(DiffRequest r) throws RestApiException {
-    GetDiff diff = getDiff.get();
     if (r.getBase() != null) {
-      diff.setBase(r.getBase());
+      getDiff.setBase(r.getBase());
     }
     if (r.getContext() != null) {
-      diff.setContext(r.getContext());
+      getDiff.setContext(r.getContext());
     }
     if (r.getIntraline() != null) {
-      diff.setIntraline(r.getIntraline());
+      getDiff.setIntraline(r.getIntraline());
     }
     if (r.getWhitespace() != null) {
-      diff.setWhitespace(r.getWhitespace());
+      getDiff.setWhitespace(r.getWhitespace());
     }
     try {
-      return diff.apply(file).value();
+      return getDiff.apply(file).value();
     } catch (IOException | InvalidChangeOperationException | OrmException e) {
       throw new RestApiException("Cannot retrieve diff", 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 cb81d83..4c7adea 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
@@ -62,7 +62,6 @@
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 
 import org.eclipse.jgit.lib.Repository;
@@ -89,11 +88,11 @@
   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<GetPatch> getPatch;
-  private final Provider<PostReview> review;
-  private final Provider<Mergeable> mergeable;
+  private final Files files;
+  private final Files.ListFiles listFiles;
+  private final GetPatch getPatch;
+  private final PostReview review;
+  private final Mergeable mergeable;
   private final FileApiImpl.Factory fileApi;
   private final ListRevisionComments listComments;
   private final ListRevisionDrafts listDrafts;
@@ -103,7 +102,7 @@
   private final Comments comments;
   private final CommentApiImpl.Factory commentFactory;
   private final GetRevisionActions revisionActions;
-  private final Provider<TestSubmitType> testSubmitType;
+  private final TestSubmitType testSubmitType;
   private final TestSubmitType.Get getSubmitType;
 
   @Inject
@@ -117,11 +116,11 @@
       PublishDraftPatchSet publish,
       Reviewed.PutReviewed putReviewed,
       Reviewed.DeleteReviewed deleteReviewed,
-      Provider<Files> files,
-      Provider<Files.ListFiles> listFiles,
-      Provider<GetPatch> getPatch,
-      Provider<PostReview> review,
-      Provider<Mergeable> mergeable,
+      Files files,
+      Files.ListFiles listFiles,
+      GetPatch getPatch,
+      PostReview review,
+      Mergeable mergeable,
       FileApiImpl.Factory fileApi,
       ListRevisionComments listComments,
       ListRevisionDrafts listDrafts,
@@ -131,7 +130,7 @@
       Comments comments,
       CommentApiImpl.Factory commentFactory,
       GetRevisionActions revisionActions,
-      Provider<TestSubmitType> testSubmitType,
+      TestSubmitType testSubmitType,
       TestSubmitType.Get getSubmitType,
       @Assisted RevisionResource r) {
     this.repoManager = repoManager;
@@ -166,7 +165,7 @@
   @Override
   public void review(ReviewInput in) throws RestApiException {
     try {
-      review.get().apply(revision, in);
+      review.apply(revision, in);
     } catch (OrmException | UpdateException e) {
       throw new RestApiException("Cannot post review", e);
     }
@@ -251,7 +250,7 @@
         view = deleteReviewed;
       }
       view.apply(
-          files.get().parse(revision, IdString.fromDecoded(path)),
+          files.parse(revision, IdString.fromDecoded(path)),
           new Reviewed.Input());
     } catch (Exception e) {
       throw new RestApiException("Cannot update reviewed flag", e);
@@ -263,7 +262,7 @@
   public Set<String> reviewed() throws RestApiException {
     try {
       return ImmutableSet.copyOf((Iterable<String>) listFiles
-          .get().setReviewed(true)
+          .setReviewed(true)
           .apply(revision).value());
     } catch (OrmException | IOException e) {
       throw new RestApiException("Cannot list reviewed files", e);
@@ -273,7 +272,7 @@
   @Override
   public MergeableInfo mergeable() throws RestApiException {
     try {
-      return mergeable.get().apply(revision);
+      return mergeable.apply(revision);
     } catch (OrmException | IOException e) {
       throw new RestApiException("Cannot check mergeability", e);
     }
@@ -282,9 +281,8 @@
   @Override
   public MergeableInfo mergeableOtherBranches() throws RestApiException {
     try {
-      Mergeable m = mergeable.get();
-      m.setOtherBranches(true);
-      return m.apply(revision);
+      mergeable.setOtherBranches(true);
+      return mergeable.apply(revision);
     } catch (OrmException | IOException e) {
       throw new RestApiException("Cannot check mergeability", e);
     }
@@ -294,7 +292,7 @@
   @Override
   public Map<String, FileInfo> files() throws RestApiException {
     try {
-      return (Map<String, FileInfo>)listFiles.get().apply(revision).value();
+      return (Map<String, FileInfo>)listFiles.apply(revision).value();
     } catch (OrmException | IOException e) {
       throw new RestApiException("Cannot retrieve files", e);
     }
@@ -304,7 +302,7 @@
   @Override
   public Map<String, FileInfo> files(String base) throws RestApiException {
     try {
-      return (Map<String, FileInfo>) listFiles.get().setBase(base)
+      return (Map<String, FileInfo>) listFiles.setBase(base)
           .apply(revision).value();
     } catch (OrmException | IOException e) {
       throw new RestApiException("Cannot retrieve files", e);
@@ -313,7 +311,7 @@
 
   @Override
   public FileApi file(String path) {
-    return fileApi.create(files.get().parse(revision,
+    return fileApi.create(files.parse(revision,
         IdString.fromDecoded(path)));
   }
 
@@ -389,7 +387,7 @@
   @Override
   public BinaryResult patch() throws RestApiException {
     try {
-      return getPatch.get().apply(revision);
+      return getPatch.apply(revision);
     } catch (IOException e) {
       throw new RestApiException("Cannot get patch", e);
     }
@@ -413,7 +411,7 @@
   public SubmitType testSubmitType(TestSubmitRuleInput in)
       throws RestApiException {
     try {
-      return testSubmitType.get().apply(revision, in);
+      return testSubmitType.apply(revision, in);
     } catch (OrmException e) {
       throw new RestApiException("Cannot test submit type", e);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupApiImpl.java
index f11ed86..6f0c504 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupApiImpl.java
@@ -41,7 +41,6 @@
 import com.google.gerrit.server.group.PutOptions;
 import com.google.gerrit.server.group.PutOwner;
 import com.google.gwtorm.server.OrmException;
-import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 
@@ -63,7 +62,7 @@
   private final PutDescription putDescription;
   private final GetOptions getOptions;
   private final PutOptions putOptions;
-  private final Provider<ListMembers> listMembers;
+  private final ListMembers listMembers;
   private final AddMembers addMembers;
   private final DeleteMembers deleteMembers;
   private final ListIncludedGroups listGroups;
@@ -84,7 +83,7 @@
       PutDescription putDescription,
       GetOptions getOptions,
       PutOptions putOptions,
-      Provider<ListMembers> listMembers,
+      ListMembers listMembers,
       AddMembers addMembers,
       DeleteMembers deleteMembers,
       ListIncludedGroups listGroups,
@@ -205,10 +204,9 @@
 
   @Override
   public List<AccountInfo> members(boolean recursive) throws RestApiException {
-    ListMembers list = listMembers.get();
-    list.setRecursive(recursive);
+    listMembers.setRecursive(recursive);
     try {
-      return list.apply(rsrc);
+      return listMembers.apply(rsrc);
     } catch (OrmException e) {
       throw new RestApiException("Cannot list group members", e);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ChildProjectApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ChildProjectApiImpl.java
index 02dc919..b972f5e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ChildProjectApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ChildProjectApiImpl.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.server.project.ChildProjectResource;
 import com.google.gerrit.server.project.GetChildProject;
-import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 
@@ -28,14 +27,14 @@
     ChildProjectApiImpl create(ChildProjectResource rsrc);
   }
 
-  private final Provider<GetChildProject> getProvider;
+  private final GetChildProject getChildProject;
   private final ChildProjectResource rsrc;
 
   @AssistedInject
   ChildProjectApiImpl(
-      Provider<GetChildProject> getProvider,
+      GetChildProject getChildProject,
       @Assisted ChildProjectResource rsrc) {
-    this.getProvider = getProvider;
+    this.getChildProject = getChildProject;
     this.rsrc = rsrc;
   }
 
@@ -46,8 +45,7 @@
 
   @Override
   public ProjectInfo get(boolean recursive) throws RestApiException {
-    GetChildProject get = getProvider.get();
-    get.setRecursive(recursive);
-    return get.apply(rsrc);
+    getChildProject.setRecursive(recursive);
+    return getChildProject.apply(rsrc);
   }
 }
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 dbd246c..b8bd905 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
@@ -16,6 +16,7 @@
 
 import static com.google.gerrit.server.account.CapabilityUtils.checkRequiresCapability;
 
+import com.google.gerrit.extensions.api.access.ProjectAccessInfo;
 import com.google.gerrit.extensions.api.projects.BranchApi;
 import com.google.gerrit.extensions.api.projects.BranchInfo;
 import com.google.gerrit.extensions.api.projects.ChildProjectApi;
@@ -34,6 +35,7 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.project.ChildProjectsCollection;
 import com.google.gerrit.server.project.CreateProject;
+import com.google.gerrit.server.project.GetAccess;
 import com.google.gerrit.server.project.GetDescription;
 import com.google.gerrit.server.project.ListBranches;
 import com.google.gerrit.server.project.ListChildProjects;
@@ -42,7 +44,6 @@
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.gerrit.server.project.ProjectsCollection;
 import com.google.gerrit.server.project.PutDescription;
-import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 
@@ -57,8 +58,8 @@
     ProjectApiImpl create(String name);
   }
 
-  private final Provider<CurrentUser> user;
-  private final Provider<CreateProject.Factory> createProjectFactory;
+  private final CurrentUser user;
+  private final CreateProject.Factory createProjectFactory;
   private final ProjectApiImpl.Factory projectApi;
   private final ProjectsCollection projects;
   private final GetDescription getDescription;
@@ -70,12 +71,13 @@
   private final String name;
   private final BranchApiImpl.Factory branchApi;
   private final TagApiImpl.Factory tagApi;
-  private final Provider<ListBranches> listBranchesProvider;
-  private final Provider<ListTags> listTagsProvider;
+  private final GetAccess getAccess;
+  private final ListBranches listBranches;
+  private final ListTags listTags;
 
   @AssistedInject
-  ProjectApiImpl(Provider<CurrentUser> user,
-      Provider<CreateProject.Factory> createProjectFactory,
+  ProjectApiImpl(CurrentUser user,
+      CreateProject.Factory createProjectFactory,
       ProjectApiImpl.Factory projectApi,
       ProjectsCollection projects,
       GetDescription getDescription,
@@ -85,17 +87,19 @@
       ProjectJson projectJson,
       BranchApiImpl.Factory branchApiFactory,
       TagApiImpl.Factory tagApiFactory,
-      Provider<ListBranches> listBranchesProvider,
-      Provider<ListTags> listTagsProvider,
+      GetAccess getAccess,
+      ListBranches listBranches,
+      ListTags listTags,
       @Assisted ProjectResource project) {
     this(user, createProjectFactory, projectApi, projects, getDescription,
         putDescription, childApi, children, projectJson, branchApiFactory,
-        tagApiFactory, listBranchesProvider, listTagsProvider, project, null);
+        tagApiFactory, getAccess, listBranches, listTags,
+        project, null);
   }
 
   @AssistedInject
-  ProjectApiImpl(Provider<CurrentUser> user,
-      Provider<CreateProject.Factory> createProjectFactory,
+  ProjectApiImpl(CurrentUser user,
+      CreateProject.Factory createProjectFactory,
       ProjectApiImpl.Factory projectApi,
       ProjectsCollection projects,
       GetDescription getDescription,
@@ -105,16 +109,18 @@
       ProjectJson projectJson,
       BranchApiImpl.Factory branchApiFactory,
       TagApiImpl.Factory tagApiFactory,
-      Provider<ListBranches> listBranchesProvider,
-      Provider<ListTags> listTagsProvider,
+      GetAccess getAccess,
+      ListBranches listBranches,
+      ListTags listTags,
       @Assisted String name) {
     this(user, createProjectFactory, projectApi, projects, getDescription,
         putDescription, childApi, children, projectJson, branchApiFactory,
-        tagApiFactory, listBranchesProvider, listTagsProvider, null, name);
+        tagApiFactory, getAccess, listBranches, listTags,
+        null, name);
   }
 
-  private ProjectApiImpl(Provider<CurrentUser> user,
-      Provider<CreateProject.Factory> createProjectFactory,
+  private ProjectApiImpl(CurrentUser user,
+      CreateProject.Factory createProjectFactory,
       ProjectApiImpl.Factory projectApi,
       ProjectsCollection projects,
       GetDescription getDescription,
@@ -124,8 +130,9 @@
       ProjectJson projectJson,
       BranchApiImpl.Factory branchApiFactory,
       TagApiImpl.Factory tagApiFactory,
-      Provider<ListBranches> listBranchesProvider,
-      Provider<ListTags> listTagsProvider,
+      GetAccess getAccess,
+      ListBranches listBranches,
+      ListTags listTags,
       ProjectResource project,
       String name) {
     this.user = user;
@@ -141,8 +148,9 @@
     this.name = name;
     this.branchApi = branchApiFactory;
     this.tagApi = tagApiFactory;
-    this.listBranchesProvider = listBranchesProvider;
-    this.listTagsProvider = listTagsProvider;
+    this.getAccess = getAccess;
+    this.listBranches = listBranches;
+    this.listTags = listTags;
   }
 
   @Override
@@ -160,7 +168,7 @@
         throw new BadRequestException("name must match input.name");
       }
       checkRequiresCapability(user, null, CreateProject.class);
-      createProjectFactory.get().create(name)
+      createProjectFactory.create(name)
           .apply(TopLevelResource.INSTANCE, in);
       return projectApi.create(projects.parse(name));
     } catch (IOException | ConfigInvalidException e) {
@@ -182,6 +190,15 @@
   }
 
   @Override
+  public ProjectAccessInfo access() throws RestApiException {
+    try {
+      return getAccess.apply(checkExists());
+    } catch (IOException e) {
+      throw new RestApiException("Cannot get access rights", e);
+    }
+  }
+
+  @Override
   public void description(PutDescriptionInput in)
       throws RestApiException {
     try {
@@ -203,13 +220,12 @@
 
   private List<BranchInfo> listBranches(ListRefsRequest<BranchInfo> request)
       throws RestApiException {
-    ListBranches list = listBranchesProvider.get();
-    list.setLimit(request.getLimit());
-    list.setStart(request.getStart());
-    list.setMatchSubstring(request.getSubstring());
-    list.setMatchRegex(request.getRegex());
+    listBranches.setLimit(request.getLimit());
+    listBranches.setStart(request.getStart());
+    listBranches.setMatchSubstring(request.getSubstring());
+    listBranches.setMatchRegex(request.getRegex());
     try {
-      return list.apply(checkExists());
+      return listBranches.apply(checkExists());
     } catch (IOException e) {
       throw new RestApiException("Cannot list branches", e);
     }
@@ -227,13 +243,12 @@
 
   private List<TagInfo> listTags(ListRefsRequest<TagInfo> request)
       throws RestApiException {
-    ListTags list = listTagsProvider.get();
-    list.setLimit(request.getLimit());
-    list.setStart(request.getStart());
-    list.setMatchSubstring(request.getSubstring());
-    list.setMatchRegex(request.getRegex());
+    listTags.setLimit(request.getLimit());
+    listTags.setStart(request.getStart());
+    listTags.setMatchSubstring(request.getSubstring());
+    listTags.setMatchRegex(request.getRegex());
     try {
-      return list.apply(checkExists());
+      return listTags.apply(checkExists());
     } catch (IOException e) {
       throw new RestApiException("Cannot list tags", e);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/UniversalAuthBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/UniversalAuthBackend.java
index 87c8af2..76a9fd6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/UniversalAuthBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/UniversalAuthBackend.java
@@ -16,11 +16,11 @@
 
 import static com.google.common.base.Preconditions.checkNotNull;
 
-import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
+import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -38,8 +38,8 @@
 
   @Override
   public AuthUser authenticate(final AuthRequest request) throws AuthException {
-    List<AuthUser> authUsers = Lists.newArrayList();
-    List<AuthException> authExs = Lists.newArrayList();
+    List<AuthUser> authUsers = new ArrayList<>();
+    List<AuthException> authExs = new ArrayList<>();
     for (AuthBackend backend : authBackends) {
       try {
         authUsers.add(checkNotNull(backend.authenticate(request)));
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 cd1c4d3..d55bbc3 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
@@ -283,16 +283,6 @@
   }
 
   @Override
-  public AuthRequest link(ReviewDb db, Account.Id to, AuthRequest who) {
-    return who;
-  }
-
-  @Override
-  public AuthRequest unlink(ReviewDb db, Account.Id from, AuthRequest who) {
-    return who;
-  }
-
-  @Override
   public void onCreateAccount(final AuthRequest who, final Account account) {
     usernameCache.put(who.getLocalUser(), Optional.of(account.getId()));
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/oauth/OAuthRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/oauth/OAuthRealm.java
index 7b061a5..cf9000d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/oauth/OAuthRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/oauth/OAuthRealm.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Account.FieldName;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.AbstractRealm;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
@@ -104,27 +103,20 @@
     if (userInfo == null) {
       throw new AccountException("Cannot authenticate");
     }
-    if (!Strings.isNullOrEmpty(userInfo.getEmailAddress())) {
+    if (!Strings.isNullOrEmpty(userInfo.getEmailAddress())
+        && (Strings.isNullOrEmpty(who.getUserName())
+            || !allowsEdit(FieldName.REGISTER_NEW_EMAIL))) {
       who.setEmailAddress(userInfo.getEmailAddress());
     }
-    if (!Strings.isNullOrEmpty(userInfo.getDisplayName())) {
+    if (!Strings.isNullOrEmpty(userInfo.getDisplayName())
+        && (Strings.isNullOrEmpty(who.getDisplayName())
+            || !allowsEdit(FieldName.FULL_NAME))) {
       who.setDisplayName(userInfo.getDisplayName());
     }
     return who;
   }
 
   @Override
-  public AuthRequest link(ReviewDb db, Account.Id to, AuthRequest who) {
-    return who;
-  }
-
-  @Override
-  public AuthRequest unlink(ReviewDb db, Account.Id to, AuthRequest who)
-      throws AccountException {
-    return who;
-  }
-
-  @Override
   public void onCreateAccount(AuthRequest who, Account account) {
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java
new file mode 100644
index 0000000..bcc5eab
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java
@@ -0,0 +1,105 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.oauth;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.cache.Cache;
+import com.google.gerrit.extensions.auth.oauth.OAuthToken;
+import com.google.gerrit.extensions.auth.oauth.OAuthTokenEncrypter;
+import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo;
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+
+@Singleton
+public class OAuthTokenCache {
+  public static final String OAUTH_TOKENS = "oauth_tokens";
+
+  private final DynamicItem<OAuthTokenEncrypter> encrypter;
+
+  public static Module module() {
+    return new CacheModule() {
+      @Override
+      protected void configure() {
+        persist(OAUTH_TOKENS, String.class, OAuthToken.class);
+      }
+    };
+  }
+
+  private final Cache<String, OAuthToken> cache;
+
+  @Inject
+  OAuthTokenCache(@Named(OAUTH_TOKENS) Cache<String, OAuthToken> cache,
+      DynamicItem<OAuthTokenEncrypter> encrypter) {
+    this.cache = cache;
+    this.encrypter = encrypter;
+  }
+
+  public boolean has(OAuthUserInfo user) {
+    return user != null
+      ? cache.getIfPresent(user.getUserName()) != null
+      : false;
+  }
+
+  public OAuthToken get(OAuthUserInfo user) {
+    return user != null
+      ? get(user.getUserName())
+      : null;
+  }
+
+  public OAuthToken get(String userName) {
+    OAuthToken accessToken = cache.getIfPresent(userName);
+    if (accessToken == null) {
+      return null;
+    }
+    accessToken = decrypt(accessToken);
+    if (accessToken.isExpired()) {
+      cache.invalidate(userName);
+      return null;
+    }
+    return accessToken;
+  }
+
+  public void put(OAuthUserInfo user, OAuthToken accessToken) {
+    cache.put(checkNotNull(user.getUserName()),
+        encrypt(checkNotNull(accessToken)));
+  }
+
+  public void remove(OAuthUserInfo user) {
+    if (user != null) {
+      cache.invalidate(user.getUserName());
+    }
+  }
+
+  private OAuthToken encrypt(OAuthToken token) {
+    OAuthTokenEncrypter enc = encrypter.get();
+    if (enc == null) {
+      return token;
+    }
+    return enc.encrypt(token);
+  }
+
+  private OAuthToken decrypt(OAuthToken token) {
+    OAuthTokenEncrypter enc = encrypter.get();
+    if (enc == null) {
+      return token;
+    }
+    return enc.decrypt(token);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/ForwardingRemovalListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/ForwardingRemovalListener.java
index 69d523b..96b437d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/ForwardingRemovalListener.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/ForwardingRemovalListener.java
@@ -59,4 +59,4 @@
       l.onRemoval(pluginName, cacheName, notification);
     }
   }
-}
\ No newline at end of file
+}
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
index 62623ea..60c806b 100644
--- 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
@@ -18,7 +18,7 @@
 
   DiskStats diskStats();
 
-  public static class DiskStats {
+  class DiskStats {
     private final long size;
     private final long space;
     private final long hitCount;
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 d8fbd3b..288dd83 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
@@ -132,7 +132,6 @@
       patchSet = psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
       change.setStatus(Change.Status.ABANDONED);
       change.setLastUpdatedOn(ctx.getWhen());
-      ctx.saveChange();
 
       update.setStatus(change.getStatus());
       message = newMessage(ctx);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java
index db8c53b..4992c8e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java
@@ -35,13 +35,16 @@
 @Singleton
 public class ActionJson {
   private final Revisions revisions;
+  private final ChangeResource.Factory changeResourceFactory;
   private final DynamicMap<RestView<ChangeResource>> changeViews;
 
   @Inject
   ActionJson(
       Revisions revisions,
+      ChangeResource.Factory changeResourceFactory,
       DynamicMap<RestView<ChangeResource>> changeViews) {
     this.revisions = revisions;
+    this.changeResourceFactory = changeResourceFactory;
     this.changeViews = changeViews;
   }
 
@@ -69,7 +72,7 @@
     Provider<CurrentUser> userProvider = Providers.of(ctl.getUser());
     for (UiAction.Description d : UiActions.from(
         changeViews,
-        new ChangeResource(ctl),
+        changeResourceFactory.create(ctl),
         userProvider)) {
       out.put(d.getId(), new ActionInfo(d));
     }
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 14fa7d6..335f201 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
@@ -31,7 +31,7 @@
   private final ArchiveCommand.Format<?> format;
   private final String mimeType;
 
-  private ArchiveFormat(String mimeType, ArchiveCommand.Format<?> format) {
+  ArchiveFormat(String mimeType, ArchiveCommand.Format<?> format) {
     this.format = format;
     this.mimeType = mimeType;
     ArchiveCommand.registerFormat(name(), format);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
index f6045c1..e508659 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
@@ -403,7 +403,7 @@
             rsrc.getChangeEdit(),
             rsrc.getPath(),
             input.content);
-      } catch(InvalidChangeOperationException | IOException e) {
+      } catch (InvalidChangeOperationException | IOException e) {
         throw new ResourceConflictException(e.getMessage());
       }
       return Response.none();
@@ -434,17 +434,21 @@
         throws AuthException, ResourceConflictException {
       try {
         editModifier.deleteFile(rsrc.getChangeEdit(), rsrc.getPath());
-      } catch(InvalidChangeOperationException | IOException e) {
+      } catch (InvalidChangeOperationException | IOException e) {
         throw new ResourceConflictException(e.getMessage());
       }
       return Response.none();
     }
   }
 
-  @Singleton
   public static class Get implements RestReadView<ChangeEditResource> {
     private final FileContentUtil fileContentUtil;
 
+    @Option(name = "--base", aliases = {"-b"},
+      usage = "whether to load the content on the base revision instead of the"
+        + " change edit")
+    private boolean base;
+
     @Inject
     Get(FileContentUtil fileContentUtil) {
       this.fileContentUtil = fileContentUtil;
@@ -454,9 +458,13 @@
     public Response<?> apply(ChangeEditResource rsrc)
         throws IOException {
       try {
+        ChangeEdit edit = rsrc.getChangeEdit();
         return Response.ok(fileContentUtil.getContent(
               rsrc.getControl().getProjectControl().getProjectState(),
-              ObjectId.fromString(rsrc.getChangeEdit().getRevision().get()),
+              base
+                  ? ObjectId.fromString(
+                      edit.getBasePatchSet().getRevision().get())
+                  : ObjectId.fromString(edit.getRevision().get()),
               rsrc.getPath()));
       } catch (ResourceNotFoundException rnfe) {
         return Response.none();
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 59c25e4..44e55c8 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
@@ -77,7 +77,7 @@
 import java.util.concurrent.ExecutorService;
 
 public class ChangeInserter extends BatchUpdate.InsertChangeOp {
-  public static interface Factory {
+  public interface Factory {
     ChangeInserter create(Change.Id cid, RevCommit rc, String refName);
   }
 
@@ -332,7 +332,6 @@
     }
     patchSet = psUtil.insert(ctx.getDb(), ctx.getRevWalk(), update, psId,
         commit, draft, newGroups, null);
-    ctx.saveChange();
 
     /* TODO: fixStatus is used here because the tests
      * (byStatusClosed() in AbstractQueryChangesTest)
@@ -406,7 +405,7 @@
         List<LabelType> labels = changeControl.getLabelTypes().getLabelTypes();
         Map<String, Short> allApprovals = new HashMap<>();
         Map<String, Short> oldApprovals = new HashMap<>();
-        for (LabelType lt : labels){
+        for (LabelType lt : labels) {
           allApprovals.put(lt.getName(), (short) 0);
           oldApprovals.put(lt.getName(), null);
         }
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 0f68ee2..eaf51e4 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
@@ -125,6 +125,8 @@
 import java.util.Collections;
 import java.util.EnumSet;
 import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -159,6 +161,7 @@
   private final ActionJson actionJson;
   private final GpgApiAdapter gpgApi;
   private final ChangeNotes.Factory notesFactory;
+  private final ChangeResource.Factory changeResourceFactory;
 
   private AccountLoader accountLoader;
   private Map<Change.Id, List<SubmitRecord>> submitRecords;
@@ -185,6 +188,7 @@
       ActionJson actionJson,
       GpgApiAdapter gpgApi,
       ChangeNotes.Factory notesFactory,
+      ChangeResource.Factory changeResourceFactory,
       @Assisted Set<ListChangesOption> options) {
     this.db = db;
     this.labelNormalizer = ln;
@@ -205,6 +209,7 @@
     this.actionJson = actionJson;
     this.gpgApi = gpgApi;
     this.notesFactory = notesFactory;
+    this.changeResourceFactory = changeResourceFactory;
     this.options = options.isEmpty()
         ? EnumSet.noneOf(ListChangesOption.class)
         : EnumSet.copyOf(options);
@@ -280,7 +285,7 @@
         }));
 
     List<List<ChangeInfo>> res = Lists.newArrayListWithCapacity(in.size());
-    Map<Change.Id, ChangeInfo> out = Maps.newHashMap();
+    Map<Change.Id, ChangeInfo> out = new HashMap<>();
     for (QueryResult r : in) {
       List<ChangeInfo> infos = toChangeInfo(out, r.changes());
       if (!infos.isEmpty() && r.moreChanges()) {
@@ -372,6 +377,7 @@
     return info;
   }
 
+  @SuppressWarnings("deprecation")
   private ChangeInfo toChangeInfo(ChangeData cd,
       Optional<PatchSet.Id> limitToPsId) throws PatchListNotAvailableException,
       GpgException, OrmException, IOException {
@@ -394,7 +400,7 @@
     out.project = in.getProject().get();
     out.branch = in.getDest().getShortName();
     out.topic = in.getTopic();
-    out.hashtags = ctl.getNotes().load().getHashtags();
+    out.hashtags = cd.hashtags();
     out.changeId = in.getKey().get();
     if (in.getStatus() != Change.Status.MERGED) {
       SubmitTypeRecord str = cd.submitTypeRecord();
@@ -610,7 +616,7 @@
     // 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();
+    Set<Account.Id> allUsers = new HashSet<>();
     allUsers.addAll(cd.reviewers().values());
     for (PatchSetApproval psa : cd.approvals().values()) {
       allUsers.add(psa.getAccountId());
@@ -633,6 +639,7 @@
           continue;
         }
         Integer value;
+        String tag = null;
         Timestamp date = null;
         PatchSetApproval psa = current.get(accountId, lt.getName());
         if (psa != null) {
@@ -643,6 +650,7 @@
             // label.
             value = labelNormalizer.canVote(ctl, lt, accountId) ? 0 : null;
           }
+          tag = psa.getTag();
           date = psa.getGranted();
         } else {
           // Either the user cannot vote on this label, or they were added as a
@@ -650,7 +658,8 @@
           // user can vote on this label.
           value = labelNormalizer.canVote(ctl, lt, accountId) ? 0 : null;
         }
-        addApproval(e.getValue().label(), approvalInfo(accountId, value, date));
+        addApproval(e.getValue().label(),
+            approvalInfo(accountId, value, tag, date));
       }
     }
   }
@@ -664,7 +673,7 @@
   private Map<String, LabelWithStatus> labelsForClosedChange(ChangeData cd,
       LabelTypes labelTypes, boolean standard, boolean detailed)
       throws OrmException {
-    Set<Account.Id> allUsers = Sets.newHashSet();
+    Set<Account.Id> allUsers = new HashSet<>();
     if (detailed) {
       // Users expect to see all reviewers on closed changes, even if they
       // didn't vote on the latest patch set. If we don't need detailed labels,
@@ -677,7 +686,7 @@
 
     // 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();
+    Set<String> labelNames = new HashSet<>();
     Multimap<Account.Id, PatchSetApproval> current = HashMultimap.create();
     for (PatchSetApproval a : cd.currentApprovals()) {
       allUsers.add(a.getAccountId());
@@ -708,7 +717,7 @@
 
       if (detailed) {
         for (Map.Entry<String, LabelWithStatus> entry : labels.entrySet()) {
-          ApprovalInfo ai = approvalInfo(accountId, 0, null);
+          ApprovalInfo ai = approvalInfo(accountId, 0, null, null);
           byLabel.put(entry.getKey(), ai);
           addApproval(entry.getValue().label(), ai);
         }
@@ -724,6 +733,7 @@
         if (info != null) {
           info.value = Integer.valueOf(val);
           info.date = psa.getGranted();
+          info.tag = psa.getTag();
         }
         if (!standard) {
           continue;
@@ -735,10 +745,12 @@
     return labels;
   }
 
-  private ApprovalInfo approvalInfo(Account.Id id, Integer value, Timestamp date) {
+  private ApprovalInfo approvalInfo(Account.Id id, Integer value, String tag,
+      Timestamp date) {
     ApprovalInfo ai = new ApprovalInfo(id.get());
     ai.value = value;
     ai.date = date;
+    ai.tag = tag;
     accountLoader.put(ai);
     return ai;
   }
@@ -749,7 +761,7 @@
 
   private void setLabelValues(LabelType type, LabelWithStatus l) {
     l.label().defaultValue = type.getDefaultValue();
-    l.label().values = Maps.newLinkedHashMap();
+    l.label().values = new LinkedHashMap<>();
     for (LabelValue v : type.getValues()) {
       l.label().values.put(v.formatValue(), v.getText());
     }
@@ -816,6 +828,7 @@
         cmi.author = accountLoader.get(message.getAuthor());
         cmi.date = message.getWrittenOn();
         cmi.message = message.getMessage();
+        cmi.tag = message.getTag();
         cmi._revisionNumber = patchNum != null ? patchNum.get() : null;
         result.add(cmi);
       }
@@ -864,7 +877,7 @@
   private Map<String, RevisionInfo> revisions(ChangeControl ctl,
       Map<PatchSet.Id, PatchSet> map) throws PatchListNotAvailableException,
       GpgException, OrmException, IOException {
-    Map<String, RevisionInfo> res = Maps.newLinkedHashMap();
+    Map<String, RevisionInfo> res = new LinkedHashMap<>();
     for (PatchSet in : map.values()) {
       if ((has(ALL_REVISIONS)
           || in.getId().equals(ctl.getChange().currentPatchSetId()))
@@ -947,7 +960,7 @@
         && userProvider.get().isIdentifiedUser()) {
 
       actionJson.addRevisionActions(out,
-          new RevisionResource(new ChangeResource(ctl), in));
+          new RevisionResource(changeResourceFactory.create(ctl), in));
     }
 
     if (has(PUSH_CERTIFICATES)) {
@@ -996,7 +1009,7 @@
 
   private Map<String, FetchInfo> makeFetchMap(ChangeControl ctl, PatchSet in)
       throws OrmException {
-    Map<String, FetchInfo> r = Maps.newLinkedHashMap();
+    Map<String, FetchInfo> r = new LinkedHashMap<>();
 
     for (DynamicMap.Entry<DownloadScheme> e : downloadSchemes) {
       String schemeName = e.getExportName();
@@ -1042,7 +1055,7 @@
   private static void addCommand(FetchInfo fetchInfo, String commandName,
       String c) {
     if (fetchInfo.commands == null) {
-      fetchInfo.commands = Maps.newTreeMap();
+      fetchInfo.commands = new TreeMap<>();
     }
     fetchInfo.commands.put(commandName, c);
   }
@@ -1056,7 +1069,7 @@
 
   private static void addApproval(LabelInfo label, ApprovalInfo approval) {
     if (label.all == null) {
-      label.all = Lists.newArrayList();
+      label.all = new ArrayList<>();
     }
     label.all.add(approval);
   }
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
index b22c49f..10e6716 100644
--- 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
@@ -288,8 +288,8 @@
   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
+      return 16 + 2 * 36 + 2 * key.strategyName.length() // Size of Key, 64 bit JVM
+          + 2 * changeKind.name().length(); // Size of ChangeKind, 64 bit JVM
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java
index 3171245..05d12b3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.change;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.common.base.MoreObjects;
 import com.google.common.hash.Hasher;
 import com.google.common.hash.Hashing;
@@ -25,11 +27,14 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.TypeLiteral;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
 
 import org.eclipse.jgit.lib.ObjectId;
 
@@ -45,9 +50,17 @@
   public static final TypeLiteral<RestView<ChangeResource>> CHANGE_KIND =
       new TypeLiteral<RestView<ChangeResource>>() {};
 
+  public interface Factory {
+    ChangeResource create(ChangeControl ctl);
+  }
+
+  private final StarredChangesUtil starredChangesUtil;
   private final ChangeControl control;
 
-  public ChangeResource(ChangeControl control) {
+  @AssistedInject
+  ChangeResource(StarredChangesUtil starredChangesUtil,
+      @Assisted ChangeControl control) {
+    this.starredChangesUtil = starredChangesUtil;
     this.control = control;
   }
 
@@ -108,8 +121,12 @@
   @Override
   public String getETag() {
     CurrentUser user = control.getUser();
-    Hasher h = Hashing.md5().newHasher()
-        .putBoolean(user.getStarredChanges().contains(getId()));
+    Hasher h = Hashing.md5().newHasher();
+    if (user.isIdentifiedUser()) {
+      h.putString(
+          starredChangesUtil.getObjectId(user.getAccountId(), getId()).name(),
+          UTF_8);
+    }
     prepareETag(h, user);
     return h.hash().toString();
   }
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 058c7d5..b89691a 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
@@ -45,6 +45,7 @@
   private final DynamicMap<RestView<ChangeResource>> views;
   private final ChangeFinder changeFinder;
   private final CreateChange createChange;
+  private final ChangeResource.Factory changeResourceFactory;
 
   @Inject
   ChangesCollection(
@@ -53,13 +54,15 @@
       Provider<QueryChanges> queryFactory,
       DynamicMap<RestView<ChangeResource>> views,
       ChangeFinder changeFinder,
-      CreateChange createChange) {
+      CreateChange createChange,
+      ChangeResource.Factory changeResourceFactory) {
     this.db = db;
     this.user = user;
     this.queryFactory = queryFactory;
     this.views = views;
     this.changeFinder = changeFinder;
     this.createChange = createChange;
+    this.changeResourceFactory = changeResourceFactory;
   }
 
   @Override
@@ -86,7 +89,7 @@
     if (!ctl.isVisible(db.get())) {
       throw new ResourceNotFoundException(id);
     }
-    return new ChangeResource(ctl);
+    return changeResourceFactory.create(ctl);
   }
 
   public ChangeResource parse(Change.Id id)
@@ -102,7 +105,7 @@
     if (!ctl.isVisible(db.get())) {
       throw new ResourceNotFoundException(toIdString(id));
     }
-    return new ChangeResource(ctl);
+    return changeResourceFactory.create(ctl);
   }
 
   private static IdString toIdString(Change.Id id) {
@@ -110,7 +113,7 @@
   }
 
   public ChangeResource parse(ChangeControl control) {
-    return new ChangeResource(control);
+    return changeResourceFactory.create(control);
   }
 
   @SuppressWarnings("unchecked")
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 513a34f..cefbf8a 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
@@ -286,7 +286,6 @@
       changeMessage.setMessage(sb.toString());
 
       cmUtil.addChangeMessage(ctx.getDb(), ctx.getUpdate(psId), changeMessage);
-      ctx.saveChange(); // Bump lastUpdatedOn to match message.
       return true;
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentJson.java
index b155b84..0af5656 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentJson.java
@@ -136,6 +136,7 @@
     r.message = Strings.emptyToNull(c.getMessage());
     r.updated = c.getWrittenOn();
     r.range = toRange(c.getRange());
+    r.tag = c.getTag();
     if (loader != null) {
       r.author = loader.get(c.getAuthor());
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraftComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraftComment.java
index 9ae9496..bfbc828 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraftComment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraftComment.java
@@ -122,10 +122,12 @@
       comment.setSide(in.side == Side.PARENT ? (short) 0 : (short) 1);
       comment.setMessage(in.message.trim());
       comment.setRange(in.range);
+      comment.setTag(in.tag);
       setCommentRevId(
           comment, patchListCache, ctx.getChange(), ps);
       plcUtil.putComments(
           ctx.getDb(), ctx.getUpdate(psId), Collections.singleton(comment));
+      ctx.bumpLastUpdatedOn(false);
       return true;
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChangeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChangeOp.java
index d90fc73..6ac17b2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChangeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChangeOp.java
@@ -31,6 +31,7 @@
 import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
 import com.google.gerrit.server.git.BatchUpdate.RepoContext;
 import com.google.gerrit.server.git.BatchUpdateReviewDb;
+import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.schema.DisabledChangesReviewDbWrapper;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -78,8 +79,8 @@
   }
 
   @Override
-  public boolean updateChange(ChangeContext ctx)
-      throws RestApiException, OrmException {
+  public boolean updateChange(ChangeContext ctx) throws RestApiException,
+      OrmException, IOException, NoSuchChangeException {
     checkState(ctx.getOrder() == BatchUpdate.Order.DB_BEFORE_REPO,
         "must use DeleteDraftChangeOp with DB_BEFORE_REPO");
     checkState(id == null, "cannot reuse DeleteDraftChangeOp");
@@ -117,7 +118,7 @@
 
     // Non-atomic operation on Accounts table; not much we can do to make it
     // atomic.
-    starredChangesUtil.unstarAll(id);
+    starredChangesUtil.unstarAll(change.getProject(), id);
 
     ctx.deleteChange();
     return true;
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 59d4290..c9f5aa3 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
@@ -35,6 +35,7 @@
 import com.google.gerrit.server.git.UpdateException;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -99,8 +100,8 @@
     }
 
     @Override
-    public boolean updateChange(ChangeContext ctx)
-        throws RestApiException, OrmException, IOException {
+    public boolean updateChange(ChangeContext ctx) throws RestApiException,
+        OrmException, IOException, NoSuchChangeException {
       patchSet = psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
       if (patchSet == null) {
         return false; // Nothing to do.
@@ -148,7 +149,8 @@
     }
 
     private void deleteOrUpdateDraftChange(ChangeContext ctx)
-        throws OrmException, RestApiException {
+        throws OrmException, RestApiException, IOException,
+        NoSuchChangeException {
       Change c = ctx.getChange();
       if (deletedOnlyPatchSet()) {
         deleteChangeOp = deleteChangeOpProvider.get();
@@ -158,7 +160,6 @@
       if (c.currentPatchSetId().equals(psId)) {
         c.setCurrentPatchSet(previousPatchSetInfo(ctx));
       }
-      ctx.saveChange();
     }
 
     private boolean deletedOnlyPatchSet() {
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 dc3b26b..ed0a6b8 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
@@ -17,55 +17,84 @@
 import com.google.common.base.Predicate;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
+import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.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.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.change.DeleteReviewer.Input;
 import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
+import com.google.gerrit.server.git.BatchUpdate.Context;
 import com.google.gerrit.server.git.UpdateException;
+import com.google.gerrit.server.mail.DeleteReviewerSender;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 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.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 @Singleton
 public class DeleteReviewer implements RestModifyView<ReviewerResource, Input> {
+  private static final Logger log = LoggerFactory
+      .getLogger(DeleteReviewer.class);
+
   public static class Input {
   }
 
   private final Provider<ReviewDb> dbProvider;
   private final ApprovalsUtil approvalsUtil;
+  private final PatchSetUtil psUtil;
   private final ChangeMessagesUtil cmUtil;
   private final BatchUpdate.Factory batchUpdateFactory;
   private final IdentifiedUser.GenericFactory userFactory;
+  private final ChangeHooks hooks;
+  private final Provider<IdentifiedUser> user;
+  private final DeleteReviewerSender.Factory deleteReviewerSenderFactory;
 
   @Inject
   DeleteReviewer(Provider<ReviewDb> dbProvider,
       ApprovalsUtil approvalsUtil,
+      PatchSetUtil psUtil,
       ChangeMessagesUtil cmUtil,
       BatchUpdate.Factory batchUpdateFactory,
-      IdentifiedUser.GenericFactory userFactory) {
+      IdentifiedUser.GenericFactory userFactory,
+      ChangeHooks hooks,
+      Provider<IdentifiedUser> user,
+      DeleteReviewerSender.Factory deleteReviewerSenderFactory) {
     this.dbProvider = dbProvider;
     this.approvalsUtil = approvalsUtil;
+    this.psUtil = psUtil;
     this.cmUtil = cmUtil;
     this.batchUpdateFactory = batchUpdateFactory;
     this.userFactory = userFactory;
+    this.hooks = hooks;
+    this.user = user;
+    this.deleteReviewerSenderFactory = deleteReviewerSenderFactory;
   }
 
   @Override
@@ -74,7 +103,7 @@
     try (BatchUpdate bu = batchUpdateFactory.create(dbProvider.get(),
         rsrc.getChangeResource().getProject(),
         rsrc.getChangeResource().getUser(), TimeUtil.nowTs())) {
-      Op op = new Op(rsrc.getReviewerUser().getAccountId());
+      Op op = new Op(rsrc.getReviewerUser().getAccount());
       bu.addOp(rsrc.getChange().getId(), op);
       bu.execute();
     }
@@ -83,29 +112,44 @@
   }
 
   private class Op extends BatchUpdate.Op {
-    private final Account.Id reviewerId;
+    private final Account reviewer;
+    ChangeMessage changeMessage;
+    Change currChange;
+    PatchSet currPs;
+    List<PatchSetApproval> del = new ArrayList<>();
+    Map<String, Short> newApprovals = new HashMap<>();
+    Map<String, Short> oldApprovals = new HashMap<>();
 
-    Op(Account.Id reviewerId) {
-      this.reviewerId = reviewerId;
+    Op(Account reviewerAccount) {
+      this.reviewer = reviewerAccount;
     }
 
     @Override
     public boolean updateChange(ChangeContext ctx)
         throws AuthException, ResourceNotFoundException, OrmException {
-      PatchSet.Id currPs = ctx.getChange().currentPatchSetId();
+      Account.Id reviewerId = reviewer.getId();
+      currChange = ctx.getChange();
+      currPs = psUtil.current(dbProvider.get(), ctx.getNotes());
+
+      LabelTypes labelTypes = ctx.getControl().getLabelTypes();
+      // removing a reviewer will remove all her votes
+      for (LabelType lt : labelTypes.getLabelTypes()) {
+        newApprovals.put(lt.getName(), (short) 0);
+      }
+
       StringBuilder msg = new StringBuilder();
-      List<PatchSetApproval> del = Lists.newArrayList();
       for (PatchSetApproval a : approvals(ctx, reviewerId)) {
         if (ctx.getControl().canRemoveReviewer(a)) {
           del.add(a);
-          if (a.getPatchSetId().equals(currPs)
-              && a.getValue() != 0) {
+          if (a.getPatchSetId().equals(currPs.getId()) && a.getValue() != 0) {
+            oldApprovals.put(a.getLabel(), a.getValue());
             if (msg.length() == 0) {
-              msg.append("Removed the following votes:\n\n");
+              msg.append("Removed reviewer ").append(reviewer.getFullName())
+                  .append(" with the following votes:\n\n");
             }
-            msg.append("* ")
-                .append(a.getLabel()).append(formatLabelValue(a.getValue()))
-                .append(" by ").append(userFactory.create(a.getAccountId()).getNameEmail())
+            msg.append("* ").append(a.getLabel())
+                .append(formatLabelValue(a.getValue())).append(" by ")
+                .append(userFactory.create(a.getAccountId()).getNameEmail())
                 .append("\n");
           }
         } else {
@@ -116,21 +160,37 @@
         throw new ResourceNotFoundException();
       }
       ctx.getDb().patchSetApprovals().delete(del);
-      ChangeUpdate update = ctx.getUpdate(currPs);
+      ChangeUpdate update = ctx.getUpdate(currPs.getId());
       update.removeReviewer(reviewerId);
 
       if (msg.length() > 0) {
-        ChangeMessage changeMessage =
-            new ChangeMessage(new ChangeMessage.Key(ctx.getChange().getId(),
+        changeMessage = new ChangeMessage(
+            new ChangeMessage.Key(currChange.getId(),
                 ChangeUtil.messageUUID(ctx.getDb())),
-                ctx.getUser().getAccountId(),
-                ctx.getWhen(), currPs);
+            ctx.getUser().getAccountId(), ctx.getWhen(), currPs.getId());
         changeMessage.setMessage(msg.toString());
         cmUtil.addChangeMessage(ctx.getDb(), update, changeMessage);
       }
+
       return true;
     }
 
+    @Override
+    public void postUpdate(Context ctx) {
+      if (changeMessage == null) {
+        return;
+      }
+
+      emailReviewers(ctx.getProject(), currChange, del, changeMessage);
+      try {
+        hooks.doReviewerDeletedHook(currChange, reviewer, currPs,
+            changeMessage.getMessage(), newApprovals, oldApprovals,
+            dbProvider.get());
+      } catch (OrmException e) {
+        log.warn("ChangeHook.doCommentAddedHook delivery failed", e);
+      }
+    }
+
     private Iterable<PatchSetApproval> approvals(ChangeContext ctx,
         final Account.Id accountId) throws OrmException {
       return Iterables.filter(
@@ -151,4 +211,29 @@
       }
     }
   }
+
+  private void emailReviewers(Project.NameKey projectName, Change change,
+      List<PatchSetApproval> dels, ChangeMessage changeMessage) {
+
+    // The user knows they removed themselves, don't bother emailing them.
+    List<Account.Id> toMail = Lists.newArrayListWithCapacity(dels.size());
+    Account.Id userId = user.get().getAccountId();
+    for (PatchSetApproval psa : dels) {
+      if (!psa.getAccountId().equals(userId)) {
+        toMail.add(psa.getAccountId());
+      }
+    }
+    if (!toMail.isEmpty()) {
+      try {
+        DeleteReviewerSender cm =
+            deleteReviewerSenderFactory.create(projectName, change.getId());
+        cm.setFrom(userId);
+        cm.addReviewers(toMail);
+        cm.setChangeMessage(changeMessage);
+        cm.send();
+      } catch (Exception err) {
+        log.error("Cannot email update for change " + change.getId(), err);
+      }
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java
index 0ded3a2..8f597ac 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java
@@ -168,7 +168,6 @@
       if (psa == null) {
         throw new ResourceNotFoundException();
       }
-      ctx.saveChange();
       ctx.getDb().patchSetApprovals().update(Collections.singleton(psa));
 
       if (msg.length() > 0) {
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 e31b11f..45794fb 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
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.common.collect.Maps;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.extensions.common.FileInfo;
@@ -33,6 +32,7 @@
 import org.eclipse.jgit.lib.ObjectId;
 
 import java.util.Map;
+import java.util.TreeMap;
 
 @Singleton
 public class FileInfoJson {
@@ -57,7 +57,7 @@
     PatchList list = patchListCache.get(
         new PatchListKey(a, b, Whitespace.IGNORE_NONE), change.getProject());
 
-    Map<String, FileInfo> files = Maps.newTreeMap();
+    Map<String, FileInfo> files = new TreeMap<>();
     for (PatchListEntry e : list.getPatches()) {
       FileInfo d = new FileInfo();
       d.status = e.getChangeType() != Patch.ChangeType.MODIFIED
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 79025b4..c504da0 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
@@ -231,7 +231,7 @@
 
     private List<String> scan(Account.Id userId, PatchSet.Id psId)
         throws OrmException {
-      List<String> r = Lists.newArrayList();
+      List<String> r = new ArrayList<>();
       for (AccountPatchReview w : db.get().accountPatchReviews()
           .byReviewer(userId, psId)) {
         r.add(w.getKey().getPatchKey().getFileName());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetBlame.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetBlame.java
new file mode 100644
index 0000000..0f0f5a6
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetBlame.java
@@ -0,0 +1,166 @@
+// 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.change;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.gerrit.extensions.common.BlameInfo;
+import com.google.gerrit.extensions.common.RangeInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.CacheControl;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.Project;
+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.patch.AutoMerger;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gitiles.blame.BlameCache;
+import com.google.gitiles.blame.Region;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.Config;
+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.merge.ThreeWayMergeStrategy;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.kohsuke.args4j.Option;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+public class GetBlame implements RestReadView<FileResource> {
+
+  private final GitRepositoryManager repoManager;
+  private final BlameCache blameCache;
+  private final boolean allowBlame;
+  private final ThreeWayMergeStrategy mergeStrategy;
+  private final AutoMerger autoMerger;
+
+  @Option(name = "--base", aliases = {"-b"},
+    usage = "whether to load the blame of the base revision (the direct"
+      + " parent of the change) instead of the change")
+  private boolean base;
+
+  @Inject
+  GetBlame(GitRepositoryManager repoManager,
+      BlameCache blameCache,
+      @GerritServerConfig Config cfg,
+      AutoMerger autoMerger) {
+    this.repoManager = repoManager;
+    this.blameCache = blameCache;
+    this.mergeStrategy = MergeUtil.getMergeStrategy(cfg);
+    this.autoMerger = autoMerger;
+    allowBlame = cfg.getBoolean("change", "allowBlame", true);
+  }
+
+  @Override
+  public Response<List<BlameInfo>> apply(FileResource resource)
+      throws RestApiException, OrmException, IOException,
+      InvalidChangeOperationException {
+    if (!allowBlame) {
+      throw new BadRequestException("blame is disabled");
+    }
+
+    Project.NameKey project = resource.getRevision().getChange().getProject();
+    try (Repository repository = repoManager.openRepository(project);
+        ObjectInserter ins = repository.newObjectInserter();
+        RevWalk revWalk = new RevWalk(ins.newReader())) {
+      String refName = resource.getRevision().getEdit().isPresent()
+          ? resource.getRevision().getEdit().get().getRefName()
+          : resource.getRevision().getPatchSet().getRefName();
+
+      Ref ref = repository.findRef(refName);
+      if (ref == null) {
+        throw new ResourceNotFoundException("unknown ref " + refName);
+      }
+      ObjectId objectId = ref.getObjectId();
+      RevCommit revCommit = revWalk.parseCommit(objectId);
+      RevCommit[] parents = revCommit.getParents();
+
+      String path = resource.getPatchKey().getFileName();
+
+      List<BlameInfo> result;
+      if (!base) {
+        result = blame(revCommit, path, repository, revWalk);
+
+      } else if (parents.length == 0) {
+        throw new ResourceNotFoundException("Initial commit doesn't have base");
+
+      } else if (parents.length == 1) {
+        result = blame(parents[0], path, repository, revWalk);
+
+      } else if (parents.length == 2) {
+        ObjectId automerge = autoMerger.merge(repository, revWalk, ins,
+            revCommit, mergeStrategy);
+        result = blame(automerge, path, repository, revWalk);
+
+      } else {
+        throw new ResourceNotFoundException(
+            "Cannot generate blame for merge commit with more than 2 parents");
+      }
+
+      Response<List<BlameInfo>> r = Response.ok(result);
+      if (resource.isCacheable()) {
+        r.caching(CacheControl.PRIVATE(7, TimeUnit.DAYS));
+      }
+      return r;
+    }
+  }
+
+  private List<BlameInfo> blame(ObjectId id, String path,
+      Repository repository, RevWalk revWalk) throws IOException {
+    ListMultimap<BlameInfo, RangeInfo> ranges = ArrayListMultimap.create();
+    List<BlameInfo> result = new ArrayList<>();
+    if (blameCache.findLastCommit(repository, id, path) == null) {
+      return result;
+    }
+
+    List<Region> blameRegions = blameCache.get(repository, id, path);
+    int from = 1;
+    for (Region region : blameRegions) {
+      RevCommit commit = revWalk.parseCommit(region.getSourceCommit());
+      BlameInfo blameInfo = toBlameInfo(commit, region.getSourceAuthor());
+      ranges.put(blameInfo, new RangeInfo(from, from + region.getCount() - 1));
+      from += region.getCount();
+    }
+
+    for (BlameInfo key : ranges.keySet()) {
+      key.ranges = ranges.get(key);
+      result.add(key);
+    }
+    return result;
+  }
+
+  private static BlameInfo toBlameInfo(RevCommit commit,
+      PersonIdent sourceAuthor) {
+    BlameInfo blameInfo = new BlameInfo();
+    blameInfo.author = sourceAuthor.getName();
+    blameInfo.id = commit.getName();
+    blameInfo.commitMsg = commit.getFullMessage();
+    blameInfo.time = commit.getCommitTime();
+    return blameInfo;
+  }
+}
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 92af1fa..3d02b83 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
@@ -164,8 +164,8 @@
           case INSERT:
           case REPLACE:
             List<Edit> internalEdit = edit instanceof ReplaceEdit
-              ? ((ReplaceEdit) edit).getInternalEdits()
-              : null;
+                ? ((ReplaceEdit) edit).getInternalEdits()
+                : null;
             content.addDiff(edit.getEndA(), edit.getEndB(), internalEdit);
             break;
           case EMPTY:
@@ -403,7 +403,7 @@
 
     private final DiffPreferencesInfo.Whitespace whitespace;
 
-    private IgnoreWhitespace(DiffPreferencesInfo.Whitespace whitespace) {
+    IgnoreWhitespace(DiffPreferencesInfo.Whitespace whitespace) {
       this.whitespace = whitespace;
     }
   }
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 b10d9f7..12e4276 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
@@ -81,7 +81,8 @@
         .byProjectGroups(rsrc.getChange().getProject(), groups);
     if (cds.isEmpty()) {
       return Collections.emptyList();
-    } if (cds.size() == 1
+    }
+    if (cds.size() == 1
         && cds.get(0).getId().equals(rsrc.getChange().getId())) {
       return Collections.emptyList();
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java
index c5837b4..d758a77 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java
@@ -42,16 +42,19 @@
   private final Config config;
   private final Provider<ReviewDb> dbProvider;
   private final MergeSuperSet mergeSuperSet;
+  private final ChangeResource.Factory changeResourceFactory;
 
   @Inject
   GetRevisionActions(
       ActionJson delegate,
       Provider<ReviewDb> dbProvider,
       MergeSuperSet mergeSuperSet,
+      ChangeResource.Factory changeResourceFactory,
       @GerritServerConfig Config config) {
     this.delegate = delegate;
     this.dbProvider = dbProvider;
     this.mergeSuperSet = mergeSuperSet;
+    this.changeResourceFactory = changeResourceFactory;
     this.config = config;
   }
 
@@ -71,7 +74,7 @@
       ChangeSet cs =
           mergeSuperSet.completeChangeSet(db, rsrc.getChange(), user);
       for (ChangeData cd : cs.changes()) {
-        new ChangeResource(cd.changeControl()).prepareETag(h, user);
+        changeResourceFactory.create(cd.changeControl()).prepareETag(h, user);
       }
     } catch (IOException | OrmException e) {
       throw new OrmRuntimeException(e);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedInResolver.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedInResolver.java
index 2c5eda4..56857b4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedInResolver.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedInResolver.java
@@ -17,7 +17,6 @@
 import com.google.common.collect.LinkedListMultimap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Multimap;
-import com.google.common.collect.Sets;
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
@@ -35,6 +34,8 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
+import java.util.HashSet;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Set;
 
@@ -107,8 +108,8 @@
 
   private boolean includedInOne(final Collection<Ref> refs) throws IOException {
     parseCommits(refs);
-    List<RevCommit> before = Lists.newLinkedList();
-    List<RevCommit> after = Lists.newLinkedList();
+    List<RevCommit> before = new LinkedList<>();
+    List<RevCommit> after = new LinkedList<>();
     partition(before, after);
     // It is highly likely that the target is reachable from the "after" set
     // Within the "before" set we are trying to handle cases arising from clock skew
@@ -120,7 +121,7 @@
    */
   private Set<String> includedIn(final Collection<RevCommit> tips, int limit)
       throws IOException, MissingObjectException, IncorrectObjectTypeException {
-    Set<String> result = Sets.newHashSet();
+    Set<String> result = new HashSet<>();
     for (RevCommit tip : tips) {
       boolean commitFound = false;
       rw.resetRetain(RevFlag.UNINTERESTING, containsTarget);
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 ca5a55b..1e3480f 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
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.common.collect.Maps;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -25,6 +24,7 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -48,7 +48,7 @@
 
   @Override
   public List<ReviewerInfo> apply(ChangeResource rsrc) throws OrmException {
-    Map<Account.Id, ReviewerResource> reviewers = Maps.newLinkedHashMap();
+    Map<Account.Id, ReviewerResource> reviewers = new LinkedHashMap<>();
     ReviewDb db = dbProvider.get();
     for (Account.Id accountId
         : approvalsUtil.getReviewers(db, rsrc.getNotes()).values()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCache.java
index e83b539..93c4ac3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCache.java
@@ -23,7 +23,7 @@
 
 /** Cache for mergeability of commits into destination branches. */
 public interface MergeabilityCache {
-  public static class NotImplemented implements MergeabilityCache {
+  class NotImplemented implements MergeabilityCache {
     @Override
     public boolean get(ObjectId commit, Ref intoRef, SubmitType submitType,
         String mergeStrategy, Branch.NameKey dest, Repository repo) {
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 23d29de5..842e8bb 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
@@ -24,7 +24,6 @@
 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.ChangeUtil;
 import com.google.gerrit.server.git.BranchOrderSection;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MergeUtil;
@@ -33,6 +32,7 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.schema.DisabledChangesReviewDbWrapper;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -47,6 +47,7 @@
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Map;
 import java.util.Objects;
 
@@ -175,10 +176,23 @@
     final boolean mergeable =
         cache.get(commit, ref, type, strategy, change.getDest(), git);
     if (!Objects.equals(mergeable, old)) {
-      // TODO(dborowitz): Include cache info in ETag somehow instead.
-      ChangeUtil.bumpRowVersionNotLastUpdatedOn(change.getId(), db.get());
+      invalidateETag(change.getId(), db.get());
       indexer.index(db.get(), change);
     }
     return mergeable;
   }
+
+  private static void invalidateETag(Change.Id id, ReviewDb db)
+      throws OrmException {
+    // Empty update of Change to bump rowVersion, changing its ETag.
+    // TODO(dborowitz): Include cache info in ETag somehow instead.
+    if (db instanceof DisabledChangesReviewDbWrapper) {
+      db = ((DisabledChangesReviewDbWrapper) db).unsafeGetDelegate();
+    }
+    Change c = db.changes().get(id);
+    if (c != null) {
+      db.changes().update(Collections.singleton(c));
+    }
+  }
+
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
index 4c9c0bf..c59bf5a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
@@ -114,6 +114,7 @@
     get(FILE_KIND, "content").to(GetContent.class);
     get(FILE_KIND, "download").to(DownloadContent.class);
     get(FILE_KIND, "diff").to(GetDiff.class);
+    get(FILE_KIND, "blame").to(GetBlame.class);
 
     child(CHANGE_KIND, "edit").to(ChangeEdits.class);
     delete(CHANGE_KIND, "edit").to(DeleteChangeEdit.class);
@@ -135,5 +136,6 @@
     factory(RebaseChangeOp.Factory.class);
     factory(ReviewerResource.Factory.class);
     factory(SetHashtagsOp.Factory.class);
+    factory(ChangeResource.Factory.class);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java
index 7322586..2139ec4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java
@@ -87,8 +87,8 @@
   public ChangeInfo apply(ChangeResource req, MoveInput input)
       throws RestApiException, OrmException, UpdateException {
     ChangeControl control = req.getControl();
-    input.destination_branch = RefNames.fullName(input.destination_branch);
-    if (!control.canMoveTo(input.destination_branch, dbProvider.get())) {
+    input.destinationBranch = RefNames.fullName(input.destinationBranch);
+    if (!control.canMoveTo(input.destinationBranch, dbProvider.get())) {
       throw new AuthException("Move not permitted");
     }
 
@@ -108,7 +108,7 @@
     private Change change;
     private Branch.NameKey newDestKey;
 
-    public Op(ChangeControl ctl, MoveInput input) {
+    Op(ChangeControl ctl, MoveInput input) {
       this.input = input;
       this.caller = ctl.getUser().asIdentifiedUser();
     }
@@ -123,7 +123,7 @@
       }
 
       Project.NameKey projectKey = change.getProject();
-      newDestKey = new Branch.NameKey(projectKey, input.destination_branch);
+      newDestKey = new Branch.NameKey(projectKey, input.destinationBranch);
       Branch.NameKey changePrevDest = change.getDest();
       if (changePrevDest.equals(newDestKey)) {
         throw new ResourceConflictException(
@@ -140,17 +140,17 @@
           throw new ResourceConflictException("Merge commit cannot be moved");
         }
 
-        ObjectId refId = repo.resolve(input.destination_branch);
+        ObjectId refId = repo.resolve(input.destinationBranch);
         // Check if destination ref exists in project repo
         if (refId == null) {
           throw new ResourceConflictException(
-              "Destination " + input.destination_branch + " not found in the project");
+              "Destination " + input.destinationBranch + " not found in the project");
         }
         RevCommit refCommit = revWalk.parseCommit(refId);
         if (revWalk.isMergedInto(currPatchsetRevCommit, refCommit)) {
           throw new ResourceConflictException(
               "Current patchset revision is reachable from tip of "
-                  + input.destination_branch);
+                  + input.destinationBranch);
         }
       }
 
@@ -186,7 +186,6 @@
       cmsg.setMessage(msgBuf.toString());
       cmUtil.addChangeMessage(ctx.getDb(), update, cmsg);
 
-      ctx.saveChange();
       return true;
     }
   }
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 1853438..3defdd7 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
@@ -68,7 +68,7 @@
   private static final Logger log =
       LoggerFactory.getLogger(PatchSetInserter.class);
 
-  public static interface Factory {
+  public interface Factory {
     PatchSetInserter create(RefControl refControl, PatchSet.Id psId,
         RevCommit commit);
   }
@@ -246,7 +246,6 @@
       change.setStatus(Change.Status.NEW);
     }
     change.setCurrentPatchSet(patchSetInfo);
-    ctx.saveChange();
     if (copyApprovals) {
       approvalCopier.copy(db, ctl, patchSet);
     }
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 bae54f2..e78fd9e 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
@@ -24,8 +24,6 @@
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
 import com.google.common.collect.Ordering;
 import com.google.common.collect.Sets;
 import com.google.common.hash.HashCode;
@@ -49,7 +47,6 @@
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.extensions.restapi.Url;
-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;
@@ -371,13 +368,6 @@
       dirty |= insertComments(ctx);
       dirty |= updateLabels(ctx);
       dirty |= insertMessage(ctx);
-      Change c = notes.getChange();
-      if (c.getLastUpdatedOn().before(ctx.getWhen())) {
-        c.setLastUpdatedOn(ctx.getWhen());
-      }
-      if (dirty) {
-        ctx.saveChange();
-      }
       return dirty;
     }
 
@@ -418,8 +408,8 @@
         }
       }
 
-      List<PatchLineComment> del = Lists.newArrayList();
-      List<PatchLineComment> ups = Lists.newArrayList();
+      List<PatchLineComment> del = new ArrayList<>();
+      List<PatchLineComment> ups = new ArrayList<>();
 
       Set<CommentSetEntry> existingIds = in.omitDuplicateComments
           ? readExistingComments(ctx)
@@ -444,6 +434,7 @@
           e.setSide(c.side == Side.PARENT ? (short) 0 : (short) 1);
           setCommentRevId(e, patchListCache, ctx.getChange(), ps);
           e.setMessage(c.message);
+          e.setTag(in.tag);
           if (c.range != null) {
             e.setRange(new CommentRange(
                 c.range.startLine,
@@ -497,9 +488,10 @@
 
     private Map<String, PatchLineComment> changeDrafts(ChangeContext ctx)
         throws OrmException {
-      Map<String, PatchLineComment> drafts = Maps.newHashMap();
+      Map<String, PatchLineComment> drafts = new HashMap<>();
       for (PatchLineComment c : plcUtil.draftByChangeAuthor(
           ctx.getDb(), ctx.getNotes(), user.getAccountId())) {
+        c.setTag(in.tag);
         drafts.put(c.getKey().get(), c);
       }
       return drafts;
@@ -507,7 +499,7 @@
 
     private Map<String, PatchLineComment> patchSetDrafts(ChangeContext ctx)
         throws OrmException {
-      Map<String, PatchLineComment> drafts = Maps.newHashMap();
+      Map<String, PatchLineComment> drafts = new HashMap<>();
       for (PatchLineComment c : plcUtil.draftByPatchSetAuthor(ctx.getDb(),
           psId, user.getAccountId(), ctx.getNotes())) {
         drafts.put(c.getKey().get(), c);
@@ -528,6 +520,7 @@
         PatchLineComment c, PatchSet ps) throws OrmException {
       c.setStatus(PatchLineComment.Status.PUBLISHED);
       c.setWrittenOn(ctx.getWhen());
+      c.setTag(in.tag);
       setCommentRevId(c, patchListCache, ctx.getChange(), checkNotNull(ps));
       return c;
     }
@@ -550,6 +543,37 @@
       }
     }
 
+    private Map<String, Short> getAllApprovals(LabelTypes labelTypes,
+        Map<String, Short> current, Map<String, Short> input) {
+      Map<String, Short> allApprovals = new HashMap<>();
+      for (LabelType lt : labelTypes.getLabelTypes()) {
+        allApprovals.put(lt.getName(), (short) 0);
+      }
+      // set approvals to existing votes
+      if (current != null) {
+        allApprovals.putAll(current);
+      }
+      // set approvals to new votes
+      if (input != null) {
+        allApprovals.putAll(input);
+      }
+      return allApprovals;
+    }
+
+    private Map<String, Short> getPreviousApprovals(
+        Map<String, Short> allApprovals, Map<String, Short> current) {
+      Map<String, Short> previous = new HashMap<>();
+      for (Map.Entry<String, Short> approval : allApprovals.entrySet()) {
+        // assume vote is 0 if there is no vote
+        if (!current.containsKey(approval.getKey())) {
+          previous.put(approval.getKey(), (short) 0);
+        } else {
+          previous.put(approval.getKey(), current.get(approval.getKey()));
+        }
+      }
+      return previous;
+    }
+
     private boolean updateLabels(ChangeContext ctx)
         throws OrmException, ResourceConflictException {
       Map<String, Short> inLabels = MoreObjects.firstNonNull(in.labels,
@@ -562,30 +586,16 @@
         return false;
       }
 
-      List<PatchSetApproval> del = Lists.newArrayList();
-      List<PatchSetApproval> ups = Lists.newArrayList();
+      List<PatchSetApproval> del = new ArrayList<>();
+      List<PatchSetApproval> ups = new ArrayList<>();
       Map<String, PatchSetApproval> current = scanLabels(ctx, del);
-
-      // get all approvals in cases of quick approve vote
-      Map<String, Short> allApprovals = approvalsByKey(current.values());
-      allApprovals.putAll(inLabels);
-
-      // get previous label votes
-      Map<String, Short> currentLabels = new HashMap<>();
-      for (Map.Entry<String, PatchSetApproval> ent : current.entrySet()) {
-        currentLabels.put(ent.getValue().getLabel(), ent.getValue().getValue());
-      }
-      Map<String, Short> previous = new HashMap<>();
-      for (Map.Entry<String, Short> ent : allApprovals.entrySet()) {
-        if (!currentLabels.containsKey(ent.getKey())) {
-          previous.put(ent.getKey(), (short)0);
-        } else {
-          previous.put(ent.getKey(), currentLabels.get(ent.getKey()));
-        }
-      }
+      LabelTypes labelTypes = ctx.getControl().getLabelTypes();
+      Map<String, Short> allApprovals = getAllApprovals(labelTypes,
+          approvalsByKey(current.values()), inLabels);
+      Map<String, Short> previous = getPreviousApprovals(allApprovals,
+          approvalsByKey(current.values()));
 
       ChangeUpdate update = ctx.getUpdate(psId);
-      LabelTypes labelTypes = ctx.getControl().getLabelTypes();
       for (Map.Entry<String, Short> ent : allApprovals.entrySet()) {
         String name = ent.getKey();
         LabelType lt = checkNotNull(labelTypes.byLabel(name), name);
@@ -607,6 +617,7 @@
         } else if (c != null && c.getValue() != ent.getValue()) {
           c.setValue(ent.getValue());
           c.setGranted(ctx.getWhen());
+          c.setTag(in.tag);
           ups.add(c);
           addLabelDelta(normName, c.getValue());
           oldApprovals.put(normName, previous.get(normName));
@@ -622,6 +633,7 @@
                   user.getAccountId(),
                   lt.getLabelId()),
               ent.getValue(), ctx.getWhen());
+          c.setTag(in.tag);
           c.setGranted(ctx.getWhen());
           ups.add(c);
           addLabelDelta(normName, c.getValue());
@@ -656,6 +668,7 @@
               ctx.getControl().getLabelTypes().getLabelTypes().get(0)
                   .getLabelId()),
               (short) 0, ctx.getWhen());
+          c.setTag(in.tag);
           c.setGranted(ctx.getWhen());
           ups.add(c);
         } else {
@@ -675,7 +688,7 @@
     private Map<String, PatchSetApproval> scanLabels(ChangeContext ctx,
         List<PatchSetApproval> del) throws OrmException {
       LabelTypes labelTypes = ctx.getControl().getLabelTypes();
-      Map<String, PatchSetApproval> current = Maps.newHashMap();
+      Map<String, PatchSetApproval> current = new HashMap<>();
 
       for (PatchSetApproval a : approvalsUtil.byPatchSetUser(
           ctx.getDb(), ctx.getControl(), psId, user.getAccountId())) {
@@ -719,6 +732,7 @@
           user.getAccountId(),
           ctx.getWhen(),
           psId);
+      message.setTag(in.tag);
       message.setMessage(String.format(
           "Patch Set %d:%s",
           psId.get(),
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 d6e80f4..7cb410e 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
@@ -17,7 +17,6 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.GroupDescription;
@@ -63,6 +62,7 @@
 
 import java.io.IOException;
 import java.text.MessageFormat;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -170,7 +170,7 @@
       return result;
     }
 
-    Map<Account.Id, ChangeControl> reviewers = Maps.newHashMap();
+    Map<Account.Id, ChangeControl> reviewers = new HashMap<>();
     ChangeControl control = rsrc.getControl();
     Set<Account> members;
     try {
@@ -275,10 +275,11 @@
               rsrc.getChange(),
               reviewers.keySet());
 
-      if (!added.isEmpty()) {
-        patchSet = psUtil.current(dbProvider.get(), rsrc.getNotes());
+      if (added.isEmpty()) {
+        return false;
       }
-      return !added.isEmpty();
+      patchSet = psUtil.current(dbProvider.get(), rsrc.getNotes());
+      return true;
     }
 
     @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java
index 74cbd5f..ff5185e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java
@@ -192,7 +192,6 @@
       if (wasDraftChange) {
         change.setStatus(Change.Status.NEW);
         update.setStatus(change.getStatus());
-        ctx.saveChange();
       }
     }
 
@@ -202,10 +201,6 @@
         throw new ResourceConflictException("Patch set is not a draft");
       }
       psUtil.publish(ctx.getDb(), ctx.getUpdate(psId), patchSet);
-      // Force ETag invalidation if not done already
-      if (!wasDraftChange) {
-        ctx.saveChange();
-      }
     }
 
     private void addReviewers(ChangeContext ctx)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java
index 8a1f290..1d756f4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java
@@ -119,7 +119,8 @@
         // because the input might be missing required fields. Just give up.
         throw new ResourceNotFoundException("comment not found: " + key);
       }
-      comment = maybeComment.get();
+      PatchLineComment origComment = maybeComment.get();
+      comment = new PatchLineComment(origComment);
 
       PatchSet.Id psId = comment.getKey().getParentKey().getParentKey();
       ChangeUpdate update = ctx.getUpdate(psId);
@@ -134,7 +135,7 @@
         // Delete then recreate the comment instead of an update.
 
         plcUtil.deleteComments(
-            ctx.getDb(), update, Collections.singleton(comment));
+            ctx.getDb(), update, Collections.singleton(origComment));
         comment = new PatchLineComment(
             new PatchLineComment.Key(
                 new Patch.Key(psId, in.path),
@@ -142,6 +143,7 @@
             comment.getLine(),
             ctx.getUser().getAccountId(),
             comment.getParentUuid(), ctx.getWhen());
+        comment.setTag(origComment.getTag());
         setCommentRevId(comment, patchListCache, ctx.getChange(), ps);
         plcUtil.putComments(ctx.getDb(), update,
             Collections.singleton(update(comment, in, ctx.getWhen())));
@@ -153,6 +155,7 @@
         plcUtil.putComments(ctx.getDb(), update,
             Collections.singleton(update(comment, in, ctx.getWhen())));
       }
+      ctx.bumpLastUpdatedOn(false);
       return true;
     }
   }
@@ -171,6 +174,10 @@
       e.setLine(in.range != null ? in.range.endLine : in.line);
     }
     e.setWrittenOn(when);
+    if (in.tag != null) {
+      // TODO(dborowitz): Can we support changing tags via PUT?
+      e.setTag(in.tag);
+    }
     return e;
   }
 }
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 65fb5ae..f0db81d 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
@@ -90,7 +90,7 @@
     private String oldTopicName;
     private String newTopicName;
 
-    public Op(Input input) {
+    Op(Input input) {
       this.input = input;
     }
 
@@ -114,7 +114,6 @@
       }
       change.setTopic(Strings.emptyToNull(newTopicName));
       update.setTopic(change.getTopic());
-      ctx.saveChange();
 
       ChangeMessage cmsg = new ChangeMessage(
           new ChangeMessage.Key(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java
index c7e11d2..50a8be0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java
@@ -56,6 +56,7 @@
   private final PatchSetInserter.Factory patchSetInserterFactory;
   private final MergeUtil.Factory mergeUtilFactory;
   private final RebaseUtil rebaseUtil;
+  private final ChangeResource.Factory changeResourceFactory;
 
   private final ChangeControl ctl;
   private final PatchSet originalPatchSet;
@@ -77,12 +78,14 @@
       PatchSetInserter.Factory patchSetInserterFactory,
       MergeUtil.Factory mergeUtilFactory,
       RebaseUtil rebaseUtil,
+      ChangeResource.Factory changeResourceFactory,
       @Assisted ChangeControl ctl,
       @Assisted PatchSet originalPatchSet,
       @Assisted @Nullable String baseCommitish) {
     this.patchSetInserterFactory = patchSetInserterFactory;
     this.mergeUtilFactory = mergeUtilFactory;
     this.rebaseUtil = rebaseUtil;
+    this.changeResourceFactory = changeResourceFactory;
     this.ctl = ctl;
     this.originalPatchSet = originalPatchSet;
     this.baseCommitish = baseCommitish;
@@ -138,7 +141,8 @@
     RevId baseRevId = new RevId((baseCommitish != null) ? baseCommitish
         : ObjectId.toString(baseCommit.getId()));
     Base base = rebaseUtil.parseBase(
-        new RevisionResource(new ChangeResource(ctl), originalPatchSet),
+        new RevisionResource(
+            changeResourceFactory.create(ctl), originalPatchSet),
         baseRevId.get());
 
     rebasedPatchSetId = ChangeUtil.nextPatchSetId(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseUtil.java
index ae2672b..0956f9e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseUtil.java
@@ -81,7 +81,7 @@
   }
 
   @AutoValue
-  static abstract class Base {
+  abstract static class Base {
     private static Base create(ChangeControl ctl, PatchSet ps) {
       if (ctl == null) {
         return null;
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 8696ed6..b03194e 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
@@ -117,7 +117,6 @@
       patchSet = psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
       change.setStatus(Status.NEW);
       change.setLastUpdatedOn(ctx.getWhen());
-      ctx.saveChange();
       update.setStatus(change.getStatus());
 
       message = newMessage(ctx);
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 c4ce3c9..5f48daa 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
@@ -214,7 +214,7 @@
   private class SendEmailOp extends BatchUpdate.Op {
     private final ChangeInserter ins;
 
-    public SendEmailOp(ChangeInserter ins) {
+    SendEmailOp(ChangeInserter ins) {
       this.ins = ins;
     }
 
@@ -236,7 +236,7 @@
   private class PostRevertedMessageOp extends BatchUpdate.Op {
     private final ObjectId computedChangeId;
 
-    public PostRevertedMessageOp(ObjectId computedChangeId) {
+    PostRevertedMessageOp(ObjectId computedChangeId) {
       this.computedChangeId = computedChangeId;
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerResource.java
index d48151b..aac9252 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerResource.java
@@ -28,7 +28,7 @@
   public static final TypeLiteral<RestView<ReviewerResource>> REVIEWER_KIND =
       new TypeLiteral<RestView<ReviewerResource>>() {};
 
-  public static interface Factory {
+  public interface Factory {
     ReviewerResource create(ChangeResource change, Account.Id id);
   }
 
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 c8db0ed..a390505 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
@@ -35,6 +35,7 @@
 import com.google.inject.Singleton;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
@@ -113,7 +114,7 @@
       // Impossibly long identifier will never match.
       return Collections.emptyList();
     } else {
-      List<RevisionResource> out = Lists.newArrayList();
+      List<RevisionResource> out = new ArrayList<>();
       for (PatchSet ps : psUtil.byChange(dbProvider.get(), change.getNotes())) {
         if (ps.getRevision() != null && ps.getRevision().get().startsWith(id)) {
           out.add(new RevisionResource(change, ps));
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 0c60569..5b4e430 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
@@ -121,6 +121,7 @@
 
     public TestSubmitInput(SubmitInput base, boolean failAfterRefUpdates) {
       this.onBehalfOf = base.onBehalfOf;
+      this.notify = base.notify;
       this.failAfterRefUpdates = failAfterRefUpdates;
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestChangeReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestChangeReviewers.java
index 46fbe67..8cbdf14 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestChangeReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestChangeReviewers.java
@@ -73,4 +73,4 @@
       };
     }
   }
-}
\ No newline at end of file
+}
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 3eecea3..45d9669 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
@@ -16,7 +16,6 @@
 
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.TestSubmitRuleInput;
@@ -34,6 +33,7 @@
 
 import org.kohsuke.args4j.Option;
 
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -116,31 +116,31 @@
       switch (n.status) {
         case OK:
           if (ok == null) {
-            ok = Maps.newLinkedHashMap();
+            ok = new LinkedHashMap<>();
           }
           ok.put(n.label, who);
           break;
         case REJECT:
           if (reject == null) {
-            reject = Maps.newLinkedHashMap();
+            reject = new LinkedHashMap<>();
           }
           reject.put(n.label, who);
           break;
         case NEED:
           if (need == null) {
-            need = Maps.newLinkedHashMap();
+            need = new LinkedHashMap<>();
           }
           need.put(n.label, new None());
           break;
         case MAY:
           if (may == null) {
-            may = Maps.newLinkedHashMap();
+            may = new LinkedHashMap<>();
           }
           may.put(n.label, who);
           break;
         case IMPOSSIBLE:
           if (impossible == null) {
-            impossible = Maps.newLinkedHashMap();
+            impossible = new LinkedHashMap<>();
           }
           impossible.put(n.label, new None());
           break;
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 5a1fdc6..73954b5 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
@@ -21,6 +21,7 @@
 import com.google.gerrit.common.EventListener;
 import com.google.gerrit.common.UserScopedEventListener;
 import com.google.gerrit.extensions.auth.oauth.OAuthLoginProvider;
+import com.google.gerrit.extensions.auth.oauth.OAuthTokenEncrypter;
 import com.google.gerrit.extensions.config.CapabilityDefinition;
 import com.google.gerrit.extensions.config.CloneCommand;
 import com.google.gerrit.extensions.config.DownloadCommand;
@@ -45,6 +46,7 @@
 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.extensions.webui.WebUiPlugin;
 import com.google.gerrit.rules.PrologModule;
 import com.google.gerrit.rules.RulesCache;
 import com.google.gerrit.server.AnonymousUser;
@@ -69,9 +71,11 @@
 import com.google.gerrit.server.account.GroupDetailFactory;
 import com.google.gerrit.server.account.GroupIncludeCacheImpl;
 import com.google.gerrit.server.account.GroupMembers;
+import com.google.gerrit.server.account.VersionedAuthorizedKeys;
 import com.google.gerrit.server.api.accounts.AccountExternalIdCreator;
 import com.google.gerrit.server.auth.AuthBackend;
 import com.google.gerrit.server.auth.UniversalAuthBackend;
+import com.google.gerrit.server.auth.oauth.OAuthTokenCache;
 import com.google.gerrit.server.avatar.AvatarProvider;
 import com.google.gerrit.server.cache.CacheRemovalListener;
 import com.google.gerrit.server.change.ChangeJson;
@@ -101,16 +105,16 @@
 import com.google.gerrit.server.git.validators.RefOperationValidators;
 import com.google.gerrit.server.git.validators.UploadValidationListener;
 import com.google.gerrit.server.git.validators.UploadValidators;
-import com.google.gerrit.server.group.GroupInfoCacheFactory;
+import com.google.gerrit.server.group.GroupInfoCache;
 import com.google.gerrit.server.group.GroupModule;
 import com.google.gerrit.server.index.change.ReindexAfterUpdate;
 import com.google.gerrit.server.mail.AddKeySender;
 import com.google.gerrit.server.mail.AddReviewerSender;
 import com.google.gerrit.server.mail.CreateChangeSender;
+import com.google.gerrit.server.mail.DeleteReviewerSender;
 import com.google.gerrit.server.mail.EmailModule;
 import com.google.gerrit.server.mail.FromAddressGenerator;
 import com.google.gerrit.server.mail.FromAddressGeneratorProvider;
-import com.google.gerrit.server.mail.MergeFailSender;
 import com.google.gerrit.server.mail.MergedSender;
 import com.google.gerrit.server.mail.RegisterNewEmailSender;
 import com.google.gerrit.server.mail.ReplacePatchSetSender;
@@ -144,6 +148,8 @@
 import com.google.gerrit.server.validators.HashtagValidationListener;
 import com.google.gerrit.server.validators.OutgoingEmailValidationListener;
 import com.google.gerrit.server.validators.ProjectCreationValidationListener;
+import com.google.gitiles.blame.BlameCache;
+import com.google.gitiles.blame.BlameCacheImpl;
 import com.google.inject.Inject;
 import com.google.inject.TypeLiteral;
 import com.google.inject.internal.UniqueAnnotations;
@@ -171,6 +177,7 @@
 
     bind(IdGenerator.class);
     bind(RulesCache.class);
+    bind(BlameCache.class).to(BlameCacheImpl.class);
     bind(Sequences.class);
     install(authModule);
     install(AccountByEmailCacheImpl.module());
@@ -185,6 +192,7 @@
     install(SectionSortCache.module());
     install(SubmitStrategy.module());
     install(TagCache.module());
+    install(OAuthTokenCache.module());
 
     install(new AccessControlModule());
     install(new CmdLineParserModule());
@@ -200,6 +208,7 @@
 
     factory(AccountInfoCacheFactory.Factory.class);
     factory(AddReviewerSender.Factory.class);
+    factory(DeleteReviewerSender.Factory.class);
     factory(AddKeySender.Factory.class);
     factory(BatchUpdate.Factory.class);
     factory(CapabilityControl.Factory.class);
@@ -207,11 +216,10 @@
     factory(ChangeJson.Factory.class);
     factory(CreateChangeSender.Factory.class);
     factory(GroupDetailFactory.Factory.class);
-    factory(GroupInfoCacheFactory.Factory.class);
+    factory(GroupInfoCache.Factory.class);
     factory(GroupMembers.Factory.class);
     factory(EmailMerge.Factory.class);
     factory(MergedSender.Factory.class);
-    factory(MergeFailSender.Factory.class);
     factory(MergeUtil.Factory.class);
     factory(PatchScriptFactory.Factory.class);
     factory(PluginUser.Factory.class);
@@ -308,7 +316,9 @@
     DynamicSet.setOf(binder(), ProjectWebLink.class);
     DynamicSet.setOf(binder(), BranchWebLink.class);
     DynamicMap.mapOf(binder(), OAuthLoginProvider.class);
+    DynamicItem.itemOf(binder(), OAuthTokenEncrypter.class);
     DynamicSet.setOf(binder(), AccountExternalIdCreator.class);
+    DynamicSet.setOf(binder(), WebUiPlugin.class);
 
     factory(UploadValidators.Factory.class);
     DynamicSet.setOf(binder(), UploadValidationListener.class);
@@ -325,6 +335,7 @@
     factory(SubmoduleSectionParser.Factory.class);
     factory(ReplaceOp.Factory.class);
     factory(GitModules.Factory.class);
+    factory(VersionedAuthorizedKeys.Factory.class);
 
     bind(AccountManager.class);
     factory(ChangeUserName.Factory.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/config/GerritServerIdProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerIdProvider.java
similarity index 94%
rename from gerrit-server/src/main/java/com/google/gerrit/server/api/config/GerritServerIdProvider.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerIdProvider.java
index 20ea8f0..9479438 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/config/GerritServerIdProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerIdProvider.java
@@ -12,13 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.api.config;
+package com.google.gerrit.server.config;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.base.Strings;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java
index e5ac370..6df7a2e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java
@@ -26,7 +26,9 @@
 import com.google.gerrit.extensions.config.DownloadScheme;
 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.restapi.RestReadView;
+import com.google.gerrit.extensions.webui.WebUiPlugin;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.server.EnableSignedPush;
@@ -58,6 +60,7 @@
   private final DynamicMap<DownloadScheme> downloadSchemes;
   private final DynamicMap<DownloadCommand> downloadCommands;
   private final DynamicMap<CloneCommand> cloneCommands;
+  private final DynamicSet<WebUiPlugin> plugins;
   private final GetArchive.AllowedFormats archiveFormats;
   private final AllProjectsName allProjectsName;
   private final AllUsersName allUsersName;
@@ -75,6 +78,7 @@
       DynamicMap<DownloadScheme> downloadSchemes,
       DynamicMap<DownloadCommand> downloadCommands,
       DynamicMap<CloneCommand> cloneCommands,
+      DynamicSet<WebUiPlugin> webUiPlugins,
       GetArchive.AllowedFormats archiveFormats,
       AllProjectsName allProjectsName,
       AllUsersName allUsersName,
@@ -89,6 +93,7 @@
     this.downloadSchemes = downloadSchemes;
     this.downloadCommands = downloadCommands;
     this.cloneCommands = cloneCommands;
+    this.plugins = webUiPlugins;
     this.archiveFormats = archiveFormats;
     this.allProjectsName = allProjectsName;
     this.allUsersName = allUsersName;
@@ -162,6 +167,7 @@
 
   private ChangeConfigInfo getChangeInfo(Config cfg) {
     ChangeConfigInfo info = new ChangeConfigInfo();
+    info.allowBlame = toBoolean(cfg.getBoolean("change", "allowBlame", true));
     info.allowDrafts = toBoolean(cfg.getBoolean("change", "allowDrafts", true));
     info.largeChange = cfg.getInt("change", "largeChange", 500);
     info.replyTooltip =
@@ -270,6 +276,12 @@
   private PluginConfigInfo getPluginInfo() {
     PluginConfigInfo info = new PluginConfigInfo();
     info.hasAvatars = toBoolean(avatar.get() != null);
+    info.jsResourcePaths = new ArrayList<>();
+    for (WebUiPlugin u : plugins) {
+      info.jsResourcePaths.add(String.format("plugins/%s/%s",
+          u.getPluginName(),
+          u.getJavaScriptResourcePath()));
+    }
     return info;
   }
 
@@ -347,6 +359,7 @@
   }
 
   public static class ChangeConfigInfo {
+    public Boolean allowBlame;
     public Boolean allowDrafts;
     public int largeChange;
     public String replyLabel;
@@ -385,6 +398,7 @@
 
   public static class PluginConfigInfo {
     public Boolean hasAvatars;
+    public List<String> jsResourcePaths;
   }
 
   public static class SshdInfo {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java
index 5dd2784..49b3467 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.config;
 
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.util.ServerRequestContext;
@@ -30,8 +31,8 @@
       @GerritServerConfig Config config,
       ThreadLocalRequestContext threadContext,
       ServerRequestContext serverCtx) {
-    super(gb, threadContext, serverCtx, config.getStringList("receive", null,
-        "allowGroup"));
+    super(gb, threadContext, serverCtx, ImmutableList.copyOf(
+        config.getStringList("receive", null, "allowGroup")));
 
     // If no group was set, default to "registered users"
     //
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java
index 545f48b..b772089 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.config;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.group.SystemGroupBackend;
@@ -29,8 +30,8 @@
       @GerritServerConfig Config config,
       ThreadLocalRequestContext threadContext,
       ServerRequestContext serverCtx) {
-    super(gb, threadContext, serverCtx, config.getStringList("upload", null,
-        "allowGroup"));
+    super(gb, threadContext, serverCtx, ImmutableList.copyOf(
+        config.getStringList("upload", null, "allowGroup")));
 
     // If no group was set, default to "registered users" and "anonymous"
     //
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitwebCgiConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitwebCgiConfig.java
index 4a58f73..830579f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitwebCgiConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitwebCgiConfig.java
@@ -53,8 +53,12 @@
 
     String cfgCgi = cfg.getString("gitweb", null, "cgi");
     Path pkgCgi = Paths.get("/usr/lib/cgi-bin/gitweb.cgi");
-    String[] resourcePaths = {"/usr/share/gitweb/static", "/usr/share/gitweb",
-        "/var/www/static", "/var/www"};
+    String[] resourcePaths = {
+        "/usr/share/gitweb/static",
+        "/usr/share/gitweb",
+        "/var/www/static",
+        "/var/www",
+    };
     Path cgi;
 
     if (cfgCgi != null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java
index 9e55a7f..0307b7c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java
@@ -28,6 +28,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.util.List;
 import java.util.Set;
 
 public abstract class GroupSetProvider implements
@@ -40,7 +41,7 @@
   @Inject
   protected GroupSetProvider(GroupBackend groupBackend,
       ThreadLocalRequestContext threadContext,
-      ServerRequestContext serverCtx, String[] groupNames) {
+      ServerRequestContext serverCtx, List<String> groupNames) {
     RequestContext ctx = threadContext.setContext(serverCtx);
     try {
       ImmutableSet.Builder<AccountGroup.UUID> builder = ImmutableSet.builder();
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
index 653f5bd..4183e61 100644
--- 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
@@ -41,7 +41,7 @@
 public class ListCaches implements RestReadView<ConfigResource> {
   private final DynamicMap<Cache<?, ?>> cacheMap;
 
-  public static enum OutputFormat {
+  public enum OutputFormat {
     LIST, TEXT_LIST
   }
 
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 c6ba605..3a87239 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
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.config;
 
 import com.google.common.base.CharMatcher;
-import com.google.common.collect.Maps;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.config.CapabilityDefinition;
 import com.google.gerrit.extensions.registration.DynamicMap;
@@ -28,6 +27,7 @@
 import org.slf4j.LoggerFactory;
 
 import java.util.Map;
+import java.util.TreeMap;
 
 /** List capabilities visible to the calling user. */
 @Singleton
@@ -43,7 +43,7 @@
   @Override
   public Map<String, CapabilityInfo> apply(ConfigResource resource)
       throws IllegalAccessException, NoSuchFieldException {
-    Map<String, CapabilityInfo> output = Maps.newTreeMap();
+    Map<String, CapabilityInfo> output = new TreeMap<>();
     collectCoreCapabilities(output);
     collectPluginCapabilities(output);
     return output;
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 291e48b..c7d10a3 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
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.server.config;
 
-import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.registration.DynamicSet;
 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.ArrayList;
 import java.util.List;
 
 @Singleton
@@ -34,7 +34,7 @@
 
   @Override
   public List<TopMenu.MenuEntry> apply(ConfigResource resource) {
-    List<TopMenu.MenuEntry> entries = Lists.newArrayList();
+    List<TopMenu.MenuEntry> entries = new ArrayList<>();
     for (TopMenu extension : extensions) {
       entries.addAll(extension.getEntries());
     }
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 a2aa937..58a6c60 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
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.config;
 
-import com.google.common.collect.Maps;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.git.ProjectLevelConfig;
 import com.google.gerrit.server.plugins.Plugin;
@@ -36,6 +35,7 @@
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.Path;
+import java.util.HashMap;
 import java.util.Map;
 
 @Singleton
@@ -60,7 +60,7 @@
     this.cfgProvider = cfgProvider;
     this.projectCache = projectCache;
     this.projectStateFactory = projectStateFactory;
-    this.pluginConfigs = Maps.newHashMap();
+    this.pluginConfigs = new HashMap<>();
 
     this.cfgSnapshot = FileSnapshot.save(site.gerrit_config.toFile());
     this.cfg = cfgProvider.get();
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
index 5178210..33a458e 100644
--- 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
@@ -52,7 +52,7 @@
     }
   }
 
-  public static enum Operation {
+  public enum Operation {
     FLUSH_ALL, FLUSH
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/RepositoryConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/RepositoryConfig.java
index 13188dc..e250395 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/RepositoryConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/RepositoryConfig.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.config;
 
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.inject.Inject;
@@ -46,9 +47,9 @@
         DEFAULT_SUBMIT_TYPE_NAME, SubmitType.MERGE_IF_NECESSARY);
   }
 
-  public String[] getOwnerGroups(Project.NameKey project) {
-    return cfg.getStringList(SECTION_NAME, findSubSection(project.get()),
-        OWNER_GROUP_NAME);
+  public List<String> getOwnerGroups(Project.NameKey project) {
+    return ImmutableList.copyOf(cfg.getStringList(SECTION_NAME,
+        findSubSection(project.get()), OWNER_GROUP_NAME));
   }
 
   public Path getBasePath(Project.NameKey project) {
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
index 4c5ab65..bbf2299 100644
--- 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
@@ -60,6 +60,8 @@
         || i.legacycidInChangeTable != null
         || i.muteCommonPathPrefixes != null
         || i.reviewCategoryStrategy != null
+        || i.signedOffBy != null
+        || i.urlAliases != null
         || i.emailStrategy != null) {
       throw new BadRequestException("unsupported option");
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/TrackingFooters.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/TrackingFooters.java
index 6fbc206..672c461 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/TrackingFooters.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/TrackingFooters.java
@@ -39,6 +39,10 @@
 
   public Multimap<String, String> extract(List<FooterLine> lines) {
     Multimap<String, String> r = ArrayListMultimap.create();
+    if (lines == null) {
+      return r;
+    }
+
     for (FooterLine footer : lines) {
       for (TrackingFooter config : trackingFooters) {
         if (footer.matches(config.footerKey())) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditJson.java
index 738d309..be9e1b5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditJson.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.edit;
 
-import com.google.common.collect.Maps;
 import com.google.gerrit.extensions.common.CommitInfo;
 import com.google.gerrit.extensions.common.EditInfo;
 import com.google.gerrit.extensions.common.FetchInfo;
@@ -31,6 +30,7 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 
 import java.util.ArrayList;
+import java.util.LinkedHashMap;
 import java.util.Map;
 
 @Singleton
@@ -78,7 +78,7 @@
   }
 
   private Map<String, FetchInfo> fillFetchMap(ChangeEdit edit) {
-    Map<String, FetchInfo> r = Maps.newLinkedHashMap();
+    Map<String, FetchInfo> r = new LinkedHashMap<>();
     for (DynamicMap.Entry<DownloadScheme> e : downloadSchemes) {
       String schemeName = e.getExportName();
       DownloadScheme scheme = e.getProvider().get();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
index 1528751..244c64c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
@@ -46,6 +46,7 @@
 import org.eclipse.jgit.dircache.DirCacheEditor.DeletePath;
 import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
 import org.eclipse.jgit.dircache.DirCacheEntry;
+import org.eclipse.jgit.errors.InvalidObjectIdException;
 import org.eclipse.jgit.lib.BatchRefUpdate;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.FileMode;
@@ -70,6 +71,7 @@
 import java.sql.Timestamp;
 import java.util.Map;
 import java.util.TimeZone;
+import java.util.concurrent.atomic.AtomicReference;
 
 /**
  * Utility functions to manipulate change edits.
@@ -82,7 +84,7 @@
 @Singleton
 public class ChangeEditModifier {
 
-  private static enum TreeOperation {
+  private enum TreeOperation {
     CHANGE_ENTRY,
     DELETE_ENTRY,
     RENAME_ENTRY,
@@ -334,14 +336,15 @@
         ObjectReader reader = repo.newObjectReader()) {
       String refName = edit.getRefName();
       RevCommit prevEdit = edit.getEditCommit();
-      ObjectId newTree = writeNewTree(op,
+      ObjectId newTree = writeNewTree(
+          op,
           rw,
           inserter,
           prevEdit,
           reader,
           file,
           newFile,
-          toBlob(inserter, content));
+          content);
       if (ObjectId.equals(newTree, prevEdit.getTree())) {
         throw new InvalidChangeOperationException("no changes were made");
       }
@@ -402,10 +405,16 @@
     return res;
   }
 
-  private static ObjectId writeNewTree(TreeOperation op, RevWalk rw,
-      ObjectInserter ins, RevCommit prevEdit, ObjectReader reader,
-      String fileName, @Nullable String newFile,
-      @Nullable final ObjectId content) throws IOException {
+  private static ObjectId writeNewTree(
+      TreeOperation op,
+      RevWalk rw,
+      final ObjectInserter ins,
+      RevCommit prevEdit,
+      ObjectReader reader,
+      String fileName,
+      @Nullable String newFile,
+      @Nullable final RawInput content)
+      throws InvalidChangeOperationException, IOException {
     DirCache newTree = readTree(reader, prevEdit);
     DirCacheEditor dce = newTree.editor();
     switch (op) {
@@ -425,15 +434,41 @@
 
       case CHANGE_ENTRY:
         checkNotNull(content, "new content required");
+
+        final AtomicReference<IOException> ioe =
+            new AtomicReference<>(null);
+        final AtomicReference<InvalidChangeOperationException> icoe =
+            new AtomicReference<>(null);
         dce.add(new PathEdit(fileName) {
           @Override
           public void apply(DirCacheEntry ent) {
-            if (ent.getRawMode() == 0) {
-              ent.setFileMode(FileMode.REGULAR_FILE);
+            try {
+              if (ent.getFileMode() == FileMode.GITLINK) {
+                ent.setLength(0);
+                ent.setLastModified(0);
+                ent.setObjectId(ObjectId.fromString(
+                    ByteStreams.toByteArray(content.getInputStream()), 0));
+              } else {
+                if (ent.getRawMode() == 0) {
+                  ent.setFileMode(FileMode.REGULAR_FILE);
+                }
+                ent.setObjectId(toBlob(ins, content));
+              }
+            } catch (IOException e) {
+              ioe.set(e);
+            } catch (InvalidObjectIdException e) {
+              icoe.set(new InvalidChangeOperationException(
+                  "Invalid object id in submodule link: " + e.getMessage()));
+              icoe.get().initCause(e);
             }
-            ent.setObjectId(content);
           }
         });
+        if (ioe.get() != null) {
+          throw ioe.get();
+        }
+        if (icoe.get() != null) {
+          throw icoe.get();
+        }
         break;
 
       case RESTORE_ENTRY:
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java
index b8e6093..bfa3188 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java
@@ -147,7 +147,7 @@
       int n = change.currentPatchSetId().get();
       String[] refNames = new String[n];
       for (int i = n; i > 0; i--) {
-        refNames[i-1] = RefNames.refsEdit(
+        refNames[i - 1] = RefNames.refsEdit(
             u.getAccountId(), change.getId(),
             new PatchSet.Id(change.getId(), i));
       }
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 50d02df..5807c26 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
@@ -244,7 +244,7 @@
         SubmitLabelAttribute la = new SubmitLabelAttribute();
         la.label = lbl.label;
         la.status = lbl.status.name();
-        if(lbl.appliedBy != null) {
+        if (lbl.appliedBy != null) {
           Account a = accountCache.get(lbl.appliedBy).getAccount();
           la.by = asAccountAttribute(a);
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventTypes.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventTypes.java
index 05eb5d8..9ade5ec 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventTypes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventTypes.java
@@ -29,7 +29,6 @@
     register(CommitReceivedEvent.TYPE, CommitReceivedEvent.class);
     register(DraftPublishedEvent.TYPE, DraftPublishedEvent.class);
     register(HashtagsChangedEvent.TYPE, HashtagsChangedEvent.class);
-    register(MergeFailedEvent.TYPE, MergeFailedEvent.class);
     register(RefUpdatedEvent.TYPE, RefUpdatedEvent.class);
     register(RefReceivedEvent.TYPE, RefReceivedEvent.class);
     register(ReviewerAddedEvent.TYPE, ReviewerAddedEvent.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/HashtagsChangedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/HashtagsChangedEvent.java
index 8c9adda..4365c74 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/HashtagsChangedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/HashtagsChangedEvent.java
@@ -28,4 +28,4 @@
   public HashtagsChangedEvent (Change change) {
     super(TYPE, change);
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/MergeFailedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/ReviewerDeletedEvent.java
similarity index 66%
rename from gerrit-server/src/main/java/com/google/gerrit/server/events/MergeFailedEvent.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/events/ReviewerDeletedEvent.java
index 47525b8..1b57906 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/MergeFailedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/ReviewerDeletedEvent.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 The Android Open Source Project
+// Copyright (C) 2016 The Android Open 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,13 +17,15 @@
 import com.google.common.base.Supplier;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.data.AccountAttribute;
+import com.google.gerrit.server.data.ApprovalAttribute;
 
-public class MergeFailedEvent extends PatchSetEvent {
-  static final String TYPE = "merge-failed";
-  public Supplier<AccountAttribute> submitter;
-  public String reason;
+public class ReviewerDeletedEvent extends PatchSetEvent {
+  static final String TYPE = "reviewer-deleted";
+  public Supplier<AccountAttribute> reviewer;
+  public Supplier<ApprovalAttribute[]> approvals;
+  public String comment;
 
-  public MergeFailedEvent(Change change) {
+  public ReviewerDeletedEvent(Change change) {
     super(TYPE, change);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
index e9ad8b7..982777d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
@@ -49,6 +49,8 @@
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.NoSuchRefException;
 import com.google.gerrit.server.schema.DisabledChangesReviewDbWrapper;
+import com.google.gwtorm.server.OrmConcurrencyException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 
@@ -100,7 +102,7 @@
   }
 
   /** Order of execution of the various phases. */
-  public static enum Order {
+  public enum Order {
     /**
      * Update the repository and execute all ref updates before touching the
      * database.
@@ -164,7 +166,8 @@
       return BatchUpdate.this.getObjectInserter();
     }
 
-    public void addRefUpdate(ReceiveCommand cmd) {
+    public void addRefUpdate(ReceiveCommand cmd) throws IOException {
+      initRepository();
       commands.add(cmd);
     }
 
@@ -179,7 +182,7 @@
     private final ReviewDbWrapper dbWrapper;
 
     private boolean deleted;
-    private boolean saved;
+    private boolean bumpLastUpdatedOn = true;
 
     private ChangeContext(ChangeControl ctl, ReviewDbWrapper dbWrapper) {
       this.ctl = ctl;
@@ -223,13 +226,11 @@
       return c;
     }
 
-    public void saveChange() {
-      checkState(!deleted, "cannot both save and delete change");
-      saved = true;
+    public void bumpLastUpdatedOn(boolean bump) {
+      bumpLastUpdatedOn = bump;
     }
 
     public void deleteChange() {
-      checkState(!saved, "cannot both save and delete change");
       deleted = true;
     }
   }
@@ -401,7 +402,7 @@
   private Repository repo;
   private ObjectInserter inserter;
   private RevWalk revWalk;
-  private ChainedReceiveCommands commands = new ChainedReceiveCommands();
+  private ChainedReceiveCommands commands;
   private BatchRefUpdate batchRefUpdate;
   private boolean closeRepo;
   private Order order;
@@ -454,6 +455,7 @@
     this.repo = checkNotNull(repo, "repo");
     this.revWalk = checkNotNull(revWalk, "revWalk");
     this.inserter = checkNotNull(inserter, "inserter");
+    commands = new ChainedReceiveCommands(repo);
     return this;
   }
 
@@ -468,6 +470,7 @@
       closeRepo = true;
       inserter = repo.newObjectInserter();
       revWalk = new RevWalk(inserter.newReader());
+      commands = new ChainedReceiveCommands(repo);
     }
   }
 
@@ -531,7 +534,7 @@
   }
 
   private void executeRefUpdates() throws IOException, UpdateException {
-    if (commands.isEmpty()) {
+    if (commands == null || commands.isEmpty()) {
       return;
     }
     // May not be opened if the caller added ref updates but no new objects.
@@ -571,24 +574,18 @@
 
           // Stage the NoteDb update and store its state in the Change.
           if (!ctx.deleted && notesMigration.writeChanges()) {
-            updateManager = updateManagerFactory.create(ctx.getProject());
-            for (ChangeUpdate u : ctx.updates.values()) {
-              updateManager.add(u);
-            }
-            NoteDbChangeState.applyDelta(
-                ctx.getChange(),
-                updateManager.stage().get(id));
+            updateManager = stageNoteDbUpdate(ctx);
           }
 
           // Bump lastUpdatedOn or rowVersion and commit.
+          Iterable<Change> cs = changesToUpdate(ctx);
           if (newChanges.containsKey(id)) {
-            db.changes().insert(bumpLastUpdatedOn(ctx));
-          } else if (ctx.saved) {
-            db.changes().update(bumpLastUpdatedOn(ctx));
+            // Insert rather than upsert in case of a race on change IDs.
+            db.changes().insert(cs);
           } else if (ctx.deleted) {
-            db.changes().delete(bumpLastUpdatedOn(ctx));
+            db.changes().delete(cs);
           } else {
-            db.changes().update(bumpRowVersionNotLastUpdatedOn(ctx));
+            db.changes().update(cs);
           }
           db.commit();
         } finally {
@@ -618,15 +615,31 @@
     }
   }
 
-  private static Iterable<Change> bumpLastUpdatedOn(ChangeContext ctx) {
-    Change c = ctx.getChange();
-    c.setLastUpdatedOn(ctx.getWhen());
-    return Collections.singleton(c);
+  private NoteDbUpdateManager stageNoteDbUpdate(ChangeContext ctx)
+      throws OrmException, IOException {
+    NoteDbUpdateManager updateManager =
+        updateManagerFactory.create(ctx.getProject());
+    for (ChangeUpdate u : ctx.updates.values()) {
+      updateManager.add(u);
+    }
+    try {
+      NoteDbChangeState.applyDelta(
+          ctx.getChange(),
+          updateManager.stage().get(ctx.getChange().getId()));
+    } catch (OrmConcurrencyException ex) {
+      // Refused to apply update because NoteDb was out of sync. Go ahead with
+      // this ReviewDb update; it's still out of sync, but this is no worse than
+      // before, and it will eventually get rebuilt.
+    }
+    return updateManager;
   }
 
-  private static Iterable<Change> bumpRowVersionNotLastUpdatedOn(
-      ChangeContext ctx) {
-    return Collections.singleton(ctx.getChange());
+  private static Iterable<Change> changesToUpdate(ChangeContext ctx) {
+    Change c = ctx.getChange();
+    if (ctx.bumpLastUpdatedOn && c.getLastUpdatedOn().before(ctx.getWhen())) {
+      c.setLastUpdatedOn(ctx.getWhen());
+    }
+    return Collections.singleton(c);
   }
 
   private ChangeContext newChangeContext(Change.Id id) throws Exception {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChainedReceiveCommands.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChainedReceiveCommands.java
index f2b6a86..a16c5c06 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChainedReceiveCommands.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChainedReceiveCommands.java
@@ -14,18 +14,18 @@
 
 package com.google.gerrit.server.git;
 
-import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.base.Optional;
 
 import org.eclipse.jgit.lib.BatchRefUpdate;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.transport.ReceiveCommand;
 
 import java.io.IOException;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.Map;
 
@@ -37,9 +37,17 @@
  * works around that limitation by allowing multiple updates per ref, as long as
  * the previous new SHA-1 matches the next old SHA-1.
  */
-public class ChainedReceiveCommands {
+public class ChainedReceiveCommands implements RefCache {
   private final Map<String, ReceiveCommand> commands = new LinkedHashMap<>();
-  private final Map<String, ObjectId> oldIds = new HashMap<>();
+  private final RepoRefCache refCache;
+
+  public ChainedReceiveCommands(Repository repo) {
+    this(new RepoRefCache(repo));
+  }
+
+  public ChainedReceiveCommands(RepoRefCache refCache) {
+    this.refCache = checkNotNull(refCache);
+  }
 
   public boolean isEmpty() {
     return commands.isEmpty();
@@ -73,38 +81,20 @@
   /**
    * Get the latest value of a ref according to this sequence of commands.
    * <p>
-   * Once the value for a ref is read once, it is cached in this instance, so
-   * that multiple callers using this instance for lookups see a single
-   * consistent snapshot.
+   * After the value for a ref is read from the repo once, it is cached as in
+   * {@link RepoRefCache}.
    *
-   * @param repo repository to read from, if result is not cached.
-   * @param refName name of the ref.
-   * @return value of the ref, taking into account commands that have already
-   *     been added to this instance. Null if the ref is deleted, matching the
-   *     behavior of {@link Repository#exactRef(String)}.
+   * @see RefCache#get(String)
    */
-  public ObjectId getObjectId(Repository repo, String refName)
-      throws IOException {
+  @Override
+  public Optional<ObjectId> get(String refName) throws IOException {
     ReceiveCommand cmd = commands.get(refName);
     if (cmd != null) {
-      return zeroToNull(cmd.getNewId());
+      return !cmd.getNewId().equals(ObjectId.zeroId())
+          ? Optional.of(cmd.getNewId())
+          : Optional.<ObjectId>absent();
     }
-    ObjectId old = oldIds.get(refName);
-    if (old != null) {
-      return zeroToNull(old);
-    }
-    Ref ref = repo.exactRef(refName);
-    ObjectId id = ref != null ? ref.getObjectId() : null;
-    // Cache missing ref as zeroId to match value in commands map.
-    oldIds.put(refName, firstNonNull(id, ObjectId.zeroId()));
-    return id;
-  }
-
-  private static ObjectId zeroToNull(ObjectId id) {
-    if (id == null || id.equals(ObjectId.zeroId())) {
-      return null;
-    }
-    return id;
+    return refCache.get(refName);
   }
 
   /**
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/DestinationList.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/DestinationList.java
index ca1f705c..7c02e5b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/DestinationList.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/DestinationList.java
@@ -53,7 +53,7 @@
 
   protected static Set<Branch.NameKey> toSet(List<Row> destRows) {
     Set<Branch.NameKey> dests = Sets.newHashSetWithExpectedSize(destRows.size());
-    for(Row row : destRows) {
+    for (Row row : destRows) {
       dests.add(new Branch.NameKey(new Project.NameKey(row.right), row.left));
     }
     return dests;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/EmailMerge.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/EmailMerge.java
index f90b72c..d236682 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/EmailMerge.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/EmailMerge.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.git;
 
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.api.changes.ReviewInput.NotifyHandling;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
@@ -42,7 +43,7 @@
 
   public interface Factory {
     EmailMerge create(Project.NameKey project, Change.Id changeId,
-        Account.Id submitter);
+        Account.Id submitter, NotifyHandling notifyHandling);
   }
 
   private final ExecutorService sendEmailsExecutor;
@@ -54,6 +55,7 @@
   private final Project.NameKey project;
   private final Change.Id changeId;
   private final Account.Id submitter;
+  private final NotifyHandling notifyHandling;
   private ReviewDb db;
 
   @Inject
@@ -64,7 +66,8 @@
       IdentifiedUser.GenericFactory identifiedUserFactory,
       @Assisted Project.NameKey project,
       @Assisted Change.Id changeId,
-      @Assisted @Nullable Account.Id submitter) {
+      @Assisted @Nullable Account.Id submitter,
+      @Assisted NotifyHandling notifyHandling) {
     this.sendEmailsExecutor = executor;
     this.mergedSenderFactory = mergedSenderFactory;
     this.schemaFactory = schemaFactory;
@@ -73,6 +76,7 @@
     this.project = project;
     this.changeId = changeId;
     this.submitter = submitter;
+    this.notifyHandling = notifyHandling;
   }
 
   public void sendAsync() {
@@ -87,6 +91,7 @@
       if (submitter != null) {
         cm.setFrom(submitter);
       }
+      cm.setNotify(notifyHandling);
       cm.send();
     } catch (Exception e) {
       log.error("Cannot email merged notification for " + changeId, e);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollectionQueue.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollectionQueue.java
index 42dc505..90e2aac 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollectionQueue.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollectionQueue.java
@@ -19,11 +19,12 @@
 import com.google.inject.Singleton;
 
 import java.util.Collection;
+import java.util.HashSet;
 import java.util.Set;
 
 @Singleton
 public class GarbageCollectionQueue {
-  private final Set<Project.NameKey> projectsScheduledForGc = Sets.newHashSet();
+  private final Set<Project.NameKey> projectsScheduledForGc = new HashSet<>();
 
   public synchronized Set<Project.NameKey> addAll(Collection<Project.NameKey> projects) {
     Set<Project.NameKey> added =
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModules.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModules.java
index 9042955..eb359e6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModules.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModules.java
@@ -19,6 +19,8 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
 import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.git.MergeOpRepoManager.OpenRepo;
+import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.util.SubmoduleSectionParser;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
@@ -27,9 +29,7 @@
 import org.eclipse.jgit.lib.BlobBasedConfig;
 import org.eclipse.jgit.lib.FileMode;
 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 org.eclipse.jgit.treewalk.TreeWalk;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -49,28 +49,29 @@
   private static final Logger log = LoggerFactory.getLogger(GitModules.class);
 
   public interface Factory {
-    GitModules create(Branch.NameKey project, String submissionId);
+    GitModules create(Branch.NameKey project, String submissionId,
+        MergeOpRepoManager m);
   }
 
   private static final String GIT_MODULES = ".gitmodules";
 
   private final String thisServer;
-  private final GitRepositoryManager repoManager;
   private final SubmoduleSectionParser.Factory subSecParserFactory;
   private final Branch.NameKey branch;
   private final String submissionId;
+  private final MergeOpRepoManager orm;
 
   Set<SubmoduleSubscription> subscriptions;
 
   @AssistedInject
   GitModules(
       @CanonicalWebUrl @Nullable String canonicalWebUrl,
-      GitRepositoryManager repoManager,
       SubmoduleSectionParser.Factory subSecParserFactory,
       @Assisted Branch.NameKey branch,
-      @Assisted String submissionId) throws SubmoduleException {
-    this.repoManager = repoManager;
+      @Assisted String submissionId,
+      @Assisted MergeOpRepoManager orm) throws SubmoduleException {
     this.subSecParserFactory = subSecParserFactory;
+    this.orm = orm;
     this.branch = branch;
     this.submissionId = submissionId;
     try {
@@ -84,24 +85,27 @@
   void load() throws IOException {
     Project.NameKey project = branch.getParentKey();
     logDebug("Loading .gitmodules of {} for project {}", branch, project);
-    try (Repository repo = repoManager.openRepository(project);
-        RevWalk rw = new RevWalk(repo)) {
+    try {
+      orm.openRepo(project, false);
+    } catch (NoSuchProjectException e) {
+      throw new IOException(e);
+    }
+    OpenRepo or = orm.getRepo(project);
 
-      ObjectId id = repo.resolve(branch.get());
-      if (id == null) {
-        throw new IOException("Cannot open branch " + branch.get());
-      }
-      RevCommit commit = rw.parseCommit(id);
+    ObjectId id = or.repo.resolve(branch.get());
+    if (id == null) {
+      throw new IOException("Cannot open branch " + branch.get());
+    }
+    RevCommit commit = or.rw.parseCommit(id);
 
-      TreeWalk tw = TreeWalk.forPath(repo, GIT_MODULES, commit.getTree());
-      if (tw == null
-          || (tw.getRawMode(0) & FileMode.TYPE_MASK) != FileMode.TYPE_FILE) {
-        return;
-      }
-
+    TreeWalk tw = TreeWalk.forPath(or.repo, GIT_MODULES, commit.getTree());
+    if (tw == null
+        || (tw.getRawMode(0) & FileMode.TYPE_MASK) != FileMode.TYPE_FILE) {
+      return;
+    }
+    try {
       BlobBasedConfig bbc =
-          new BlobBasedConfig(null, repo, commit, GIT_MODULES);
-
+          new BlobBasedConfig(null, or.repo, commit, GIT_MODULES);
       subscriptions = subSecParserFactory.create(bbc, thisServer,
           branch).parseAllSections();
     } catch (ConfigInvalidException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java
index ab6d66c..29e14ec 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java
@@ -46,9 +46,6 @@
 
   /**
    * Create (and open) a repository by name.
-   * <p>
-   * If the implementation supports separate metadata repositories, this method
-   * must also create the metadata repository, but does not open it.
    *
    * @param name the repository name, relative to the base directory.
    * @return the cached Repository instance. Caller must call {@code close()}
@@ -62,23 +59,6 @@
       throws RepositoryCaseMismatchException, RepositoryNotFoundException,
       IOException;
 
-  /**
-   * Open the repository storing metadata for the given project.
-   * <p>
-   * This includes any project-specific metadata <em>except</em> what is stored
-   * in {@code refs/meta/config}. Implementations may choose to store all
-   * metadata in the original project.
-   *
-   * @param name the base project name name.
-   * @return the cached metadata Repository instance. Caller must call
-   *         {@code close()} when done to decrement the resource handle.
-   * @throws RepositoryNotFoundException the name does not denote an existing
-   *         repository.
-   * @throws IOException the name cannot be read as a repository.
-   */
-  Repository openMetadataRepository(Project.NameKey name)
-      throws RepositoryNotFoundException, IOException;
-
   /** @return set of all known projects, sorted by natural NameKey order. */
   SortedSet<Project.NameKey> list();
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GroupCollector.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GroupCollector.java
index 8f9add5..d832260 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GroupCollector.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GroupCollector.java
@@ -103,7 +103,7 @@
     return rsrc.getPatchSet().getGroups();
   }
 
-  private static interface Lookup {
+  private interface Lookup {
     List<String> lookup(PatchSet.Id psId)
         throws OrmException, NoSuchChangeException;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GroupList.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GroupList.java
index 1477f6a..f4a03cf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GroupList.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GroupList.java
@@ -39,7 +39,7 @@
     List<Row> rows = parse(text, FILE_NAME, TRIM, TRIM, errors);
     Map<AccountGroup.UUID, GroupReference> groupsByUUID =
         new HashMap<>(rows.size());
-    for(Row row : rows) {
+    for (Row row : rows) {
       AccountGroup.UUID uuid = new AccountGroup.UUID(row.left);
       String name = row.right;
       GroupReference ref = new GroupReference(uuid, name);
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 e0c4c74..da9cf1d 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
@@ -14,16 +14,12 @@
 
 package com.google.gerrit.server.git;
 
-import static com.google.common.base.Preconditions.checkState;
-
-import com.google.common.base.MoreObjects;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -124,23 +120,17 @@
   }
 
   private final Path basePath;
-  private final NotesMigration notesMigration;
-  private final Path noteDbPath;
   private final Lock namesUpdateLock;
   private volatile SortedSet<Project.NameKey> names = new TreeSet<>();
 
   @Inject
   LocalDiskRepositoryManager(SitePaths site,
-      @GerritServerConfig Config cfg,
-      NotesMigration notesMigration) {
-    this.notesMigration = notesMigration;
+      @GerritServerConfig Config cfg) {
     basePath = site.resolve(cfg.getString("gerrit", null, "basePath"));
     if (basePath == null) {
       throw new IllegalStateException("gerrit.basePath must be configured");
     }
 
-    noteDbPath = site.resolve(MoreObjects.firstNonNull(
-        cfg.getString("gerrit", null, "noteDbPath"), "notedb"));
     namesUpdateLock = new ReentrantLock(true /* fair */);
   }
 
@@ -213,15 +203,7 @@
   @Override
   public Repository createRepository(Project.NameKey name)
       throws RepositoryNotFoundException, RepositoryCaseMismatchException {
-    Repository repo = createRepository(getBasePath(name), name);
-    if (notesMigration.writeChanges() && !noteDbPath.equals(basePath)) {
-      createRepository(noteDbPath, name);
-    }
-    return repo;
-  }
-
-  private Repository createRepository(Path path, Project.NameKey name)
-      throws RepositoryNotFoundException, RepositoryCaseMismatchException {
+    Path path = getBasePath(name);
     if (isUnreasonableName(name)) {
       throw new RepositoryNotFoundException("Invalid name: " + name);
     }
@@ -276,17 +258,6 @@
     }
   }
 
-  @Override
-  public Repository openMetadataRepository(Project.NameKey name)
-      throws RepositoryNotFoundException, IOException {
-    checkState(notesMigration.readChanges(), "NoteDb disabled");
-    try {
-      return openRepository(noteDbPath, name);
-    } catch (RepositoryNotFoundException e) {
-      return createRepository(noteDbPath, name);
-    }
-  }
-
   private void onCreateProject(final Project.NameKey newProjectName) {
     namesUpdateLock.lock();
     try {
@@ -362,7 +333,7 @@
     final String name = nameKey.get();
 
     return name.length() == 0  // no empty paths
-      || name.charAt(name.length() -1) == '/' // no suffix
+      || name.charAt(name.length() - 1) == '/' // no suffix
       || name.indexOf('\\') >= 0 // no windows/dos style paths
       || name.charAt(0) == '/' // no absolute paths
       || new File(name).isAbsolute() // no absolute paths
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 07aa892..26db045 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
@@ -28,7 +28,6 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSetMultimap;
 import com.google.common.collect.Iterables;
-import com.google.common.collect.Maps;
 import com.google.common.collect.Multimap;
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.collect.Ordering;
@@ -54,7 +53,8 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.InternalUser;
 import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
-import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
+import com.google.gerrit.server.git.MergeOpRepoManager.OpenBranch;
+import com.google.gerrit.server.git.MergeOpRepoManager.OpenRepo;
 import com.google.gerrit.server.git.strategy.SubmitStrategy;
 import com.google.gerrit.server.git.strategy.SubmitStrategyFactory;
 import com.google.gerrit.server.git.strategy.SubmitStrategyListener;
@@ -62,8 +62,6 @@
 import com.google.gerrit.server.git.validators.MergeValidators;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
@@ -72,17 +70,10 @@
 import com.google.inject.Provider;
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.ObjectReader;
 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.RevFlag;
-import org.eclipse.jgit.revwalk.RevSort;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -97,7 +88,6 @@
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
 import java.util.Set;
 
 /**
@@ -117,88 +107,6 @@
 public class MergeOp implements AutoCloseable {
   private static final Logger log = LoggerFactory.getLogger(MergeOp.class);
 
-  private class OpenRepo {
-    final Repository repo;
-    final CodeReviewRevWalk rw;
-    final RevFlag canMergeFlag;
-    final ObjectInserter ins;
-
-    ProjectState project;
-    BatchUpdate update;
-
-    private final ObjectReader reader;
-    private final Map<Branch.NameKey, OpenBranch> branches;
-
-    OpenRepo(Repository repo, ProjectState project) {
-      this.repo = repo;
-      this.project = project;
-      ins = repo.newObjectInserter();
-      reader = ins.newReader();
-      rw = CodeReviewCommit.newRevWalk(reader);
-      rw.sort(RevSort.TOPO);
-      rw.sort(RevSort.COMMIT_TIME_DESC, true);
-      rw.setRetainBody(false);
-      canMergeFlag = rw.newFlag("CAN_MERGE");
-      rw.retainOnReset(canMergeFlag);
-
-      branches = Maps.newHashMapWithExpectedSize(1);
-    }
-
-    OpenBranch getBranch(Branch.NameKey branch) throws IntegrationException {
-      OpenBranch ob = branches.get(branch);
-      if (ob == null) {
-        ob = new OpenBranch(this, branch);
-        branches.put(branch, ob);
-      }
-      return ob;
-    }
-
-    Project.NameKey getProjectName() {
-      return project.getProject().getNameKey();
-    }
-
-    BatchUpdate getUpdate() {
-      if (update == null) {
-        update = batchUpdateFactory.create(db, getProjectName(), caller, ts);
-        update.setRepository(repo, rw, ins);
-      }
-      return update;
-    }
-
-    void close() {
-      if (update != null) {
-        update.close();
-      }
-      rw.close();
-      reader.close();
-      ins.close();
-      repo.close();
-    }
-  }
-
-  private static class OpenBranch {
-    final RefUpdate update;
-    final CodeReviewCommit oldTip;
-    MergeTip mergeTip;
-
-    OpenBranch(OpenRepo or, Branch.NameKey name) throws IntegrationException {
-      try {
-        update = or.repo.updateRef(name.get());
-        if (update.getOldObjectId() != null) {
-          oldTip = or.rw.parseCommit(update.getOldObjectId());
-        } else if (Objects.equals(or.repo.getFullBranch(), name.get())) {
-          oldTip = null;
-          update.setExpectedOldObjectId(ObjectId.zeroId());
-        } else {
-          throw new IntegrationException("The destination branch "
-              + name + " does not exist anymore.");
-        }
-      } catch (IOException e) {
-        throw new IntegrationException("Cannot open branch " + name, e);
-      }
-    }
-  }
-
   public static class CommitStatus {
     private final ImmutableMap<Change.Id, ChangeData> changes;
     private final ImmutableSetMultimap<Branch.NameKey, Change.Id> byBranch;
@@ -307,16 +215,13 @@
 
   private final ChangeMessagesUtil cmUtil;
   private final BatchUpdate.Factory batchUpdateFactory;
-  private final GitRepositoryManager repoManager;
   private final InternalUser.Factory internalUserFactory;
   private final MergeSuperSet mergeSuperSet;
   private final MergeValidators.Factory mergeValidatorsFactory;
-  private final ProjectCache projectCache;
   private final InternalChangeQuery internalChangeQuery;
   private final SubmitStrategyFactory submitStrategyFactory;
   private final Provider<SubmoduleOp> subOpProvider;
-
-  private final Map<Project.NameKey, OpenRepo> openRepos;
+  private final MergeOpRepoManager orm;
 
   private static final String MACHINE_ID;
   static {
@@ -339,56 +244,27 @@
   @Inject
   MergeOp(ChangeMessagesUtil cmUtil,
       BatchUpdate.Factory batchUpdateFactory,
-      GitRepositoryManager repoManager,
       InternalUser.Factory internalUserFactory,
       MergeSuperSet mergeSuperSet,
       MergeValidators.Factory mergeValidatorsFactory,
-      ProjectCache projectCache,
       InternalChangeQuery internalChangeQuery,
       SubmitStrategyFactory submitStrategyFactory,
-      Provider<SubmoduleOp> subOpProvider) {
+      Provider<SubmoduleOp> subOpProvider,
+      MergeOpRepoManager orm) {
     this.cmUtil = cmUtil;
     this.batchUpdateFactory = batchUpdateFactory;
-    this.repoManager = repoManager;
     this.internalUserFactory = internalUserFactory;
     this.mergeSuperSet = mergeSuperSet;
     this.mergeValidatorsFactory = mergeValidatorsFactory;
-    this.projectCache = projectCache;
     this.internalChangeQuery = internalChangeQuery;
     this.submitStrategyFactory = submitStrategyFactory;
     this.subOpProvider = subOpProvider;
-
-    openRepos = new HashMap<>();
-  }
-
-  private OpenRepo getRepo(Project.NameKey project) {
-    OpenRepo or = openRepos.get(project);
-    checkState(or != null, "repo not yet opened: %s", project);
-    return or;
-  }
-
-  private void openRepo(Project.NameKey project)
-      throws NoSuchProjectException, IOException {
-    checkState(!openRepos.containsKey(project),
-        "repo already opened: %s", project);
-    ProjectState projectState = projectCache.get(project);
-    if (projectState == null) {
-      throw new NoSuchProjectException(project);
-    }
-    try {
-      OpenRepo or =
-          new OpenRepo(repoManager.openRepository(project), projectState);
-      openRepos.put(project, or);
-    } catch (RepositoryNotFoundException e) {
-      throw new NoSuchProjectException(project);
-    }
+    this.orm = orm;
   }
 
   @Override
   public void close() {
-    for (OpenRepo repo : openRepos.values()) {
-      repo.close();
-    }
+    orm.close();
   }
 
   private static Optional<SubmitRecord> findOkRecord(
@@ -544,6 +420,8 @@
     this.caller = caller;
     updateSubmissionId(change);
     this.db = db;
+    orm.setContext(db, ts, caller);
+
     logDebug("Beginning integration of {}", change);
     try {
       ChangeSet cs = mergeSuperSet.completeChangeSet(db, change, caller);
@@ -605,14 +483,14 @@
     openRepos(projects);
 
     for (Branch.NameKey branch : branches) {
-      OpenRepo or = getRepo(branch.getParentKey());
+      OpenRepo or = orm.getRepo(branch.getParentKey());
       toSubmit.put(branch, validateChangeList(or, cbb.get(branch)));
     }
     failFast(cs); // Done checks that don't involve running submit strategies.
 
     List<SubmitStrategy> strategies = new ArrayList<>(branches.size());
     for (Branch.NameKey branch : branches) {
-      OpenRepo or = getRepo(branch.getParentKey());
+      OpenRepo or = orm.getRepo(branch.getParentKey());
       OpenBranch ob = or.getBranch(branch);
       BranchBatch submitting = toSubmit.get(branch);
       checkNotNull(submitting.submitType(),
@@ -647,14 +525,13 @@
       throw new IntegrationException(msg, e);
     }
 
-    SubmoduleOp subOp = subOpProvider.get();
-    updateSuperProjects(subOp, br.values());
+    updateSuperProjects(br.values());
   }
 
   private List<BatchUpdate> batchUpdates(Collection<Project.NameKey> projects) {
     List<BatchUpdate> updates = new ArrayList<>(projects.size());
     for (Project.NameKey project : projects) {
-      updates.add(getRepo(project).getUpdate());
+      updates.add(orm.getRepo(project).getUpdate());
     }
     return updates;
   }
@@ -676,7 +553,7 @@
       CodeReviewCommit branchTip) throws IntegrationException {
     return submitStrategyFactory.create(submitType, db, or.repo, or.rw, or.ins,
         or.canMergeFlag, getAlreadyAccepted(or, branchTip), destBranch, caller,
-        mergeTip, commits, submissionId);
+        mergeTip, commits, submissionId, submitInput.notify);
   }
 
   private Set<RevCommit> getAlreadyAccepted(OpenRepo or,
@@ -706,7 +583,7 @@
   }
 
   @AutoValue
-  static abstract class BranchBatch {
+  abstract static class BranchBatch {
     @Nullable abstract SubmitType submitType();
     abstract List<ChangeData> changes();
   }
@@ -848,11 +725,11 @@
     }
   }
 
-  private void updateSuperProjects(SubmoduleOp subOp,
-      Collection<Branch.NameKey> branches) {
+  private void updateSuperProjects(Collection<Branch.NameKey> branches) {
     logDebug("Updating superprojects");
+    SubmoduleOp subOp = subOpProvider.get();
     try {
-      subOp.updateSuperProjects(db, branches, submissionId);
+      subOp.updateSuperProjects(db, branches, submissionId, orm);
       logDebug("Updating superprojects done");
     } catch (SubmoduleException e) {
       logError("The gitlinks were not updated according to the "
@@ -864,7 +741,7 @@
       throws IntegrationException {
     for (Project.NameKey project : projects) {
       try {
-        openRepo(project);
+        orm.openRepo(project, true);
       } catch (NoSuchProjectException noProject) {
         logWarn("Project " + noProject.project() + " no longer exists, "
             + "abandoning open changes");
@@ -899,7 +776,6 @@
               cmUtil.addChangeMessage(ctx.getDb(),
                   ctx.getUpdate(change.currentPatchSetId()), msg);
 
-              ctx.saveChange();
               return true;
             }
           });
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java
new file mode 100644
index 0000000..1f8930c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java
@@ -0,0 +1,199 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.common.base.Preconditions.checkState;
+
+import com.google.common.collect.Maps;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevFlag;
+import org.eclipse.jgit.revwalk.RevSort;
+
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * This is a helper class for MergeOp and not intended for general use.
+ *
+ * Some database backends require to open a repository just once within
+ * a transaction of a submission, this caches open repositories to satisfy
+ * that requirement.
+ */
+public class MergeOpRepoManager implements AutoCloseable {
+  public class OpenRepo {
+    final Repository repo;
+    final CodeReviewRevWalk rw;
+    final RevFlag canMergeFlag;
+    final ObjectInserter ins;
+
+    final ProjectState project;
+    BatchUpdate update;
+
+    private final ObjectReader reader;
+    private final Map<Branch.NameKey, OpenBranch> branches;
+
+    private OpenRepo(Repository repo, ProjectState project) {
+      this.repo = repo;
+      this.project = project;
+      ins = repo.newObjectInserter();
+      reader = ins.newReader();
+      rw = CodeReviewCommit.newRevWalk(reader);
+      rw.sort(RevSort.TOPO);
+      rw.sort(RevSort.COMMIT_TIME_DESC, true);
+      rw.setRetainBody(false);
+      canMergeFlag = rw.newFlag("CAN_MERGE");
+      rw.retainOnReset(canMergeFlag);
+
+      branches = Maps.newHashMapWithExpectedSize(1);
+    }
+
+    OpenBranch getBranch(Branch.NameKey branch) throws IntegrationException {
+      OpenBranch ob = branches.get(branch);
+      if (ob == null) {
+        ob = new OpenBranch(this, branch);
+        branches.put(branch, ob);
+      }
+      return ob;
+    }
+
+    Project.NameKey getProjectName() {
+      return project.getProject().getNameKey();
+    }
+
+    BatchUpdate getUpdate() {
+      checkState(db != null, "call setContext before getUpdate");
+      if (update == null) {
+        update = batchUpdateFactory.create(db, getProjectName(), caller, ts);
+        update.setRepository(repo, rw, ins);
+      }
+      return update;
+    }
+
+    void close() {
+      if (update != null) {
+        update.close();
+      }
+      rw.close();
+      reader.close();
+      ins.close();
+      repo.close();
+    }
+  }
+
+  public static class OpenBranch {
+    final RefUpdate update;
+    final CodeReviewCommit oldTip;
+    MergeTip mergeTip;
+
+    OpenBranch(OpenRepo or, Branch.NameKey name) throws IntegrationException {
+      try {
+        update = or.repo.updateRef(name.get());
+        if (update.getOldObjectId() != null) {
+          oldTip = or.rw.parseCommit(update.getOldObjectId());
+        } else if (Objects.equals(or.repo.getFullBranch(), name.get())) {
+          oldTip = null;
+          update.setExpectedOldObjectId(ObjectId.zeroId());
+        } else {
+          throw new IntegrationException("The destination branch "
+              + name + " does not exist anymore.");
+        }
+      } catch (IOException e) {
+        throw new IntegrationException("Cannot open branch " + name, e);
+      }
+    }
+  }
+
+
+  private final Map<Project.NameKey, OpenRepo> openRepos;
+  private final BatchUpdate.Factory batchUpdateFactory;
+  private final GitRepositoryManager repoManager;
+  private final ProjectCache projectCache;
+
+  private ReviewDb db;
+  private Timestamp ts;
+  private IdentifiedUser caller;
+
+  @Inject
+  MergeOpRepoManager(
+      GitRepositoryManager repoManager,
+      ProjectCache projectCache,
+      BatchUpdate.Factory batchUpdateFactory) {
+    this.repoManager = repoManager;
+    this.projectCache = projectCache;
+    this.batchUpdateFactory = batchUpdateFactory;
+
+    openRepos = new HashMap<>();
+  }
+
+  void setContext(ReviewDb db, Timestamp ts, IdentifiedUser caller) {
+    this.db = db;
+    this.ts = ts;
+    this.caller = caller;
+  }
+
+  public OpenRepo getRepo(Project.NameKey project) {
+    OpenRepo or = openRepos.get(project);
+    checkState(or != null, "repo not yet opened: %s", project);
+    return or;
+  }
+
+  public void openRepo(Project.NameKey project, boolean abortIfOpen)
+      throws NoSuchProjectException, IOException {
+    if (abortIfOpen) {
+      checkState(!openRepos.containsKey(project),
+          "repo already opened: %s", project);
+    } else {
+      if (openRepos.containsKey(project)) {
+        return;
+      }
+    }
+    ProjectState projectState = projectCache.get(project);
+    if (projectState == null) {
+      throw new NoSuchProjectException(project);
+    }
+    try {
+      OpenRepo or =
+          new OpenRepo(repoManager.openRepository(project), projectState);
+      openRepos.put(project, or);
+    } catch (RepositoryNotFoundException e) {
+      throw new NoSuchProjectException(project);
+    }
+  }
+
+  @Override
+  public void close() {
+    for (OpenRepo repo : openRepos.values()) {
+      repo.close();
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeTip.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeTip.java
index 7be7014..5ea0c02 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeTip.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeTip.java
@@ -17,12 +17,12 @@
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 
-import com.google.common.collect.Maps;
 import com.google.gerrit.common.Nullable;
 
 import org.eclipse.jgit.lib.ObjectId;
 
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.Map;
 
 /**
@@ -50,7 +50,7 @@
     checkArgument(!toMerge.isEmpty(), "toMerge may not be empty");
     this.initialTip = initialTip;
     this.branchTip = initialTip;
-    this.mergeResults = Maps.newHashMap();
+    this.mergeResults = new HashMap<>();
     // Assume fast-forward merge until opposite is proven.
     for (CodeReviewCommit commit : toMerge) {
       mergeResults.put(commit.copy(), commit.copy());
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 4a2eee4..e832483 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
@@ -106,7 +106,7 @@
         : MergeStrategy.RESOLVE;
   }
 
-  public static interface Factory {
+  public interface Factory {
     MergeUtil create(ProjectState project);
     MergeUtil create(ProjectState project, boolean useContentMerge);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManager.java
index dc72665..dffcf30 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManager.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.RepositoryConfig;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.lib.Config;
@@ -48,9 +47,8 @@
   @Inject
   MultiBaseLocalDiskRepositoryManager(SitePaths site,
       @GerritServerConfig Config cfg,
-      NotesMigration notesMigration,
       RepositoryConfig config) {
-    super(site, cfg, notesMigration);
+    super(site, cfg);
     this.config = config;
 
     for (Path alternateBasePath : config.getAllBasePaths()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/NotifyConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/NotifyConfig.java
index abc53f4..d8ed075 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/NotifyConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/NotifyConfig.java
@@ -15,16 +15,16 @@
 package com.google.gerrit.server.git;
 
 import com.google.common.base.Strings;
-import com.google.common.collect.Sets;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
 import com.google.gerrit.server.mail.Address;
 
 import java.util.EnumSet;
+import java.util.HashSet;
 import java.util.Set;
 
 public class NotifyConfig implements Comparable<NotifyConfig> {
-  public static enum Header {
+  public enum Header {
     TO, CC, BCC
   }
 
@@ -33,8 +33,8 @@
   private String filter;
 
   private Header header;
-  private Set<GroupReference> groups = Sets.newHashSet();
-  private Set<Address> addresses = Sets.newHashSet();
+  private Set<GroupReference> groups = new HashSet<>();
+  private Set<Address> addresses = new HashSet<>();
 
   public String getName() {
     return name;
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 6087432..91bc428 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
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.git;
 
-import com.google.common.collect.Maps;
 import com.google.gerrit.server.config.RequestScopedReviewDbProvider;
 import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
@@ -25,6 +24,7 @@
 import com.google.inject.Provider;
 import com.google.inject.Scope;
 
+import java.util.HashMap;
 import java.util.Map;
 import java.util.concurrent.Callable;
 
@@ -37,7 +37,7 @@
     private final Map<Key<?>, Object> map;
 
     private Context() {
-      map = Maps.newHashMap();
+      map = new HashMap<>();
     }
 
     private <T> T get(Key<T> key, Provider<T> creator) {
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 1edfa16..d07dea5 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
@@ -69,6 +69,7 @@
 import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
@@ -527,7 +528,7 @@
    */
   private void loadNotifySections(
       Config rc, Map<String, GroupReference> groupsByName) {
-    notifySections = Maps.newHashMap();
+    notifySections = new HashMap<>();
     for (String sectionName : rc.getSubsections(NOTIFY)) {
       NotifyConfig n = new NotifyConfig();
       n.setName(sectionName);
@@ -574,7 +575,7 @@
       Config rc, Map<String, GroupReference> groupsByName) {
     accessSections = new HashMap<>();
     for (String refName : rc.getSubsections(ACCESS)) {
-      if (RefConfigSection.isValid(refName) & isValidRegex(refName)) {
+      if (RefConfigSection.isValid(refName) && isValidRegex(refName)) {
         AccessSection as = getAccessSection(refName, true);
 
         for (String varName : rc.getStringList(ACCESS, refName, KEY_GROUP_PERMISSIONS)) {
@@ -683,7 +684,7 @@
 
   private void loadLabelSections(Config rc) {
     Map<String, String> lowerNames = Maps.newHashMapWithExpectedSize(2);
-    labelSections = Maps.newLinkedHashMap();
+    labelSections = new LinkedHashMap<>();
     for (String name : rc.getSubsections(LABEL)) {
       String lower = name.toLowerCase();
       if (lowerNames.containsKey(lower)) {
@@ -693,7 +694,7 @@
       }
       lowerNames.put(lower, name);
 
-      List<LabelValue> values = Lists.newArrayList();
+      List<LabelValue> values = new ArrayList<>();
       for (String value : rc.getStringList(LABEL, name, KEY_VALUE)) {
         try {
           values.add(parseLabelValue(value));
@@ -818,7 +819,7 @@
   }
 
   private void loadPluginSections(Config rc) {
-    pluginConfigs = Maps.newHashMap();
+    pluginConfigs = new HashMap<>();
     for (String plugin : rc.getSubsections(PLUGIN)) {
       Config pluginConfig = new Config();
       pluginConfigs.put(plugin, pluginConfig);
@@ -973,7 +974,7 @@
   private void saveNotifySections(
       Config rc, Set<AccountGroup.UUID> keepGroups) {
     for (NotifyConfig nc : sort(notifySections.values())) {
-      List<String> email = Lists.newArrayList();
+      List<String> email = new ArrayList<>();
       for (GroupReference gr : nc.getGroups()) {
         if (gr.getUUID() != null) {
           keepGroups.add(gr.getUUID());
@@ -982,7 +983,7 @@
       }
       Collections.sort(email);
 
-      List<String> addrs = Lists.newArrayList();
+      List<String> addrs = new ArrayList<>();
       for (Address addr : nc.getAddresses()) {
         addrs.add(addr.toString());
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectRunnable.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectRunnable.java
index 32f157d..7032878 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectRunnable.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectRunnable.java
@@ -22,4 +22,4 @@
   String getRemoteName();
 
   boolean hasCustomizedPrint();
-}
\ No newline at end of file
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/QueueProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/QueueProvider.java
index cd37c9e..06b87f2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/QueueProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/QueueProvider.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.git;
 
 public interface QueueProvider {
-  public static enum QueueType {
+  enum QueueType {
     INTERACTIVE, BATCH
   }
 
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 c81ff83..82826d9 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
@@ -63,6 +63,7 @@
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.extensions.api.changes.HashtagsInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput.NotifyHandling;
+import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.registration.DynamicMap.Entry;
 import com.google.gerrit.extensions.registration.DynamicSet;
@@ -193,6 +194,11 @@
       "Please read the documentation and contact an administrator\n"
           + "if you feel the configuration is incorrect";
 
+  private static final String SAME_CHANGE_ID_IN_MULTIPLE_CHANGES =
+      "same Change-Id in multiple changes.\n"
+          + "Squash the commits with the same Change-Id or "
+          + "ensure Change-Ids are unique for each commit";
+
   private enum Error {
         CONFIG_UPDATE("You are not allowed to perform this operation.\n"
         + "Configuration changes can only be pushed by project owners\n"
@@ -335,6 +341,7 @@
   private final Provider<SubmoduleOp> subOpProvider;
   private final Provider<Submit> submitProvider;
   private final Provider<MergeOp> mergeOpProvider;
+  private final Provider<MergeOpRepoManager> ormProvider;
   private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
   private final NotesMigration notesMigration;
   private final ChangeEditUtil editUtil;
@@ -349,48 +356,49 @@
   private BatchRefUpdate batch;
 
   @Inject
-  ReceiveCommits(final ReviewDb db,
-      final Sequences seq,
-      final Provider<InternalChangeQuery> queryProvider,
-      final SchemaFactory<ReviewDb> schemaFactory,
-      final ChangeNotes.Factory notesFactory,
-      final AccountResolver accountResolver,
-      final CmdLineParser.Factory optionParserFactory,
-      final MergedSender.Factory mergedSenderFactory,
-      final GitReferenceUpdated gitRefUpdated,
-      final PatchSetInfoFactory patchSetInfoFactory,
-      final ChangeHooks hooks,
-      final ChangeMessagesUtil cmUtil,
-      final PatchSetUtil psUtil,
-      final ProjectCache projectCache,
-      final GitRepositoryManager repoManager,
-      final TagCache tagCache,
-      final AccountCache accountCache,
-      final ChangeCache changeCache,
-      final ChangesCollection changes,
-      final ChangeInserter.Factory changeInserterFactory,
-      final CommitValidators.Factory commitValidatorsFactory,
-      @CanonicalWebUrl final String canonicalWebUrl,
-      @SendEmailExecutor final ExecutorService sendEmailExecutor,
+  ReceiveCommits(ReviewDb db,
+      Sequences seq,
+      Provider<InternalChangeQuery> queryProvider,
+      SchemaFactory<ReviewDb> schemaFactory,
+      ChangeNotes.Factory notesFactory,
+      AccountResolver accountResolver,
+      CmdLineParser.Factory optionParserFactory,
+      MergedSender.Factory mergedSenderFactory,
+      GitReferenceUpdated gitRefUpdated,
+      PatchSetInfoFactory patchSetInfoFactory,
+      ChangeHooks hooks,
+      ChangeMessagesUtil cmUtil,
+      PatchSetUtil psUtil,
+      ProjectCache projectCache,
+      GitRepositoryManager repoManager,
+      TagCache tagCache,
+      AccountCache accountCache,
+      ChangeCache changeCache,
+      ChangesCollection changes,
+      ChangeInserter.Factory changeInserterFactory,
+      CommitValidators.Factory commitValidatorsFactory,
+      @CanonicalWebUrl String canonicalWebUrl,
+      @SendEmailExecutor ExecutorService sendEmailExecutor,
       @ChangeUpdateExecutor ListeningExecutorService changeUpdateExector,
-      final RequestScopePropagator requestScopePropagator,
-      final SshInfo sshInfo,
-      final AllProjectsName allProjectsName,
+      RequestScopePropagator requestScopePropagator,
+      SshInfo sshInfo,
+      AllProjectsName allProjectsName,
       ReceiveConfig receiveConfig,
       TransferConfig transferConfig,
       DynamicSet<ReceivePackInitializer> initializers,
       Provider<LazyPostReceiveHookChain> lazyPostReceive,
-      @Assisted final ProjectControl projectControl,
-      @Assisted final Repository repo,
-      final Provider<SubmoduleOp> subOpProvider,
-      final Provider<Submit> submitProvider,
-      final Provider<MergeOp> mergeOpProvider,
-      final DynamicMap<ProjectConfigEntry> pluginConfigEntries,
-      final NotesMigration notesMigration,
-      final ChangeEditUtil editUtil,
-      final BatchUpdate.Factory batchUpdateFactory,
-      final SetHashtagsOp.Factory hashtagsFactory,
-      final ReplaceOp.Factory replaceOpFactory) throws IOException {
+      @Assisted ProjectControl projectControl,
+      @Assisted Repository repo,
+      Provider<SubmoduleOp> subOpProvider,
+      Provider<Submit> submitProvider,
+      Provider<MergeOp> mergeOpProvider,
+      Provider<MergeOpRepoManager> ormProvider,
+      DynamicMap<ProjectConfigEntry> pluginConfigEntries,
+      NotesMigration notesMigration,
+      ChangeEditUtil editUtil,
+      BatchUpdate.Factory batchUpdateFactory,
+      SetHashtagsOp.Factory hashtagsFactory,
+      ReplaceOp.Factory replaceOpFactory) throws IOException {
     this.user = projectControl.getUser().asIdentifiedUser();
     this.db = db;
     this.seq = seq;
@@ -434,6 +442,7 @@
     this.subOpProvider = subOpProvider;
     this.submitProvider = submitProvider;
     this.mergeOpProvider = mergeOpProvider;
+    this.ormProvider = ormProvider;
     this.pluginConfigEntries = pluginConfigEntries;
     this.notesMigration = notesMigration;
 
@@ -468,9 +477,11 @@
     });
 
     if (!projectControl.allRefsAreVisible()) {
-      rp.setCheckReferencedObjectsAreReachable(receiveConfig.checkReferencedObjectsAreReachable);
-      rp.setAdvertiseRefsHook(new VisibleRefFilter(tagCache, changeCache, repo, projectControl, db, false));
+      rp.setCheckReferencedObjectsAreReachable(
+          receiveConfig.checkReferencedObjectsAreReachable);
     }
+    rp.setAdvertiseRefsHook(new VisibleRefFilter(tagCache, changeCache, repo,
+        projectControl, db, false));
     List<AdvertiseRefsHook> advHooks = new ArrayList<>(3);
     advHooks.add(new AdvertiseRefsHook() {
       @Override
@@ -520,7 +531,7 @@
   }
 
   /** Set a message sender for this operation. */
-  public void setMessageSender(final MessageSender ms) {
+  public void setMessageSender(MessageSender ms) {
     messageSender = ms != null ? ms : new ReceivePackMessageSender();
   }
 
@@ -570,8 +581,8 @@
     }
   }
 
-  void processCommands(final Collection<ReceiveCommand> commands,
-      final MultiProgressMonitor progress) {
+  void processCommands(Collection<ReceiveCommand> commands,
+      MultiProgressMonitor progress) {
     newProgress = progress.beginSubTask("new", UNKNOWN);
     replaceProgress = progress.beginSubTask("updated", UNKNOWN);
     closeProgress = progress.beginSubTask("closed", UNKNOWN);
@@ -620,8 +631,8 @@
       rp.sendMessage(COMMAND_REJECTION_MESSAGE_FOOTER);
     }
 
-    Set<Branch.NameKey> branches = Sets.newHashSet();
-    for (final ReceiveCommand c : commands) {
+    Set<Branch.NameKey> branches = new HashSet<>();
+    for (ReceiveCommand c : commands) {
         if (c.getResult() == OK) {
           if (c.getType() == ReceiveCommand.Type.UPDATE) { // aka fast-forward
               tagCache.updateFastForward(project.getNameKey(),
@@ -667,8 +678,9 @@
     }
     // Update superproject gitlinks if required.
     SubmoduleOp op = subOpProvider.get();
-    try {
-      op.updateSuperProjects(db, branches, "receiveID");
+    try (MergeOpRepoManager orm = ormProvider.get()) {
+      orm.setContext(db, TimeUtil.nowTs(), user);
+      op.updateSuperProjects(db, branches, "receiveID", orm);
     } catch (SubmoduleException e) {
       log.error("Can't update the superprojects", e);
     }
@@ -691,7 +703,8 @@
       addMessage("");
       addMessage("New Changes:");
       for (CreateRequest c : created) {
-        addMessage(formatChangeUrl(canonicalWebUrl, c.change, false));
+        addMessage(formatChangeUrl(canonicalWebUrl, c.change,
+            c.change.getSubject(), false));
       }
       addMessage("");
     }
@@ -716,20 +729,21 @@
       addMessage("Updated Changes:");
       boolean edit = magicBranch != null && magicBranch.edit;
       for (ReplaceRequest u : updated) {
-        addMessage(formatChangeUrl(canonicalWebUrl, u.change, edit));
+        addMessage(formatChangeUrl(canonicalWebUrl, u.change,
+            u.info.getSubject(), edit));
       }
       addMessage("");
     }
   }
 
   private static String formatChangeUrl(String url, Change change,
-      boolean edit) {
+      String subject, boolean edit) {
     StringBuilder m = new StringBuilder()
         .append("  ")
         .append(url)
         .append(change.getChangeId())
         .append(" ")
-        .append(ChangeUtil.cropSubject(change.getSubject()));
+        .append(ChangeUtil.cropSubject(subject));
     if (change.getStatus() == Change.Status.DRAFT) {
       m.append(" [DRAFT]");
     }
@@ -775,7 +789,7 @@
       return;
     }
 
-    List<String> lastCreateChangeErrors = Lists.newArrayList();
+    List<String> lastCreateChangeErrors = new ArrayList<>();
     for (CreateRequest create : newChanges) {
       if (create.cmd.getResult() == OK) {
         okToInsert++;
@@ -805,7 +819,7 @@
     }
 
     try {
-      List<CheckedFuture<?, RestApiException>> futures = Lists.newArrayList();
+      List<CheckedFuture<?, RestApiException>> futures = new ArrayList<>();
       for (ReplaceRequest replace : replaceByChange.values()) {
         if (replace.inputCommand == magicBranch.cmd) {
           futures.add(replace.insertPatchSet());
@@ -857,8 +871,8 @@
     return displayName;
   }
 
-  private void parseCommands(final Collection<ReceiveCommand> commands) {
-    for (final ReceiveCommand cmd : commands) {
+  private void parseCommands(Collection<ReceiveCommand> commands) {
+    for (ReceiveCommand cmd : commands) {
       if (cmd.getResult() != NOT_ATTEMPTED) {
         // Already rejected by the core receive process.
         //
@@ -876,11 +890,11 @@
         continue;
       }
 
-      final Matcher m = NEW_PATCHSET.matcher(cmd.getRefName());
+      Matcher m = NEW_PATCHSET.matcher(cmd.getRefName());
       if (m.matches()) {
         // The referenced change must exist and must still be open.
         //
-        final Change.Id changeId = Change.Id.parse(m.group(1));
+        Change.Id changeId = Change.Id.parse(m.group(1));
         parseReplaceCommand(cmd, changeId);
         continue;
       }
@@ -1009,7 +1023,7 @@
     }
   }
 
-  private void parseCreate(final ReceiveCommand cmd) {
+  private void parseCreate(ReceiveCommand cmd) {
     RevObject obj;
     try {
       obj = rp.getRevWalk().parseAny(cmd.getNewId());
@@ -1034,7 +1048,7 @@
     }
   }
 
-  private void parseUpdate(final ReceiveCommand cmd) {
+  private void parseUpdate(ReceiveCommand cmd) {
     RefControl ctl = projectControl.controlForRef(cmd.getRefName());
     if (ctl.canUpdate()) {
       if (isHead(cmd) && !isCommit(cmd)) {
@@ -1053,7 +1067,7 @@
     }
   }
 
-  private boolean isCommit(final ReceiveCommand cmd) {
+  private boolean isCommit(ReceiveCommand cmd) {
     RevObject obj;
     try {
       obj = rp.getRevWalk().parseAny(cmd.getNewId());
@@ -1072,7 +1086,7 @@
     }
   }
 
-  private void parseDelete(final ReceiveCommand cmd) {
+  private void parseDelete(ReceiveCommand cmd) {
     RefControl ctl = projectControl.controlForRef(cmd.getRefName());
     if (ctl.getRefName().startsWith(REFS_CHANGES)) {
       errors.put(Error.DELETE_CHANGES, ctl.getRefName());
@@ -1089,7 +1103,7 @@
     }
   }
 
-  private void parseRewind(final ReceiveCommand cmd) {
+  private void parseRewind(ReceiveCommand cmd) {
     RevCommit newObject;
     try {
       newObject = rp.getRevWalk().parseCommit(cmd.getNewId());
@@ -1173,7 +1187,7 @@
 
     @Option(name = "--label", aliases = {"-l"}, metaVar = "LABEL+VALUE",
         usage = "label(s) to assign (defaults to +1 if no value provided")
-    void addLabel(final String token) throws CmdLineException {
+    void addLabel(String token) throws CmdLineException {
       LabelVote v = LabelVote.parse(token);
       try {
         LabelType.checkName(v.label());
@@ -1194,7 +1208,7 @@
     @Option(name = "--hashtag", aliases = {"-t"}, metaVar = "HASHTAG",
         usage = "add hashtag to changes")
     void addHashtag(String token) throws CmdLineException {
-      if (!notesMigration.enabled()) {
+      if (!notesMigration.readChanges()) {
         throw clp.reject("cannot add hashtags; noteDb is disabled");
       }
       String hashtag = cleanupHashtag(token);
@@ -1204,7 +1218,6 @@
       //TODO(dpursehouse): validate hashtags
     }
 
-    @Inject
     MagicBranchInput(ReceiveCommand cmd, LabelTypes labelTypes,
         NotesMigration notesMigration) {
       this.cmd = cmd;
@@ -1259,7 +1272,7 @@
     }
   }
 
-  private void parseMagicBranch(final ReceiveCommand cmd) {
+  private void parseMagicBranch(ReceiveCommand cmd) {
     // Permit exactly one new change request per push.
     if (magicBranch != null) {
       reject(cmd, "duplicate request");
@@ -1408,8 +1421,8 @@
         // is "connected" to the branch.
         return;
       }
-      final RevCommit h = walk.parseCommit(targetRef.getObjectId());
-      final RevFilter oldRevFilter = walk.getRevFilter();
+      RevCommit h = walk.parseCommit(targetRef.getObjectId());
+      RevFilter oldRevFilter = walk.getRevFilter();
       try {
         walk.reset();
         walk.setRevFilter(RevFilter.MERGE_BASE);
@@ -1437,14 +1450,13 @@
     }
   }
 
-  private void parseReplaceCommand(final ReceiveCommand cmd,
-      final Change.Id changeId) {
+  private void parseReplaceCommand(ReceiveCommand cmd, Change.Id changeId) {
     if (cmd.getType() != ReceiveCommand.Type.CREATE) {
       reject(cmd, "invalid usage");
       return;
     }
 
-    final RevCommit newCommit;
+    RevCommit newCommit;
     try {
       newCommit = rp.getRevWalk().parseCommit(cmd.getNewId());
     } catch (IOException e) {
@@ -1453,7 +1465,7 @@
       return;
     }
 
-    final Change changeEnt;
+    Change changeEnt;
     try {
       changeEnt = notesFactory.createChecked(db, project.getNameKey(), changeId)
           .getChange();
@@ -1474,15 +1486,14 @@
     requestReplace(cmd, true, changeEnt, newCommit);
   }
 
-  private boolean requestReplace(final ReceiveCommand cmd,
-      final boolean checkMergedInto, final Change change,
-      final RevCommit newCommit) {
+  private boolean requestReplace(ReceiveCommand cmd, boolean checkMergedInto,
+      Change change, RevCommit newCommit) {
     if (change.getStatus().isClosed()) {
       reject(cmd, "change " + canonicalWebUrl + change.getId() + " closed");
       return false;
     }
 
-    final ReplaceRequest req =
+    ReplaceRequest req =
         new ReplaceRequest(change.getId(), newCommit, cmd, checkMergedInto);
     if (replaceByChange.containsKey(req.ontoChange)) {
       reject(cmd, "duplicate request");
@@ -1493,7 +1504,7 @@
   }
 
   private void selectNewAndReplacedChangesFromMagicBranch() {
-    newChanges = Lists.newArrayList();
+    newChanges = new ArrayList<>();
 
     SetMultimap<ObjectId, Ref> existing = changeRefsById();
     GroupCollector groupCollector = GroupCollector.create(refsById, db, psUtil,
@@ -1520,12 +1531,12 @@
             magicBranch.ctl != null ? magicBranch.ctl.getRefName() : null);
       }
 
-      List<ChangeLookup> pending = Lists.newArrayList();
-      final Set<Change.Key> newChangeIds = new HashSet<>();
-      final int maxBatchChanges =
+      List<ChangeLookup> pending = new ArrayList<>();
+      Set<Change.Key> newChangeIds = new HashSet<>();
+      int maxBatchChanges =
           receiveConfig.getEffectiveMaxBatchChangesLimit(user);
       for (;;) {
-        final RevCommit c = rp.getRevWalk().next();
+        RevCommit c = rp.getRevWalk().next();
         if (c == null) {
           break;
         }
@@ -1563,13 +1574,13 @@
             + "to override please set the base manually");
         }
 
-        final List<String> idList = c.getFooterLines(CHANGE_ID);
+        List<String> idList = c.getFooterLines(CHANGE_ID);
         if (idList.isEmpty()) {
           newChanges.add(new CreateRequest(c, magicBranch.dest.get()));
           continue;
         }
 
-        final String idStr = idList.get(idList.size() - 1).trim();
+        String idStr = idList.get(idList.size() - 1).trim();
         if (idStr.matches("^I00*$")) {
           // Reject this invalid line from EGit.
           reject(magicBranch.cmd, "invalid Change-Id");
@@ -1590,7 +1601,7 @@
 
       for (ChangeLookup p : pending) {
         if (newChangeIds.contains(p.changeKey)) {
-          reject(magicBranch.cmd, "squash commits first");
+          reject(magicBranch.cmd, SAME_CHANGE_ID_IN_MULTIPLE_CHANGES);
           newChanges = Collections.emptyList();
           return;
         }
@@ -1748,9 +1759,9 @@
       RevCommit commit = state.rw.parseCommit(commitId);
       state.rw.parseBody(commit);
       final PatchSet.Id psId = ins.setGroups(groups).getPatchSetId();
-      final Account.Id me = user.getAccountId();
-      final List<FooterLine> footerLines = commit.getFooterLines();
-      final MailRecipients recipients = new MailRecipients();
+      Account.Id me = user.getAccountId();
+      List<FooterLine> footerLines = commit.getFooterLines();
+      MailRecipients recipients = new MailRecipients();
       Map<String, Short> approvals = new HashMap<>();
       checkNotNull(magicBranch);
       recipients.add(magicBranch.getMailRecipients());
@@ -1807,7 +1818,7 @@
     RevisionResource rsrc = new RevisionResource(changes.parse(changeCtl), ps);
     try (MergeOp op = mergeOpProvider.get()) {
       op.merge(db, rsrc.getChange(),
-          changeCtl.getUser().asIdentifiedUser(), false, null);
+          changeCtl.getUser().asIdentifiedUser(), false, new SubmitInput());
     }
     addMessage("");
     Change c = notesFactory
@@ -1918,8 +1929,8 @@
     private PatchSet.Id priorPatchSet;
     List<String> groups = ImmutableList.of();
 
-    ReplaceRequest(final Change.Id toChange, final RevCommit newCommit,
-        final ReceiveCommand cmd, final boolean checkMergedInto) {
+    ReplaceRequest(Change.Id toChange, RevCommit newCommit, ReceiveCommand cmd,
+        boolean checkMergedInto) {
       this.ontoChange = toChange;
       this.newCommitId = newCommit.copy();
       this.inputCommand = cmd;
@@ -1978,7 +1989,7 @@
         return false;
       }
 
-      for (final Ref r : rp.getRepository().getRefDatabase()
+      for (Ref r : rp.getRepository().getRefDatabase()
           .getRefs("refs/changes").values()) {
         if (r.getObjectId().equals(newCommit)) {
           reject(inputCommand, "commit already exists (in the project)");
@@ -1991,7 +2002,7 @@
         // very common error due to users making a new commit rather than
         // amending when trying to address review comments.
         if (rp.getRevWalk().isMergedInto(prior, newCommit)) {
-          reject(inputCommand, "squash commits first");
+          reject(inputCommand, SAME_CHANGE_ID_IN_MULTIPLE_CHANGES);
           return false;
         }
       }
@@ -2006,11 +2017,11 @@
       // or no parents were updated (rebase), else warn that only part
       // of the commit was modified.
       if (newCommit.getTree().equals(priorCommit.getTree())) {
-        final boolean messageEq =
+        boolean messageEq =
             eq(newCommit.getFullMessage(), priorCommit.getFullMessage());
-        final boolean parentsEq = parentsEqual(newCommit, priorCommit);
-        final boolean authorEq = authorEqual(newCommit, priorCommit);
-        final ObjectReader reader = rp.getRevWalk().getObjectReader();
+        boolean parentsEq = parentsEqual(newCommit, priorCommit);
+        boolean authorEq = authorEqual(newCommit, priorCommit);
+        ObjectReader reader = rp.getRevWalk().getObjectReader();
 
         if (messageEq && parentsEq && authorEq && !autoClose) {
           addMessage(String.format(
@@ -2019,7 +2030,7 @@
               reader.abbreviate(newCommit).name()));
         } else {
           StringBuilder msg = new StringBuilder();
-          msg.append("(W) ");
+          msg.append("(I) ");
           msg.append(reader.abbreviate(newCommit).name());
           msg.append(":");
           msg.append(" no files changed");
@@ -2051,7 +2062,7 @@
       try {
         edit = editUtil.byChange(changeCtl);
       } catch (AuthException | IOException e) {
-        log.error("Cannt retrieve edit", e);
+        log.error("Cannot retrieve edit", e);
         return false;
       }
 
@@ -2316,7 +2327,7 @@
     }
 
     boolean defaultName = Strings.isNullOrEmpty(user.getAccount().getFullName());
-    final RevWalk walk = rp.getRevWalk();
+    RevWalk walk = rp.getRevWalk();
     walk.reset();
     walk.sort(RevSort.NONE);
     try {
@@ -2384,7 +2395,7 @@
   }
 
   private void autoCloseChanges(final ReceiveCommand cmd) {
-    final RevWalk rw = rp.getRevWalk();
+    RevWalk rw = rp.getRevWalk();
     try {
       RevCommit newTip = rw.parseCommit(cmd.getNewId());
       Branch.NameKey branch =
@@ -2396,9 +2407,9 @@
         rw.markUninteresting(rw.parseCommit(cmd.getOldId()));
       }
 
-      final SetMultimap<ObjectId, Ref> byCommit = changeRefsById();
+      SetMultimap<ObjectId, Ref> byCommit = changeRefsById();
       Map<Change.Key, Change> byKey = null;
-      final List<ReplaceRequest> toClose = new ArrayList<>();
+      List<ReplaceRequest> toClose = new ArrayList<>();
       for (RevCommit c; (c = rw.next()) != null;) {
         rw.parseBody(c);
 
@@ -2414,14 +2425,14 @@
           }
         }
 
-        for (final String changeId : c.getFooterLines(CHANGE_ID)) {
+        for (String changeId : c.getFooterLines(CHANGE_ID)) {
           if (byKey == null) {
             byKey = openChangesByBranch(branch);
           }
 
-          final Change onto = byKey.get(new Change.Key(changeId.trim()));
+          Change onto = byKey.get(new Change.Key(changeId.trim()));
           if (onto != null) {
-            final ReplaceRequest req =
+           ReplaceRequest req =
                 new ReplaceRequest(onto.getId(), c, cmd, false);
             req.change = onto;
             toClose.add(req);
@@ -2430,8 +2441,8 @@
         }
       }
 
-      for (final ReplaceRequest req : toClose) {
-        final PatchSet.Id psi = req.validate(true)
+      for (ReplaceRequest req : toClose) {
+        PatchSet.Id psi = req.validate(true)
             ? req.insertPatchSet().checkedGet()
             : null;
         if (psi != null) {
@@ -2488,7 +2499,7 @@
 
   private Map<Change.Key, Change> openChangesByBranch(Branch.NameKey branch)
       throws OrmException {
-    final Map<Change.Key, Change> r = new HashMap<>();
+    Map<Change.Key, Change> r = new HashMap<>();
     for (ChangeData cd : queryProvider.get().byBranchOpen(branch)) {
       r.put(cd.change().getKey(), cd.change());
     }
@@ -2507,7 +2518,6 @@
           if (change.getStatus().isOpen()) {
             change.setCurrentPatchSet(info);
             change.setStatus(Change.Status.MERGED);
-            ctx.saveChange();
 
             // we cannot reconstruct the submit records for when this change was
             // submitted, this is why we must fix the status
@@ -2574,20 +2584,20 @@
     }));
   }
 
-  private void reject(final ReceiveCommand cmd) {
+  private void reject(ReceiveCommand cmd) {
     reject(cmd, "prohibited by Gerrit");
   }
 
-  private void reject(final ReceiveCommand cmd, final String why) {
+  private void reject(ReceiveCommand cmd, String why) {
     cmd.setResult(REJECTED_OTHER_REASON, why);
     commandProgress.update(1);
   }
 
-  private static boolean isHead(final ReceiveCommand cmd) {
+  private static boolean isHead(ReceiveCommand cmd) {
     return cmd.getRefName().startsWith(Constants.R_HEADS);
   }
 
-  private static boolean isConfig(final ReceiveCommand cmd) {
+  private static boolean isConfig(ReceiveCommand cmd) {
     return cmd.getRefName().equals(RefNames.REFS_CONFIG);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/RefCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/RefCache.java
new file mode 100644
index 0000000..562db08
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/RefCache.java
@@ -0,0 +1,44 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.base.Optional;
+
+import org.eclipse.jgit.lib.ObjectId;
+
+import java.io.IOException;
+
+/**
+ * Simple short-lived cache of individual refs read from a repo.
+ * <p>
+ * Within a single request that is known to read a small bounded number of refs,
+ * this class can be used to ensure a consistent view of one ref, and avoid
+ * multiple system calls to read refs multiple times.
+ * <p>
+ * <strong>Note:</strong> Implementations of this class are only appropriate
+ * for short-term caching, and do not support invalidation. It is also not
+ * threadsafe.
+ */
+public interface RefCache {
+  /**
+   * Get the possibly-cached value of a ref.
+   *
+   * @param refName name of the ref.
+   * @return value of the ref; absent if the ref does not exist in the repo.
+   *     Never null, and never present with a value of {@link
+   *     ObjectId#zeroId()}.
+   */
+  Optional<ObjectId> get(String refName) throws IOException;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
index e778735..3469298 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
@@ -263,7 +263,6 @@
     if (mergedIntoRef == null) {
       resetChange(ctx, msg);
     }
-    ctx.saveChange();
 
     return true;
   }
@@ -380,7 +379,7 @@
       hooks.doChangeMergedHook(change, account, newPatchSet, ctx.getDb(),
           commit.getName());
     }
-    try{
+    try {
       runHook(ctx);
     } catch (Exception e) {
       log.warn("ChangeHook.doCommentAddedHook delivery failed", e);
@@ -403,7 +402,7 @@
     List<LabelType> labels = changeControl.getLabelTypes().getLabelTypes();
     Map<String, Short> allApprovals = new HashMap<>();
     Map<String, Short> oldApprovals = new HashMap<>();
-    for (LabelType lt : labels){
+    for (LabelType lt : labels) {
       allApprovals.put(lt.getName(), (short) 0);
       oldApprovals.put(lt.getName(), null);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/RepoRefCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/RepoRefCache.java
new file mode 100644
index 0000000..1dfa51e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/RepoRefCache.java
@@ -0,0 +1,51 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.base.Optional;
+
+import org.eclipse.jgit.lib.ObjectId;
+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.HashMap;
+import java.util.Map;
+
+/** {@link RefCache} backed directly by a repository. */
+public class RepoRefCache implements RefCache {
+  private final RefDatabase refdb;
+  private final Map<String, Optional<ObjectId>> ids;
+
+  public RepoRefCache(Repository repo) {
+    this.refdb = repo.getRefDatabase();
+    this.ids = new HashMap<>();
+  }
+
+  @Override
+  public Optional<ObjectId> get(String refName) throws IOException {
+    Optional<ObjectId> id = ids.get(refName);
+    if (id != null) {
+      return id;
+    }
+    Ref ref = refdb.exactRef(refName);
+    id = ref != null
+        ? Optional.of(ref.getObjectId())
+        : Optional.<ObjectId>absent();
+    ids.put(refName, id);
+    return id;
+  }
+}
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 bd8c156..63c71a1 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
@@ -29,6 +29,8 @@
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.MergeOpRepoManager.OpenRepo;
+import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
 
@@ -67,7 +69,6 @@
 
   private final GitModules.Factory gitmodulesFactory;
   private final PersonIdent myIdent;
-  private final GitRepositoryManager repoManager;
   private final GitReferenceUpdated gitRefUpdated;
   private final ProjectCache projectCache;
   private final Set<Branch.NameKey> updatedSubscribers;
@@ -82,14 +83,12 @@
       GitModules.Factory gitmodulesFactory,
       @GerritPersonIdent PersonIdent myIdent,
       @GerritServerConfig Config cfg,
-      GitRepositoryManager repoManager,
       GitReferenceUpdated gitRefUpdated,
       ProjectCache projectCache,
       @Nullable Account account,
       ChangeHooks changeHooks) {
     this.gitmodulesFactory = gitmodulesFactory;
     this.myIdent = myIdent;
-    this.repoManager = repoManager;
     this.gitRefUpdated = gitRefUpdated;
     this.projectCache = projectCache;
     this.account = account;
@@ -99,11 +98,10 @@
     this.enableSuperProjectSubscriptions = cfg.getBoolean("submodule",
         "enableSuperProjectSubscriptions", true);
     updatedSubscribers = new HashSet<>();
-
   }
 
   public Collection<Branch.NameKey> getDestinationBranches(Branch.NameKey src,
-      SubscribeSection s) throws IOException {
+      SubscribeSection s, MergeOpRepoManager orm) throws IOException {
     Collection<Branch.NameKey> ret = new ArrayList<>();
     logDebug("Inspecting SubscribeSection " + s);
     for (RefSpec r : s.getRefSpecs()) {
@@ -111,11 +109,17 @@
       if (r.matchSource(src.get())) {
         if (r.getDestination() == null) {
           // no need to care for wildcard, as we matched already
-          try (Repository repo = repoManager.openRepository(s.getProject())) {
-            for (Ref ref : repo.getRefDatabase().getRefs(
-                RefNames.REFS_HEADS).values()) {
-              ret.add(new Branch.NameKey(s.getProject(), ref.getName()));
-            }
+          try {
+            orm.openRepo(s.getProject(), false);
+          } catch (NoSuchProjectException e) {
+            // A project listed a non existent project to be allowed
+            // to subscribe to it. Allow this for now.
+            continue;
+          }
+          OpenRepo or = orm.getRepo(s.getProject());
+          for (Ref ref : or.repo.getRefDatabase().getRefs(
+              RefNames.REFS_HEADS).values()) {
+            ret.add(new Branch.NameKey(s.getProject(), ref.getName()));
           }
         } else if (r.isWildcard()) {
           // refs/heads/*:refs/heads/*
@@ -134,26 +138,27 @@
 
   public Collection<SubmoduleSubscription>
       superProjectSubscriptionsForSubmoduleBranch(
-      Branch.NameKey branch) throws IOException {
+      Branch.NameKey branch, MergeOpRepoManager orm) throws IOException {
     logDebug("Calculating possible superprojects for " + branch);
     Collection<SubmoduleSubscription> ret = new ArrayList<>();
     Project.NameKey project = branch.getParentKey();
     ProjectConfig cfg = projectCache.get(project).getConfig();
     for (SubscribeSection s : cfg.getSubscribeSections(branch)) {
-      Collection<Branch.NameKey> branches = getDestinationBranches(branch, s);
+      Collection<Branch.NameKey> branches =
+          getDestinationBranches(branch, s, orm);
       for (Branch.NameKey targetBranch : branches) {
-        GitModules m = gitmodulesFactory.create(targetBranch, updateId);
+        GitModules m = gitmodulesFactory.create(targetBranch, updateId, orm);
         m.load();
         ret.addAll(m.subscribedTo(branch));
       }
     }
-    logDebug("Calculated superprojects for " + branch + " are "+ ret);
+    logDebug("Calculated superprojects for " + branch + " are " + ret);
     return ret;
   }
 
   protected void updateSuperProjects(ReviewDb db,
-      Collection<Branch.NameKey> updatedBranches, String updateId)
-          throws SubmoduleException {
+      Collection<Branch.NameKey> updatedBranches, String updateId,
+      MergeOpRepoManager orm) throws SubmoduleException {
     if (!enableSuperProjectSubscriptions) {
       logDebug("Updating superprojects disabled");
       return;
@@ -168,7 +173,7 @@
     try {
       for (Branch.NameKey updatedBranch : updatedBranches) {
         for (SubmoduleSubscription sub :
-          superProjectSubscriptionsForSubmoduleBranch(updatedBranch)) {
+          superProjectSubscriptionsForSubmoduleBranch(updatedBranch, orm)) {
           targets.put(sub.getSuperProject(), sub);
         }
       }
@@ -182,7 +187,7 @@
         if (!updatedSubscribers.add(dest)) {
           log.error("Possible circular subscription involving " + dest);
         } else {
-          updateGitlinks(db, dest, targets.get(dest));
+          updateGitlinks(db, dest, targets.get(dest), orm);
         }
       } catch (SubmoduleException e) {
         log.warn("Cannot update gitlinks for " + dest, e);
@@ -198,78 +203,92 @@
    * @throws SubmoduleException
    */
   private void updateGitlinks(ReviewDb db, Branch.NameKey subscriber,
-      Collection<SubmoduleSubscription> updates) throws SubmoduleException {
+      Collection<SubmoduleSubscription> updates, MergeOpRepoManager orm)
+          throws SubmoduleException {
     PersonIdent author = null;
     StringBuilder msgbuf = new StringBuilder("Update git submodules\n\n");
     boolean sameAuthorForAll = true;
 
-    try (Repository pdb = repoManager.openRepository(subscriber.getParentKey())) {
-      if (pdb.exactRef(subscriber.get()) == null) {
+    try {
+      orm.openRepo(subscriber.getParentKey(), false);
+    } catch (NoSuchProjectException | IOException e) {
+      throw new SubmoduleException("Cannot access superproject", e);
+    }
+    OpenRepo or = orm.getRepo(subscriber.getParentKey());
+    try {
+      Ref r = or.repo.exactRef(subscriber.get());
+      if (r == null) {
         throw new SubmoduleException(
             "The branch was probably deleted from the subscriber repository");
       }
 
-      DirCache dc = readTree(pdb, pdb.exactRef(subscriber.get()));
+      DirCache dc = readTree(r, or.rw);
       DirCacheEditor ed = dc.editor();
 
       for (SubmoduleSubscription s : updates) {
-        try (Repository subrepo = repoManager.openRepository(
-            s.getSubmodule().getParentKey());
-            RevWalk rw = CodeReviewCommit.newRevWalk(subrepo)) {
-          Ref ref = subrepo.getRefDatabase().exactRef(s.getSubmodule().get());
-          if (ref == null) {
-            ed.add(new DeletePath(s.getPath()));
+        try {
+          orm.openRepo(s.getSubmodule().getParentKey(), false);
+        } catch (NoSuchProjectException | IOException e) {
+          throw new SubmoduleException("Cannot access submodule", e);
+        }
+        OpenRepo subOr = orm.getRepo(s.getSubmodule().getParentKey());
+        Repository subrepo = subOr.repo;
+
+        Ref ref = subrepo.getRefDatabase().exactRef(s.getSubmodule().get());
+        if (ref == null) {
+          ed.add(new DeletePath(s.getPath()));
+          continue;
+        }
+
+        final ObjectId updateTo = ref.getObjectId();
+        RevCommit newCommit = subOr.rw.parseCommit(updateTo);
+
+        subOr.rw.parseBody(newCommit);
+        if (author == null) {
+          author = newCommit.getAuthorIdent();
+        } else if (!author.equals(newCommit.getAuthorIdent())) {
+          sameAuthorForAll = false;
+        }
+
+        DirCacheEntry dce = dc.getEntry(s.getPath());
+        ObjectId oldId;
+        if (dce != null) {
+          if (!dce.getFileMode().equals(FileMode.GITLINK)) {
+            log.error("Requested to update gitlink " + s.getPath() + " in "
+                + s.getSubmodule().getParentKey().get() + " but entry "
+                + "doesn't have gitlink file mode.");
             continue;
           }
+          oldId = dce.getObjectId();
+        } else {
+          // This submodule did not exist before. We do not want to add
+          // the full submodule history to the commit message, so omit it.
+          oldId = updateTo;
+        }
 
-          final ObjectId updateTo = ref.getObjectId();
-          RevCommit newCommit = rw.parseCommit(updateTo);
-
-          if (author == null) {
-            author = newCommit.getAuthorIdent();
-          } else if (!author.equals(newCommit.getAuthorIdent())) {
-            sameAuthorForAll = false;
+        ed.add(new PathEdit(s.getPath()) {
+          @Override
+          public void apply(DirCacheEntry ent) {
+            ent.setFileMode(FileMode.GITLINK);
+            ent.setObjectId(updateTo);
           }
+        });
+        if (verboseSuperProject) {
+          msgbuf.append("Project: " + s.getSubmodule().getParentKey().get());
+          msgbuf.append(" " + s.getSubmodule().getShortName());
+          msgbuf.append(" " + updateTo.getName());
+          msgbuf.append("\n\n");
 
-          DirCacheEntry dce = dc.getEntry(s.getPath());
-          ObjectId oldId;
-          if (dce != null) {
-            if (!dce.getFileMode().equals(FileMode.GITLINK)) {
-              log.error("Requested to update gitlink " + s.getPath() + " in "
-                  + s.getSubmodule().getParentKey().get() + " but entry "
-                  + "doesn't have gitlink file mode.");
-              continue;
+          try {
+            subOr.rw.markStart(newCommit);
+            subOr.rw.markUninteresting(subOr.rw.parseCommit(oldId));
+            for (RevCommit c : subOr.rw) {
+              subOr.rw.parseBody(c);
+              msgbuf.append(c.getFullMessage() + "\n\n");
             }
-            oldId = dce.getObjectId();
-          } else {
-            // This submodule did not exist before. We do not want to add
-            // the full submodule history to the commit message, so omit it.
-            oldId = updateTo;
-          }
-
-          ed.add(new PathEdit(s.getPath()) {
-            @Override
-            public void apply(DirCacheEntry ent) {
-              ent.setFileMode(FileMode.GITLINK);
-              ent.setObjectId(updateTo);
-            }
-          });
-          if (verboseSuperProject) {
-            msgbuf.append("Project: " + s.getSubmodule().getParentKey().get());
-            msgbuf.append(" " + s.getSubmodule().getShortName());
-            msgbuf.append(" " + updateTo.getName());
-            msgbuf.append("\n\n");
-
-            try {
-              rw.markStart(newCommit);
-              rw.markUninteresting(rw.parseCommit(oldId));
-              for (RevCommit c : rw) {
-                msgbuf.append(c.getFullMessage() + "\n\n");
-              }
-            } catch (IOException e) {
-              throw new SubmoduleException("Could not perform a revwalk to "
-                  + "create superproject commit message", e);
-            }
+          } catch (IOException e) {
+            throw new SubmoduleException("Could not perform a revwalk to "
+                + "create superproject commit message", e);
           }
         }
       }
@@ -279,11 +298,11 @@
         author = myIdent;
       }
 
-      ObjectInserter oi = pdb.newObjectInserter();
+      ObjectInserter oi = or.repo.newObjectInserter();
       ObjectId tree = dc.writeTree(oi);
 
       ObjectId currentCommitId =
-          pdb.exactRef(subscriber.get()).getObjectId();
+          or.repo.exactRef(subscriber.get()).getObjectId();
 
       CommitBuilder commit = new CommitBuilder();
       commit.setTreeId(tree);
@@ -296,7 +315,7 @@
 
       ObjectId commitId = oi.idFor(Constants.OBJ_COMMIT, commit.build());
 
-      final RefUpdate rfu = pdb.updateRef(subscriber.get());
+      final RefUpdate rfu = or.repo.updateRef(subscriber.get());
       rfu.setForceUpdate(false);
       rfu.setNewObjectId(commitId);
       rfu.setExpectedOldObjectId(currentCommitId);
@@ -322,24 +341,23 @@
           throw new IOException(rfu.getResult().name());
       }
       // Recursive call: update subscribers of the subscriber
-      updateSuperProjects(db, Sets.newHashSet(subscriber), updateId);
+      updateSuperProjects(db, Sets.newHashSet(subscriber), updateId, orm);
     } catch (IOException e) {
       throw new SubmoduleException("Cannot update gitlinks for "
           + subscriber.get(), e);
     }
   }
 
-  private static DirCache readTree(final Repository pdb, final Ref branch)
-      throws MissingObjectException, IncorrectObjectTypeException, IOException {
-    try (RevWalk rw = new RevWalk(pdb)) {
-      final DirCache dc = DirCache.newInCore();
-      final DirCacheBuilder b = dc.builder();
-      b.addTree(new byte[0], // no prefix path
-          DirCacheEntry.STAGE_0, // standard stage
-          pdb.newObjectReader(), rw.parseTree(branch.getObjectId()));
-      b.finish();
-      return dc;
-    }
+  private static DirCache readTree(final Ref branch, RevWalk rw)
+      throws MissingObjectException, IncorrectObjectTypeException,
+      IOException {
+    final DirCache dc = DirCache.newInCore();
+    final DirCacheBuilder b = dc.builder();
+    b.addTree(new byte[0], // no prefix path
+        DirCacheEntry.STAGE_0, // standard stage
+        rw.getObjectReader(), rw.parseTree(branch.getObjectId()));
+    b.finish();
+    return dc;
   }
 
   private void logDebug(String msg, Object... args) {
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 53389d2..ecba568 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
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.git;
 
 import com.google.common.base.MoreObjects;
-import com.google.common.collect.Lists;
 
 import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.dircache.DirCacheBuilder;
@@ -50,6 +49,7 @@
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.StringReader;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
 
@@ -479,7 +479,7 @@
     TreeWalk tw = new TreeWalk(reader);
     tw.addTree(revision.getTree());
     tw.setRecursive(recursive);
-    List<PathInfo> paths = Lists.newArrayList();
+    List<PathInfo> paths = new ArrayList<>();
     while (tw.next()) {
       paths.add(new PathInfo(tw));
     }
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 954e412..6095546 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
@@ -14,7 +14,6 @@
 
 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;
@@ -129,7 +128,7 @@
   }
 
   public <T> List<T> getTaskInfos(TaskInfoFactory<T> factory) {
-    List<T> taskInfos = Lists.newArrayList();
+    List<T> taskInfos = new ArrayList<>();
     for (Executor exe : queues) {
       for (Task<?> task : exe.getTasks()) {
         taskInfos.add(factory.getTaskInfo(task));
@@ -262,7 +261,7 @@
      * <li>{@link #DONE}: finished executing, if not periodic.</li>
      * </ol>
      */
-    public static enum State {
+    public enum State {
       // Ordered like this so ordinal matches the order we would
       // prefer to see tasks sorted in: done before running,
       // running before ready, ready before sleeping.
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 9a9e0b9..91d2cc7 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
@@ -160,7 +160,6 @@
           prevPs != null ? prevPs.getGroups() : ImmutableList.<String> of(),
           null);
       ctx.getChange().setCurrentPatchSet(patchSetInfo);
-      ctx.saveChange();
 
       // Don't copy approvals, as this is already taken care of by
       // SubmitStrategyOp.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CommitMergeStatus.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CommitMergeStatus.java
index 1102357..bb9d359 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CommitMergeStatus.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CommitMergeStatus.java
@@ -59,7 +59,7 @@
 
   private String message;
 
-  private CommitMergeStatus(String message) {
+  CommitMergeStatus(String message) {
     this.message = message;
   }
 
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 63f5963..60662fe 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
@@ -18,6 +18,7 @@
 
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.extensions.api.changes.ReviewInput.NotifyHandling;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.reviewdb.client.Branch;
@@ -89,7 +90,8 @@
           RevFlag canMergeFlag,
           ReviewDb db,
           Set<RevCommit> alreadyAccepted,
-          String submissionId);
+          String submissionId,
+          NotifyHandling notifyHandling);
     }
 
     final AccountCache accountCache;
@@ -120,6 +122,7 @@
     final Set<RevCommit> alreadyAccepted;
     final String submissionId;
     final SubmitType submitType;
+    final NotifyHandling notifyHandling;
 
     final ProjectState project;
     final MergeSorter mergeSorter;
@@ -154,7 +157,8 @@
         @Assisted ReviewDb db,
         @Assisted Set<RevCommit> alreadyAccepted,
         @Assisted String submissionId,
-        @Assisted SubmitType submitType) {
+        @Assisted SubmitType submitType,
+        @Assisted NotifyHandling notifyHandling) {
       this.accountCache = accountCache;
       this.approvalsUtil = approvalsUtil;
       this.batchUpdateFactory = batchUpdateFactory;
@@ -183,6 +187,7 @@
       this.alreadyAccepted = alreadyAccepted;
       this.submissionId = submissionId;
       this.submitType = submitType;
+      this.notifyHandling = notifyHandling;
 
       this.project = checkNotNull(projectCache.get(destBranch.getParentKey()),
             "project not found: %s", destBranch.getParentKey());
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 e9e060c..1e3fdbe 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,6 +14,7 @@
 
 package com.google.gerrit.server.git.strategy;
 
+import com.google.gerrit.extensions.api.changes.ReviewInput.NotifyHandling;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -51,11 +52,11 @@
       Repository repo, CodeReviewRevWalk rw, ObjectInserter inserter,
       RevFlag canMergeFlag, Set<RevCommit> alreadyAccepted,
       Branch.NameKey destBranch, IdentifiedUser caller, MergeTip mergeTip,
-      CommitStatus commits, String submissionId)
+      CommitStatus commits, String submissionId, NotifyHandling notifyHandling)
       throws IntegrationException {
     SubmitStrategy.Arguments args = argsFactory.create(submitType, destBranch,
         commits, rw, caller, mergeTip, inserter, repo, canMergeFlag, db,
-        alreadyAccepted, submissionId);
+        alreadyAccepted, submissionId, notifyHandling);
     switch (submitType) {
       case CHERRY_PICK:
         return new CherryPick(args);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java
index 8f15c44..01ae0b8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java
@@ -21,7 +21,6 @@
 
 import com.google.common.base.Function;
 import com.google.common.collect.Iterables;
-import com.google.common.collect.Maps;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
@@ -63,6 +62,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;
 import java.util.Objects;
@@ -327,7 +327,7 @@
   private LabelNormalizer.Result approve(ChangeContext ctx, ChangeUpdate update)
       throws OrmException {
     PatchSet.Id psId = update.getPatchSetId();
-    Map<PatchSetApproval.Key, PatchSetApproval> byKey = Maps.newHashMap();
+    Map<PatchSetApproval.Key, PatchSetApproval> byKey = new HashMap<>();
     for (PatchSetApproval psa : args.approvalsUtil.byPatchSet(
         ctx.getDb(), ctx.getControl(), psId)) {
       byKey.put(psa.getKey(), psa);
@@ -484,7 +484,6 @@
     logDebug("Setting change {} merged", c.getId());
     c.setStatus(Change.Status.MERGED);
     c.setSubmissionId(args.submissionId);
-    ctx.saveChange();
 
     // TODO(dborowitz): We need to be able to change the author of the message,
     // which is not the user from the update context. addMergedMessage was able
@@ -518,7 +517,8 @@
     // have failed fast in one of the other steps.
     try {
       args.mergedSenderFactory
-          .create(ctx.getProject(), getId(), submitter.getAccountId())
+          .create(ctx.getProject(), getId(), submitter.getAccountId(),
+              args.notifyHandling)
           .sendAsync();
     } catch (Exception e) {
       log.error("Cannot email merged notification for " + getId(), e);
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 b069ca9..5e84fd3 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
@@ -66,7 +66,7 @@
   private static final Logger log = LoggerFactory
       .getLogger(CommitValidators.class);
 
-  public static enum Policy {
+  public enum Policy {
     /** Use {@link #validateForGerritCommits}. */
     GERRIT,
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java
index f54e071..4bf3076 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.git.validators;
 
-import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.registration.DynamicMap.Entry;
 import com.google.gerrit.extensions.registration.DynamicSet;
@@ -36,6 +35,7 @@
 import org.eclipse.jgit.lib.Repository;
 
 import java.io.IOException;
+import java.util.LinkedList;
 import java.util.List;
 
 public class MergeValidators {
@@ -60,7 +60,7 @@
       PatchSet.Id patchSetId,
       IdentifiedUser caller)
       throws MergeValidationException {
-    List<MergeValidationListener> validators = Lists.newLinkedList();
+    List<MergeValidationListener> validators = new LinkedList<>();
 
     validators.add(new PluginMergeValidationListener(mergeValidationListeners));
     validators.add(projectConfigValidatorFactory.create());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/RefOperationValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/RefOperationValidators.java
index 6fd0f5c..769c7d2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/RefOperationValidators.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/RefOperationValidators.java
@@ -15,7 +15,6 @@
 
 import com.google.common.base.Predicate;
 import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.IdentifiedUser;
@@ -29,6 +28,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.util.ArrayList;
 import java.util.List;
 
 public class RefOperationValidators {
@@ -63,7 +63,7 @@
   public List<ValidationMessage> validateForRefOperation()
     throws RefOperationValidationException {
 
-    List<ValidationMessage> messages = Lists.newArrayList();
+    List<ValidationMessage> messages = new ArrayList<>();
     boolean withException = false;
     try {
       for (RefOperationValidationListener listener : refOperationValidationListeners) {
@@ -97,4 +97,4 @@
       return input.isError();
     }
   }
-}
\ No newline at end of file
+}
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
index 159496b..be264b6 100644
--- 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
@@ -27,4 +27,4 @@
   public UploadValidationException(String message) {
     super(message);
   }
-}
\ No newline at end of file
+}
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 c274a37..2deb44a 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
@@ -17,7 +17,6 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
 import com.google.gerrit.audit.AuditService;
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.extensions.common.GroupInfo;
@@ -39,6 +38,8 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import java.util.HashMap;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 
@@ -98,8 +99,8 @@
     input = Input.init(input);
 
     GroupControl control = resource.getControl();
-    Map<AccountGroup.UUID, AccountGroupById> newIncludedGroups = Maps.newHashMap();
-    List<GroupInfo> result = Lists.newLinkedList();
+    Map<AccountGroup.UUID, AccountGroupById> newIncludedGroups = new HashMap<>();
+    List<GroupInfo> result = new LinkedList<>();
     Account.Id me = control.getUser().getAccountId();
 
     for (String includedGroup : input.groups) {
@@ -166,9 +167,6 @@
 
   @Singleton
   static class UpdateIncludedGroup implements RestModifyView<IncludedGroupResource, PutIncludedGroup.Input> {
-    static class Input {
-    }
-
     private final Provider<GetIncludedGroup> get;
 
     @Inject
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 e947f2f..0039c3c 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
@@ -16,7 +16,6 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
 import com.google.gerrit.audit.AuditService;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -47,7 +46,9 @@
 import com.google.inject.Singleton;
 
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -174,7 +175,7 @@
 
   public void addMembers(AccountGroup.Id groupId,
       Collection<? extends Account.Id> newMemberIds) throws OrmException {
-    Map<Account.Id, AccountGroupMember> newAccountGroupMembers = Maps.newHashMap();
+    Map<Account.Id, AccountGroupMember> newAccountGroupMembers = new HashMap<>();
     for (Account.Id accId : newMemberIds) {
       if (!newAccountGroupMembers.containsKey(accId)) {
         AccountGroupMember.Key key =
@@ -186,12 +187,13 @@
         }
       }
     }
-
-    auditService.dispatchAddAccountsToGroup(self.get().getAccountId(),
-        newAccountGroupMembers.values());
-    db.get().accountGroupMembers().insert(newAccountGroupMembers.values());
-    for (AccountGroupMember m : newAccountGroupMembers.values()) {
-      accountCache.evict(m.getAccountId());
+    if (!newAccountGroupMembers.isEmpty()) {
+      auditService.dispatchAddAccountsToGroup(self.get().getAccountId(),
+          newAccountGroupMembers.values());
+      db.get().accountGroupMembers().insert(newAccountGroupMembers.values());
+      for (AccountGroupMember m : newAccountGroupMembers.values()) {
+        accountCache.evict(m.getAccountId());
+      }
     }
   }
 
@@ -212,7 +214,7 @@
 
   private List<AccountInfo> toAccountInfoList(Set<Account.Id> accountIds)
       throws OrmException {
-    List<AccountInfo> result = Lists.newLinkedList();
+    List<AccountInfo> result = new LinkedList<>();
     AccountLoader loader = infoFactory.create(true);
     for (Account.Id accId : accountIds) {
       result.add(loader.get(accId));
@@ -253,9 +255,6 @@
 
   @Singleton
   static class UpdateMember implements RestModifyView<MemberResource, PutMember.Input> {
-    static class Input {
-    }
-
     private final GetMember get;
 
     @Inject
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java
index f3f12a1..dbcad99 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java
@@ -56,7 +56,7 @@
 
 @RequiresCapability(GlobalCapability.CREATE_GROUP)
 public class CreateGroup implements RestModifyView<TopLevelResource, GroupInput> {
-  public static interface Factory {
+  public interface Factory {
     CreateGroup create(@Assisted String name);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/DbGroupMemberAuditListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/DbGroupMemberAuditListener.java
index bc8bff7..30b856a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/DbGroupMemberAuditListener.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/DbGroupMemberAuditListener.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.group;
 
 import com.google.common.base.Joiner;
-import com.google.common.collect.Lists;
 import com.google.gerrit.audit.GroupMemberAuditListener;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.reviewdb.client.Account;
@@ -37,6 +36,7 @@
 import java.text.MessageFormat;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.LinkedList;
 import java.util.List;
 
 class DbGroupMemberAuditListener implements GroupMemberAuditListener {
@@ -49,7 +49,7 @@
   private final UniversalGroupBackend groupBackend;
 
   @Inject
-  public DbGroupMemberAuditListener(SchemaFactory<ReviewDb> schema,
+  DbGroupMemberAuditListener(SchemaFactory<ReviewDb> schema,
       AccountCache accountCache, GroupCache groupCache,
       UniversalGroupBackend groupBackend) {
     this.schema = schema;
@@ -61,7 +61,7 @@
   @Override
   public void onAddAccountsToGroup(Account.Id me,
       Collection<AccountGroupMember> added) {
-    List<AccountGroupMemberAudit> auditInserts = Lists.newLinkedList();
+    List<AccountGroupMemberAudit> auditInserts = new LinkedList<>();
     for (AccountGroupMember m : added) {
       AccountGroupMemberAudit audit =
           new AccountGroupMemberAudit(m, me, TimeUtil.nowTs());
@@ -79,8 +79,8 @@
   @Override
   public void onDeleteAccountsFromGroup(Account.Id me,
       Collection<AccountGroupMember> removed) {
-    List<AccountGroupMemberAudit> auditInserts = Lists.newLinkedList();
-    List<AccountGroupMemberAudit> auditUpdates = Lists.newLinkedList();
+    List<AccountGroupMemberAudit> auditInserts = new LinkedList<>();
+    List<AccountGroupMemberAudit> auditUpdates = new LinkedList<>();
     try (ReviewDb db = schema.open()) {
       for (AccountGroupMember m : removed) {
         AccountGroupMemberAudit audit = null;
@@ -131,7 +131,7 @@
   @Override
   public void onDeleteGroupsFromGroup(Account.Id me,
       Collection<AccountGroupById> removed) {
-    final List<AccountGroupByIdAud> auditUpdates = Lists.newLinkedList();
+    final List<AccountGroupByIdAud> auditUpdates = new LinkedList<>();
     try (ReviewDb db = schema.open()) {
       for (final AccountGroupById g : removed) {
         AccountGroupByIdAud audit = null;
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 bde8fb7..da683a3 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
@@ -15,8 +15,6 @@
 package com.google.gerrit.server.group;
 
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
 import com.google.gerrit.audit.AuditService;
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -37,6 +35,8 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import java.util.HashMap;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 
@@ -71,7 +71,7 @@
 
     final GroupControl control = resource.getControl();
     final Map<AccountGroup.UUID, AccountGroupById> includedGroups = getIncludedGroups(internalGroup.getId());
-    final List<AccountGroupById> toRemove = Lists.newLinkedList();
+    final List<AccountGroupById> toRemove = new LinkedList<>();
 
     for (final String includedGroup : input.groups) {
       GroupDescription.Basic d = groupsCollection.parse(includedGroup);
@@ -100,8 +100,7 @@
 
   private Map<AccountGroup.UUID, AccountGroupById> getIncludedGroups(
       final AccountGroup.Id groupId) throws OrmException {
-    final Map<AccountGroup.UUID, AccountGroupById> groups =
-        Maps.newHashMap();
+    final Map<AccountGroup.UUID, AccountGroupById> groups = new HashMap<>();
     for (AccountGroupById g : db.get().accountGroupById().byGroup(groupId)) {
       groups.put(g.getIncludeUUID(), g);
     }
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 b14974b..5a759bc 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
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.server.group;
 
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
 import com.google.gerrit.audit.AuditService;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
@@ -36,6 +34,8 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import java.util.HashMap;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 
@@ -71,7 +71,7 @@
 
     final GroupControl control = resource.getControl();
     final Map<Account.Id, AccountGroupMember> members = getMembers(internalGroup.getId());
-    final List<AccountGroupMember> toRemove = Lists.newLinkedList();
+    final List<AccountGroupMember> toRemove = new LinkedList<>();
 
     for (final String nameOrEmail : input.members) {
       Account a = accounts.parse(nameOrEmail).getAccount();
@@ -102,7 +102,7 @@
 
   private Map<Account.Id, AccountGroupMember> getMembers(
       final AccountGroup.Id groupId) throws OrmException {
-    final Map<Account.Id, AccountGroupMember> members = Maps.newHashMap();
+    final Map<Account.Id, AccountGroupMember> members = new HashMap<>();
     for (final AccountGroupMember m : db.get().accountGroupMembers()
         .byGroup(groupId)) {
       members.put(m.getAccountId(), m);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupInfoCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupInfoCache.java
index b0e238a..d660db0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupInfoCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupInfoCache.java
@@ -14,67 +14,42 @@
 
 package com.google.gerrit.server.group;
 
-import com.google.gerrit.common.data.GroupInfo;
+import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.inject.Inject;
 
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 
-/** In-memory table of {@link GroupInfo}, indexed by {@code AccountGroup.Id}. */
+/** Efficiently builds a {@link GroupInfoCache}. */
 public class GroupInfoCache {
-  private static final GroupInfoCache EMPTY;
-  static {
-    EMPTY = new GroupInfoCache();
-    EMPTY.groups = Collections.emptyMap();
+  public interface Factory {
+    GroupInfoCache create();
   }
 
-  /** Obtain an empty cache singleton. */
-  public static GroupInfoCache empty() {
-    return EMPTY;
-  }
+  private final GroupBackend groupBackend;
+  private final Map<AccountGroup.UUID, GroupDescription.Basic> out;
 
-  protected Map<AccountGroup.UUID, GroupInfo> groups;
-
-  protected GroupInfoCache() {
-  }
-
-  public GroupInfoCache(final Iterable<GroupInfo> list) {
-    groups = new HashMap<>();
-    for (final GroupInfo gi : list) {
-      groups.put(gi.getId(), gi);
-    }
+  @Inject
+  GroupInfoCache(GroupBackend groupBackend) {
+    this.groupBackend = groupBackend;
+    this.out = new HashMap<>();
   }
 
   /**
-   * Lookup the group summary
-   * <p>
-   * The return value can take on one of three forms:
-   * <ul>
-   * <li>{@code null}, if {@code id == null}.</li>
-   * <li>a valid info block, if {@code id} was loaded.</li>
-   * <li>an anonymous info block, if {@code id} was not loaded.</li>
-   * </ul>
+   * Indicate a group will be needed later on.
    *
-   * @param uuid the id desired.
-   * @return info block for the group.
+   * @param uuid identity that will be needed in the future; may be null.
    */
-  public GroupInfo get(final AccountGroup.UUID uuid) {
-    if (uuid == null) {
-      return null;
+  public void want(final AccountGroup.UUID uuid) {
+    if (uuid != null && !out.containsKey(uuid)) {
+      out.put(uuid, groupBackend.get(uuid));
     }
-
-    GroupInfo r = groups.get(uuid);
-    if (r == null) {
-      r = new GroupInfo(uuid);
-      groups.put(uuid, r);
-    }
-    return r;
   }
 
-  /** Merge the information from another cache into this one. */
-  public void merge(final GroupInfoCache other) {
-    assert this != EMPTY;
-    groups.putAll(other.groups);
+  public GroupDescription.Basic get(final AccountGroup.UUID uuid) {
+    want(uuid);
+    return out.get(uuid);
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupInfoCacheFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupInfoCacheFactory.java
deleted file mode 100644
index 0abf618..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupInfoCacheFactory.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.group;
-
-import com.google.common.collect.Maps;
-import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.account.GroupBackend;
-import com.google.inject.Inject;
-
-import java.util.Map;
-
-/** Efficiently builds a {@link GroupInfoCache}. */
-public class GroupInfoCacheFactory {
-  public interface Factory {
-    GroupInfoCacheFactory create();
-  }
-
-  private final GroupBackend groupBackend;
-  private final Map<AccountGroup.UUID, GroupDescription.Basic> out;
-
-  @Inject
-  GroupInfoCacheFactory(GroupBackend groupBackend) {
-    this.groupBackend = groupBackend;
-    this.out = Maps.newHashMap();
-  }
-
-  /**
-   * Indicate a group will be needed later on.
-   *
-   * @param uuid identity that will be needed in the future; may be null.
-   */
-  public void want(final AccountGroup.UUID uuid) {
-    if (uuid != null && !out.containsKey(uuid)) {
-      out.put(uuid, groupBackend.get(uuid));
-    }
-  }
-
-  /** Indicate one or more groups will be needed later on. */
-  public void want(final Iterable<AccountGroup.UUID> uuids) {
-    for (final AccountGroup.UUID uuid : uuids) {
-      want(uuid);
-    }
-  }
-
-  public GroupDescription.Basic get(final AccountGroup.UUID uuid) {
-    want(uuid);
-    return out.get(uuid);
-  }
-}
\ No newline at end of file
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 4636c2f..6268d72 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
@@ -69,7 +69,7 @@
     final CurrentUser user = self.get();
     if (user instanceof AnonymousUser) {
       throw new AuthException("Authentication required");
-    } else if(!(user.isIdentifiedUser())) {
+    } else if (!(user.isIdentifiedUser())) {
       throw new ResourceNotFoundException();
     }
 
@@ -82,7 +82,7 @@
     final CurrentUser user = self.get();
     if (user instanceof AnonymousUser) {
       throw new AuthException("Authentication required");
-    } else if(!(user.isIdentifiedUser())) {
+    } else if (!(user.isIdentifiedUser())) {
       throw new ResourceNotFoundException(id);
     }
 
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 1b1ddbf..d23cac4 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
@@ -18,8 +18,6 @@
 import com.google.common.base.Strings;
 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.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupDescriptions;
 import com.google.gerrit.common.data.GroupReference;
@@ -49,11 +47,14 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import java.util.SortedMap;
+import java.util.TreeMap;
 
 /** List groups visible to the calling user. */
 public class ListGroups implements RestReadView<TopLevelResource> {
@@ -61,12 +62,12 @@
   protected final GroupCache groupCache;
 
   private final List<ProjectControl> projects = new ArrayList<>();
-  private final Set<AccountGroup.UUID> groupsToInspect = Sets.newHashSet();
+  private final Set<AccountGroup.UUID> groupsToInspect = new HashSet<>();
   private final GroupControl.Factory groupControlFactory;
   private final GroupControl.GenericFactory genericGroupControlFactory;
   private final Provider<IdentifiedUser> identifiedUser;
   private final IdentifiedUser.GenericFactory userFactory;
-  private final Provider<GetGroups> accountGetGroups;
+  private final GetGroups accountGetGroups;
   private final GroupJson json;
   private final GroupBackend groupBackend;
 
@@ -149,7 +150,8 @@
       final GroupControl.GenericFactory genericGroupControlFactory,
       final Provider<IdentifiedUser> identifiedUser,
       final IdentifiedUser.GenericFactory userFactory,
-      final Provider<GetGroups> accountGetGroups, GroupJson json,
+      final GetGroups accountGetGroups,
+      GroupJson json,
       GroupBackend groupBackend) {
     this.groupCache = groupCache;
     this.groupControlFactory = groupControlFactory;
@@ -176,7 +178,7 @@
   @Override
   public SortedMap<String, GroupInfo> apply(TopLevelResource resource)
       throws OrmException, BadRequestException {
-    SortedMap<String, GroupInfo> output = Maps.newTreeMap();
+    SortedMap<String, GroupInfo> output = new TreeMap<>();
     for (GroupInfo info : get()) {
       output.put(MoreObjects.firstNonNull(
           info.name,
@@ -197,7 +199,7 @@
     }
 
     if (user != null) {
-      return accountGetGroups.get().apply(
+      return accountGetGroups.apply(
           new AccountResource(userFactory.create(user)));
     }
 
@@ -208,7 +210,7 @@
     List<GroupInfo> groupInfos;
     List<AccountGroup> groupList;
     if (!projects.isEmpty()) {
-      Map<AccountGroup.UUID, AccountGroup> groups = Maps.newHashMap();
+      Map<AccountGroup.UUID, AccountGroup> groups = new HashMap<>();
       for (final ProjectControl projectControl : projects) {
         final Set<GroupReference> groupsRefs = projectControl.getAllGroups();
         for (final GroupReference groupRef : groupsRefs) {
@@ -289,7 +291,7 @@
 
   private List<GroupInfo> getGroupsOwnedBy(IdentifiedUser user)
       throws OrmException {
-    List<GroupInfo> groups = Lists.newArrayList();
+    List<GroupInfo> groups = new ArrayList<>();
     int found = 0;
     int foundIndex = 0;
     for (AccountGroup g : filterGroups(groupCache.all())) {
@@ -313,7 +315,7 @@
   }
 
   private List<AccountGroup> filterGroups(final Iterable<AccountGroup> groups) {
-    final List<AccountGroup> filteredGroups = Lists.newArrayList();
+    final List<AccountGroup> filteredGroups = new ArrayList<>();
     final boolean isAdmin =
         identifiedUser.get().getCapabilities().canAdministrateServer();
     for (final AccountGroup group : groups) {
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 8e22ef9..803c498 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
@@ -16,7 +16,6 @@
 
 import static com.google.common.base.Strings.nullToEmpty;
 
-import com.google.common.collect.Lists;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
@@ -31,6 +30,7 @@
 
 import org.slf4j.Logger;
 
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
@@ -59,7 +59,7 @@
     }
 
     boolean ownerOfParent = rsrc.getControl().isOwner();
-    List<GroupInfo> included = Lists.newArrayList();
+    List<GroupInfo> included = new ArrayList<>();
     for (AccountGroupById u : dbProvider.get()
         .accountGroupById()
         .byGroup(rsrc.toAccountGroup().getId())) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java
index d623b31..98d18ca 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.group;
 
 import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
 import com.google.gerrit.common.data.GroupDetail;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.extensions.common.AccountInfo;
@@ -35,6 +34,7 @@
 import org.kohsuke.args4j.Option;
 
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -90,7 +90,7 @@
       final HashSet<AccountGroup.UUID> seenGroups) throws OrmException {
     seenGroups.add(groupUUID);
 
-    final Map<Account.Id, AccountInfo> members = Maps.newHashMap();
+    final Map<Account.Id, AccountInfo> members = new HashMap<>();
     final AccountGroup group = groupCache.get(groupUUID);
     if (group == null) {
       // the included group is an external group and can't be resolved
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
index fe06c41..a3d3ad5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
@@ -140,11 +140,7 @@
       threads = config.getInt("index", null, "threads", 0);
     }
     if (threads <= 0) {
-      threads =
-          config.getInt("changeMerge", null, "interactiveThreadPoolSize", 0);
-    }
-    if (threads <= 0) {
-      return MoreExecutors.newDirectExecutorService();
+      threads = Runtime.getRuntime().availableProcessors() / 2 + 1;
     }
     return MoreExecutors.listeningDecorator(
         workQueue.createQueue(threads, "Index-Interactive"));
@@ -161,9 +157,6 @@
     }
     int threads = config.getInt("index", null, "batchThreads", 0);
     if (threads <= 0) {
-      threads = config.getInt("changeMerge", null, "threadPoolSize", 0);
-    }
-    if (threads <= 0) {
       threads = Runtime.getRuntime().availableProcessors();
     }
     return MoreExecutors.listeningDecorator(
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 a8ec51f..10f5ecb 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
@@ -21,6 +21,7 @@
 import com.google.common.base.Optional;
 import com.google.common.base.Predicates;
 import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.server.index.FieldDef.FillArgs;
 import com.google.gwtorm.server.OrmException;
@@ -28,10 +29,38 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
+import java.util.List;
 
 /** Specific version of a secondary index schema. */
 public class Schema<T> {
+  public static class Builder<T> {
+    private final List<FieldDef<T, ?>> fields = new ArrayList<>();
+
+    public Builder<T> add(Schema<T> schema) {
+      this.fields.addAll(schema.getFields().values());
+      return this;
+    }
+
+    @SafeVarargs
+    public final Builder<T> add(FieldDef<T, ?>... fields) {
+      this.fields.addAll(Arrays.asList(fields));
+      return this;
+    }
+
+    @SafeVarargs
+    public final Builder<T> remove(FieldDef<T, ?>... fields) {
+      this.fields.removeAll(Arrays.asList(fields));
+      return this;
+    }
+
+    public Schema<T> build() {
+      return new Schema<>(ImmutableList.copyOf(fields));
+    }
+  }
+
   private static final Logger log = LoggerFactory.getLogger(Schema.class);
 
   public static class Values<T> {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/SchemaUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/SchemaUtil.java
index 53ff0e3..ca61b00 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/SchemaUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/SchemaUtil.java
@@ -22,8 +22,6 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSortedMap;
 import com.google.common.collect.Iterables;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Sets;
 
 import org.eclipse.jgit.lib.PersonIdent;
 
@@ -32,6 +30,7 @@
 import java.lang.reflect.ParameterizedType;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
@@ -39,7 +38,7 @@
 public class SchemaUtil {
   public static <V> ImmutableSortedMap<Integer, Schema<V>> schemasFromClass(
       Class<?> schemasClass, Class<V> valueClass) {
-    Map<Integer, Schema<V>> schemas = Maps.newHashMap();
+    Map<Integer, Schema<V>> schemas = new HashMap<>();
     for (Field f : schemasClass.getDeclaredFields()) {
       if (Modifier.isStatic(f.getModifiers())
           && Modifier.isFinal(f.getModifiers())
@@ -73,6 +72,16 @@
   }
 
   @SafeVarargs
+  public static <V> Schema<V> schema(Schema<V> schema,
+      FieldDef<V, ?>... moreFields) {
+    return new Schema<>(
+        new ImmutableList.Builder<FieldDef<V, ?>>()
+            .addAll(schema.getFields().values())
+            .addAll(ImmutableList.copyOf(moreFields))
+            .build());
+  }
+
+  @SafeVarargs
   public static <V> Schema<V> schema(FieldDef<V, ?>... fields) {
     return schema(ImmutableList.copyOf(fields));
   }
@@ -90,7 +99,7 @@
       Iterable<String> emails) {
     Splitter at = Splitter.on('@');
     Splitter s = Splitter.on(CharMatcher.anyOf("@.- ")).omitEmptyStrings();
-    HashSet<String> parts = Sets.newHashSet();
+    HashSet<String> parts = new HashSet<>();
     for (String email : emails) {
       if (email == null) {
         continue;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/SiteIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/SiteIndexer.java
index 2aa2bdb..8ee1ced 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/SiteIndexer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/SiteIndexer.java
@@ -17,14 +17,23 @@
 import static com.google.common.base.Preconditions.checkNotNull;
 
 import com.google.common.base.Stopwatch;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
 
+import org.eclipse.jgit.lib.ProgressMonitor;
 import org.eclipse.jgit.util.io.NullOutputStream;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.io.OutputStream;
 import java.io.PrintWriter;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 public abstract class SiteIndexer<K, V, I extends Index<K, V>> {
+  private static final Logger log = LoggerFactory.getLogger(SiteIndexer.class);
+
   public static class Result {
     private final long elapsedNanos;
     private final boolean success;
@@ -73,4 +82,60 @@
   }
 
   public abstract Result indexAll(I index);
+
+  protected final void addErrorListener(ListenableFuture<?> future,
+      String desc, ProgressMonitor progress, AtomicBoolean ok) {
+    future.addListener(
+        new ErrorListener(future, desc, progress, ok),
+        MoreExecutors.directExecutor());
+  }
+
+  private static class ErrorListener implements Runnable {
+    private final ListenableFuture<?> future;
+    private final String desc;
+    private final ProgressMonitor progress;
+    private final AtomicBoolean ok;
+
+    private ErrorListener(ListenableFuture<?> future, String desc,
+        ProgressMonitor progress, AtomicBoolean ok) {
+      this.future = future;
+      this.desc = desc;
+      this.progress = progress;
+      this.ok = ok;
+    }
+
+    @Override
+    public void run() {
+      try {
+        future.get();
+      } catch (ExecutionException | InterruptedException e) {
+        fail(e);
+      } catch (RuntimeException e) {
+        failAndThrow(e);
+      } catch (Error e) {
+        // Can't join with RuntimeException because "RuntimeException |
+        // Error" becomes Throwable, which messes with signatures.
+        failAndThrow(e);
+      } finally {
+        synchronized (progress) {
+          progress.update(1);
+        }
+      }
+    }
+
+    private void fail(Throwable t) {
+      log.error("Failed to index " + desc, t);
+      ok.set(false);
+    }
+
+    private void failAndThrow(RuntimeException e) {
+      fail(e);
+      throw e;
+    }
+
+    private void failAndThrow(Error e) {
+      fail(e);
+      throw e;
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountField.java
index 6da7eef..8627e3a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountField.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountField.java
@@ -16,6 +16,7 @@
 
 import com.google.common.base.Function;
 import com.google.common.base.Predicates;
+import com.google.common.base.Strings;
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
@@ -128,10 +129,10 @@
 
   public static final FieldDef<AccountState, String> USERNAME =
       new FieldDef.Single<AccountState, String>(
-            "username", null, false) {
+            "username", FieldType.EXACT, false) {
         @Override
         public String get(AccountState input, FillArgs args) {
-          return input.getUserName().toLowerCase();
+          return Strings.nullToEmpty(input.getUserName()).toLowerCase();
         }
       };
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndex.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndex.java
new file mode 100644
index 0000000..cb7b3ef
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndex.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.account;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.index.Index;
+import com.google.gerrit.server.index.IndexDefinition;
+
+public interface AccountIndex extends Index<Account.Id, AccountState> {
+  public interface Factory extends
+      IndexDefinition.IndexFactory<Account.Id, AccountState, AccountIndex> {
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndexCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndexCollection.java
new file mode 100644
index 0000000..9f4cca8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndexCollection.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.account;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.index.IndexCollection;
+import com.google.inject.Singleton;
+
+@Singleton
+public class AccountIndexCollection extends
+    IndexCollection<Account.Id, AccountState, AccountIndex> {
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndexDefinition.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndexDefinition.java
new file mode 100644
index 0000000..ea16e13
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndexDefinition.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.account;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.index.IndexDefinition;
+import com.google.inject.Inject;
+
+public class AccountIndexDefinition
+    extends IndexDefinition<Account.Id, AccountState, AccountIndex> {
+
+  @Inject
+  AccountIndexDefinition(
+      AccountIndexCollection indexCollection,
+      AccountIndex.Factory indexFactory,
+      AllAccountsIndexer allAccountsIndexer) {
+    super(AccountSchemaDefinitions.INSTANCE, indexCollection, indexFactory,
+        allAccountsIndexer);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java
new file mode 100644
index 0000000..0c5af2c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.account;
+
+import static com.google.gerrit.server.index.SchemaUtil.schema;
+
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.index.Schema;
+import com.google.gerrit.server.index.SchemaDefinitions;
+
+public class AccountSchemaDefinitions extends SchemaDefinitions<AccountState> {
+  static final Schema<AccountState> V1 = schema(
+      AccountField.ID,
+      AccountField.ACTIVE,
+      AccountField.EMAIL,
+      AccountField.EXTERNAL_ID,
+      AccountField.NAME_PART,
+      AccountField.REGISTERED,
+      AccountField.USERNAME);
+
+  public static final AccountSchemaDefinitions INSTANCE =
+      new AccountSchemaDefinitions();
+
+  private AccountSchemaDefinitions() {
+    super("accounts", AccountState.class);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountSchemas.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountSchemas.java
deleted file mode 100644
index 1c94706..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountSchemas.java
+++ /dev/null
@@ -1,62 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF 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.account;
-
-import static com.google.common.base.Preconditions.checkArgument;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Iterables;
-import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.index.FieldDef;
-import com.google.gerrit.server.index.Schema;
-import com.google.gerrit.server.index.SchemaUtil;
-
-import java.util.Collection;
-
-public class AccountSchemas {
-  static final Schema<AccountState> V1 = schema(
-      AccountField.ID,
-      AccountField.ACTIVE,
-      AccountField.EMAIL,
-      AccountField.EXTERNAL_ID,
-      AccountField.NAME_PART,
-      AccountField.REGISTERED,
-      AccountField.USERNAME);
-
-  private static Schema<AccountState> schema(
-      Collection<FieldDef<AccountState, ?>> fields) {
-    return new Schema<>(ImmutableList.copyOf(fields));
-  }
-
-  @SafeVarargs
-  private static Schema<AccountState> schema(
-      FieldDef<AccountState, ?>... fields) {
-    return schema(ImmutableList.copyOf(fields));
-  }
-
-  public static final ImmutableMap<Integer, Schema<AccountState>> ALL =
-      SchemaUtil.schemasFromClass(AccountSchemas.class, AccountState.class);
-
-  public static Schema<AccountState> get(int version) {
-    Schema<AccountState> schema = ALL.get(version);
-    checkArgument(schema != null, "Unrecognized schema version: %s", version);
-    return schema;
-  }
-
-  public static Schema<AccountState> getLatest() {
-    return Iterables.getLast(ALL.values());
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AllAccountsIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AllAccountsIndexer.java
new file mode 100644
index 0000000..1c008b46
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AllAccountsIndexer.java
@@ -0,0 +1,138 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.account;
+
+import static com.google.gerrit.server.git.QueueProvider.QueueType.BATCH;
+
+import com.google.common.base.Stopwatch;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.index.IndexExecutor;
+import com.google.gerrit.server.index.SiteIndexer;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.ProgressMonitor;
+import org.eclipse.jgit.lib.TextProgressMonitor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+@Singleton
+public class AllAccountsIndexer
+    extends SiteIndexer<Account.Id, AccountState, AccountIndex> {
+  private static final Logger log =
+      LoggerFactory.getLogger(AllAccountsIndexer.class);
+
+  private final SchemaFactory<ReviewDb> schemaFactory;
+  private final ListeningExecutorService executor;
+  private final AccountCache accountCache;
+
+  @Inject
+  AllAccountsIndexer(
+      SchemaFactory<ReviewDb> schemaFactory,
+      @IndexExecutor(BATCH) ListeningExecutorService executor,
+      AccountCache accountCache) {
+    this.schemaFactory = schemaFactory;
+    this.executor = executor;
+    this.accountCache = accountCache;
+  }
+
+  @Override
+  public SiteIndexer.Result indexAll(final AccountIndex index) {
+    ProgressMonitor progress =
+        new TextProgressMonitor(new PrintWriter(progressOut));
+    progress.start(2);
+    Stopwatch sw = Stopwatch.createStarted();
+    List<Account.Id> ids;
+    try {
+      ids = collectAccounts(progress);
+    } catch (OrmException e) {
+      log.error("Error collecting accounts", e);
+      return new Result(sw, false, 0, 0);
+    }
+    return reindexAccounts(index, ids, progress);
+  }
+
+  private SiteIndexer.Result reindexAccounts(final AccountIndex index,
+      List<Account.Id> ids, ProgressMonitor progress) {
+    progress.beginTask("Reindexing accounts", ids.size());
+    List<ListenableFuture<?>> futures = new ArrayList<>(ids.size());
+    AtomicBoolean ok = new AtomicBoolean(true);
+    final AtomicInteger done = new AtomicInteger();
+    final AtomicInteger failed = new AtomicInteger();
+    Stopwatch sw = Stopwatch.createStarted();
+    for (final Account.Id id : ids) {
+      final String desc = "account " + id;
+      ListenableFuture<?> future = executor.submit(
+          new Callable<Void>() {
+            @Override
+            public Void call() throws Exception {
+              try {
+                accountCache.evict(id);
+                index.replace(accountCache.get(id));
+                if (verboseWriter != null) {
+                  verboseWriter.println("Reindexed " + desc);
+                }
+                done.incrementAndGet();
+              } catch (Exception e) {
+                failed.incrementAndGet();
+                throw e;
+              }
+              return null;
+            }
+          });
+      addErrorListener(future, desc, progress, ok);
+      futures.add(future);
+    }
+
+    try {
+      Futures.successfulAsList(futures).get();
+    } catch (ExecutionException | InterruptedException e) {
+      log.error("Error waiting on account futures", e);
+      return new Result(sw, false, 0, 0);
+    }
+
+    progress.endTask();
+    return new Result(sw, ok.get(), done.get(), failed.get());
+  }
+
+  private List<Account.Id> collectAccounts(ProgressMonitor progress)
+      throws OrmException {
+    progress.beginTask("Collecting accounts", ProgressMonitor.UNKNOWN);
+    List<Account.Id> ids = new ArrayList<>();
+    try (ReviewDb db = schemaFactory.open()) {
+      for (Account account : db.accounts().all()) {
+        ids.add(account.getId());
+      }
+    }
+    progress.endTask();
+    return ids;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/AllChangesIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
index a38b1e7..14723b6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
@@ -27,7 +27,6 @@
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
-import com.google.common.util.concurrent.MoreExecutors;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -39,7 +38,7 @@
 import com.google.gerrit.server.index.IndexExecutor;
 import com.google.gerrit.server.index.SiteIndexer;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.patch.PatchListLoader;
+import com.google.gerrit.server.patch.AutoMerger;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.SchemaFactory;
@@ -67,6 +66,7 @@
 
 import java.io.IOException;
 import java.io.PrintWriter;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Iterator;
@@ -90,6 +90,7 @@
   private final ChangeNotes.Factory notesFactory;
   private final ProjectCache projectCache;
   private final ThreeWayMergeStrategy mergeStrategy;
+  private final AutoMerger autoMerger;
 
   @Inject
   AllChangesIndexer(SchemaFactory<ReviewDb> schemaFactory,
@@ -99,7 +100,8 @@
       ChangeIndexer.Factory indexerFactory,
       ChangeNotes.Factory notesFactory,
       @GerritServerConfig Config config,
-      ProjectCache projectCache) {
+      ProjectCache projectCache,
+      AutoMerger autoMerger) {
     this.schemaFactory = schemaFactory;
     this.changeDataFactory = changeDataFactory;
     this.repoManager = repoManager;
@@ -108,6 +110,7 @@
     this.notesFactory = notesFactory;
     this.projectCache = projectCache;
     this.mergeStrategy = MergeUtil.getMergeStrategy(config);
+    this.autoMerger = autoMerger;
   }
 
   @Override
@@ -145,47 +148,15 @@
         totalWork >= 0 ? totalWork : MultiProgressMonitor.UNKNOWN);
     final Task failedTask = mpm.beginSubTask("failed", MultiProgressMonitor.UNKNOWN);
 
-    final List<ListenableFuture<?>> futures = Lists.newArrayList();
+    final List<ListenableFuture<?>> futures = new ArrayList<>();
     final AtomicBoolean ok = new AtomicBoolean(true);
 
     for (final Project.NameKey project : projects) {
-      final ListenableFuture<?> future = executor.submit(reindexProject(
+      ListenableFuture<?> future = executor.submit(reindexProject(
           indexerFactory.create(executor, index), project, doneTask, failedTask,
           verboseWriter));
+      addErrorListener(future, "project " + project, projTask, ok);
       futures.add(future);
-      future.addListener(new Runnable() {
-        @Override
-        public void run() {
-          try {
-            future.get();
-          } catch (ExecutionException | InterruptedException e) {
-            fail(project, e);
-          } catch (RuntimeException e) {
-            failAndThrow(project, e);
-          } catch (Error e) {
-            // Can't join with RuntimeException because "RuntimeException |
-            // Error" becomes Throwable, which messes with signatures.
-            failAndThrow(project, e);
-          } finally {
-            projTask.update(1);
-          }
-        }
-
-        private void fail(Project.NameKey project, Throwable t) {
-          log.error("Failed to index project " + project, t);
-          ok.set(false);
-        }
-
-        private void failAndThrow(Project.NameKey project, RuntimeException e) {
-          fail(project, e);
-          throw e;
-        }
-
-        private void failAndThrow(Project.NameKey project, Error e) {
-          fail(project, e);
-          throw e;
-        }
-      }, MoreExecutors.directExecutor());
     }
 
     try {
@@ -236,6 +207,7 @@
           }
           new ProjectIndexer(indexer,
               mergeStrategy,
+              autoMerger,
               byId,
               repo,
               done,
@@ -257,15 +229,16 @@
   private static class ProjectIndexer implements Callable<Void> {
     private final ChangeIndexer indexer;
     private final ThreeWayMergeStrategy mergeStrategy;
+    private final AutoMerger autoMerger;
     private final Multimap<ObjectId, ChangeData> byId;
     private final ProgressMonitor done;
     private final ProgressMonitor failed;
     private final PrintWriter verboseWriter;
     private final Repository repo;
-    private RevWalk walk;
 
     private ProjectIndexer(ChangeIndexer indexer,
         ThreeWayMergeStrategy mergeStrategy,
+        AutoMerger autoMerger,
         Multimap<ObjectId, ChangeData> changesByCommitId,
         Repository repo,
         ProgressMonitor done,
@@ -273,6 +246,7 @@
         PrintWriter verboseWriter) {
       this.indexer = indexer;
       this.mergeStrategy = mergeStrategy;
+      this.autoMerger = autoMerger;
       this.byId = changesByCommitId;
       this.repo = repo;
       this.done = done;
@@ -282,8 +256,8 @@
 
     @Override
     public Void call() throws Exception {
-      walk = new RevWalk(repo);
-      try {
+      try (ObjectInserter ins = repo.newObjectInserter();
+          RevWalk walk = new RevWalk(ins.newReader())) {
         // Walk only refs first to cover as many changes as we can without having
         // to mark every single change.
         for (Ref ref : repo.getRefDatabase().getRefs(Constants.R_HEADS).values()) {
@@ -296,26 +270,25 @@
         RevCommit bCommit;
         while ((bCommit = walk.next()) != null && !byId.isEmpty()) {
           if (byId.containsKey(bCommit)) {
-            getPathsAndIndex(bCommit);
+            getPathsAndIndex(walk, ins, bCommit);
             byId.removeAll(bCommit);
           }
         }
 
         for (ObjectId id : byId.keySet()) {
-          getPathsAndIndex(id);
+          getPathsAndIndex(walk, ins, id);
         }
-      } finally {
-        walk.close();
       }
       return null;
     }
 
-    private void getPathsAndIndex(ObjectId b) throws Exception {
+    private void getPathsAndIndex(RevWalk walk, ObjectInserter ins, ObjectId b)
+        throws Exception {
       List<ChangeData> cds = Lists.newArrayList(byId.get(b));
       try (DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE)) {
         RevCommit bCommit = walk.parseCommit(b);
         RevTree bTree = bCommit.getTree();
-        RevTree aTree = aFor(bCommit, walk);
+        RevTree aTree = aFor(bCommit, walk, ins);
         df.setRepository(repo);
         if (!cds.isEmpty()) {
           List<String> paths = (aTree != null)
@@ -357,7 +330,8 @@
       return ImmutableList.copyOf(paths);
     }
 
-    private RevTree aFor(RevCommit b, RevWalk walk) throws IOException {
+    private RevTree aFor(RevCommit b, RevWalk walk, ObjectInserter ins)
+        throws IOException {
       switch (b.getParentCount()) {
         case 0:
           return walk.parseTree(emptyTree());
@@ -366,7 +340,8 @@
           walk.parseBody(a);
           return walk.parseTree(a.getTree());
         case 2:
-          return PatchListLoader.automerge(repo, walk, b, mergeStrategy);
+          RevCommit am = autoMerger.merge(repo, walk, ins, b, mergeStrategy);
+          return am == null ? null : am.getTree();
         default:
           return null;
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
index fdeb654..e7d2ab2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.index.change;
 
 import static com.google.common.base.MoreObjects.firstNonNull;
+import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.base.Function;
 import com.google.common.base.Splitter;
@@ -29,6 +30,8 @@
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.server.ReviewDbUtil;
+import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.index.FieldDef;
 import com.google.gerrit.server.index.FieldType;
 import com.google.gerrit.server.index.SchemaUtil;
@@ -50,6 +53,7 @@
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -77,7 +81,7 @@
 
   /** Newer style Change-Id key. */
   public static final FieldDef<ChangeData, String> ID =
-      new FieldDef.Single<ChangeData, String>("change_id",
+      new FieldDef.Single<ChangeData, String>(ChangeQueryBuilder.FIELD_CHANGE_ID,
           FieldType.PREFIX, false) {
         @Override
         public String get(ChangeData input, FillArgs args)
@@ -175,7 +179,7 @@
   /** Submission id assigned by MergeOp. */
   public static final FieldDef<ChangeData, String> SUBMISSIONID =
       new FieldDef.Single<ChangeData, String>(
-          "submissionid", FieldType.EXACT, false) {
+          ChangeQueryBuilder.FIELD_SUBMISSIONID, FieldType.EXACT, false) {
         @Override
         public String get(ChangeData input, FillArgs args)
             throws OrmException {
@@ -206,7 +210,7 @@
   public static final FieldDef<ChangeData, Iterable<String>> PATH =
       new FieldDef.Repeatable<ChangeData, String>(
           // Named for backwards compatibility.
-          "file", FieldType.EXACT, false) {
+          ChangeQueryBuilder.FIELD_FILE, FieldType.EXACT, false) {
         @Override
         public Iterable<String> get(ChangeData input, FillArgs args)
             throws OrmException {
@@ -221,7 +225,7 @@
       return ImmutableSet.of();
     }
     Splitter s = Splitter.on('/').omitEmptyStrings();
-    Set<String> r = Sets.newHashSet();
+    Set<String> r = new HashSet<>();
     for (String path : paths) {
       for (String part : s.split(path)) {
         r.add(part);
@@ -233,18 +237,33 @@
   /** Hashtags tied to a change */
   public static final FieldDef<ChangeData, Iterable<String>> HASHTAG =
       new FieldDef.Repeatable<ChangeData, String>(
-          "hashtag", FieldType.EXACT, false) {
+          ChangeQueryBuilder.FIELD_HASHTAG, FieldType.EXACT, false) {
         @Override
         public Iterable<String> get(ChangeData input, FillArgs args)
             throws OrmException {
-          return ImmutableSet.copyOf(Iterables.transform(input.notes().load()
-              .getHashtags(), new Function<String, String>() {
-
+          return ImmutableSet.copyOf(Iterables.transform(input.hashtags(),
+              new Function<String, String>() {
             @Override
             public String apply(String input) {
               return input.toLowerCase();
             }
+          }));
+        }
+      };
 
+  /** Hashtags with original case. */
+  public static final FieldDef<ChangeData, Iterable<byte[]>> HASHTAG_CASE_AWARE =
+      new FieldDef.Repeatable<ChangeData, byte[]>(
+          "_hashtag", FieldType.STORED_ONLY, true) {
+        @Override
+        public Iterable<byte[]> get(ChangeData input, FillArgs args)
+            throws OrmException {
+          return ImmutableSet.copyOf(Iterables.transform(input.hashtags(),
+              new Function<String, byte[]>() {
+            @Override
+            public byte[] apply(String hashtag) {
+              return hashtag.getBytes(UTF_8);
+            }
           }));
         }
       };
@@ -252,7 +271,7 @@
   /** Components of each file path modified in the current patch set. */
   public static final FieldDef<ChangeData, Iterable<String>> FILE_PART =
       new FieldDef.Repeatable<ChangeData, String>(
-          "filepart", FieldType.EXACT, false) {
+          ChangeQueryBuilder.FIELD_FILEPART, FieldType.EXACT, false) {
         @Override
         public Iterable<String> get(ChangeData input, FillArgs args)
             throws OrmException {
@@ -286,7 +305,7 @@
           if (c == null) {
             return ImmutableSet.of();
           }
-          Set<Integer> r = Sets.newHashSet();
+          Set<Integer> r = new HashSet<>();
           if (!args.allowsDrafts && c.getStatus() == Change.Status.DRAFT) {
             return r;
           }
@@ -311,7 +330,7 @@
   /** Commit ID of any patch set on the change, using exact match. */
   public static final FieldDef<ChangeData, Iterable<String>> EXACT_COMMIT =
       new FieldDef.Repeatable<ChangeData, String>(
-          "exactcommit", FieldType.EXACT, false) {
+          ChangeQueryBuilder.FIELD_EXACTCOMMIT, FieldType.EXACT, false) {
         @Override
         public Iterable<String> get(ChangeData input, FillArgs args)
             throws OrmException {
@@ -320,7 +339,7 @@
       };
 
   private static Set<String> getRevisions(ChangeData cd) throws OrmException {
-    Set<String> revisions = Sets.newHashSet();
+    Set<String> revisions = new HashSet<>();
     for (PatchSet ps : cd.patchSets()) {
       if (ps.getRevision() != null) {
         revisions.add(ps.getRevision().get());
@@ -356,8 +375,8 @@
         @Override
         public Iterable<String> get(ChangeData input, FillArgs args)
             throws OrmException {
-          Set<String> allApprovals = Sets.newHashSet();
-          Set<String> distinctApprovals = Sets.newHashSet();
+          Set<String> allApprovals = new HashSet<>();
+          Set<String> distinctApprovals = new HashSet<>();
           for (PatchSetApproval a : input.currentApprovals()) {
             if (a.getValue() != 0 && !a.isLegacySubmit()) {
               allApprovals.add(formatLabel(a.getLabel(), a.getValue(),
@@ -489,7 +508,7 @@
         @Override
         public Iterable<String> get(ChangeData input, FillArgs args)
             throws OrmException {
-          Set<String> r = Sets.newHashSet();
+          Set<String> r = new HashSet<>();
           for (PatchLineComment c : input.publishedComments()) {
             r.add(c.getMessage());
           }
@@ -503,7 +522,7 @@
   /** Whether the change is mergeable. */
   public static final FieldDef<ChangeData, String> MERGEABLE =
       new FieldDef.Single<ChangeData, String>(
-          "mergeable2", FieldType.EXACT, true) {
+          ChangeQueryBuilder.FIELD_MERGEABLE, FieldType.EXACT, true) {
         @Override
         public String get(ChangeData input, FillArgs args)
             throws OrmException {
@@ -576,10 +595,60 @@
         }
       };
 
+  /** Users who have starred this change. */
+  @Deprecated
+  public static final FieldDef<ChangeData, Iterable<Integer>> STARREDBY =
+      new FieldDef.Repeatable<ChangeData, Integer>(
+          ChangeQueryBuilder.FIELD_STARREDBY, FieldType.INTEGER, true) {
+        @Override
+        public Iterable<Integer> get(ChangeData input, FillArgs args)
+            throws OrmException {
+          return Iterables.transform(input.starredBy(),
+              new Function<Account.Id, Integer>() {
+            @Override
+            public Integer apply(Account.Id accountId) {
+              return accountId.get();
+            }
+          });
+        }
+      };
+
+  /**
+   * Star labels on this change in the format: &lt;account-id&gt;:&lt;label&gt;
+   */
+  public static final FieldDef<ChangeData, Iterable<String>> STAR =
+      new FieldDef.Repeatable<ChangeData, String>(
+          ChangeQueryBuilder.FIELD_STAR, FieldType.EXACT, true) {
+        @Override
+        public Iterable<String> get(ChangeData input, FillArgs args)
+            throws OrmException {
+          return Iterables.transform(input.stars().entries(),
+              new Function<Map.Entry<Account.Id, String>, String>() {
+            @Override
+            public String apply(Map.Entry<Account.Id, String> e) {
+              return StarredChangesUtil.StarField.create(
+                  e.getKey(), e.getValue()).toString();
+            }
+          });
+        }
+      };
+
+  /** Users that have starred the change with any label. */
+  public static final FieldDef<ChangeData, Iterable<Integer>> STARBY =
+      new FieldDef.Repeatable<ChangeData, Integer>(
+          ChangeQueryBuilder.FIELD_STARBY, FieldType.INTEGER, false) {
+        @Override
+        public Iterable<Integer> get(ChangeData input, FillArgs args)
+            throws OrmException {
+          return Iterables.transform(input.stars().keySet(),
+              ReviewDbUtil.INT_KEY_FUNCTION);
+        }
+      };
+
   /** Opaque group identifiers for this change's patch sets. */
   public static final FieldDef<ChangeData, Iterable<String>> GROUP =
       new FieldDef.Repeatable<ChangeData, String>(
-          "group", FieldType.EXACT, false) {
+          ChangeQueryBuilder.FIELD_GROUP, FieldType.EXACT, false) {
         @Override
         public Iterable<String> get(ChangeData input, FillArgs args)
             throws OrmException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexer.java
index 48df562..0ca9922 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexer.java
@@ -186,14 +186,26 @@
   /**
    * Synchronously index a change.
    *
-   * @param change change to index.
    * @param db review database.
+   * @param change change to index.
    */
   public void index(ReviewDb db, Change change) throws IOException {
     index(changeDataFactory.create(db, change));
   }
 
   /**
+   * Synchronously index a change.
+   *
+   * @param db review database.
+   * @param project the project to which the change belongs.
+   * @param changeId ID of the change to index.
+   */
+  public void index(ReviewDb db, Project.NameKey project, Change.Id changeId)
+      throws IOException {
+    index(changeDataFactory.create(db, project, changeId));
+  }
+
+  /**
    * Start deleting a change.
    *
    * @param id change to delete.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
index b39a0b7..db889c2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -59,49 +59,26 @@
       ChangeField.COMMITTER);
 
   @Deprecated
-  static final Schema<ChangeData> V26 = schema(
-      ChangeField.LEGACY_ID,
-      ChangeField.ID,
-      ChangeField.STATUS,
-      ChangeField.PROJECT,
-      ChangeField.PROJECTS,
-      ChangeField.REF,
-      ChangeField.EXACT_TOPIC,
-      ChangeField.FUZZY_TOPIC,
-      ChangeField.UPDATED,
-      ChangeField.FILE_PART,
-      ChangeField.PATH,
-      ChangeField.OWNER,
-      ChangeField.REVIEWER,
-      ChangeField.COMMIT,
-      ChangeField.TR,
-      ChangeField.LABEL,
-      ChangeField.COMMIT_MESSAGE,
-      ChangeField.COMMENT,
-      ChangeField.CHANGE,
-      ChangeField.APPROVAL,
-      ChangeField.MERGEABLE,
-      ChangeField.ADDED,
-      ChangeField.DELETED,
-      ChangeField.DELTA,
-      ChangeField.HASHTAG,
-      ChangeField.COMMENTBY,
-      ChangeField.PATCH_SET,
-      ChangeField.GROUP,
-      ChangeField.SUBMISSIONID,
-      ChangeField.EDITBY,
-      ChangeField.REVIEWEDBY,
-      ChangeField.EXACT_COMMIT,
-      ChangeField.AUTHOR,
-      ChangeField.COMMITTER,
-      ChangeField.DRAFTBY);
+  static final Schema<ChangeData> V26 = schema(V25, ChangeField.DRAFTBY);
 
+  @Deprecated
   static final Schema<ChangeData> V27 = schema(V26.getFields().values());
 
+  @Deprecated
+  static final Schema<ChangeData> V28 = schema(V27, ChangeField.STARREDBY);
+
+  @Deprecated
+  static final Schema<ChangeData> V29 =
+      schema(V28, ChangeField.HASHTAG_CASE_AWARE);
+
+  static final Schema<ChangeData> V30 =
+      schema(V29, ChangeField.STAR, ChangeField.STARBY);
+
+  public static final String NAME = "changes";
   public static final ChangeSchemaDefinitions INSTANCE =
       new ChangeSchemaDefinitions();
 
   private ChangeSchemaDefinitions() {
-    super("changes", ChangeData.class);
+    super(NAME, ChangeData.class);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java
index dbd0438..1e8bdf4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java
@@ -24,7 +24,7 @@
 
 /** Send notice about a change being abandoned by its owner. */
 public class AbandonedSender extends ReplyToChangeSender {
-  public static interface Factory extends
+  public interface Factory extends
       ReplyToChangeSender.Factory<AbandonedSender> {
     @Override
     AbandonedSender create(Project.NameKey project, Change.Id change);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddReviewerSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddReviewerSender.java
index 98fc4a0..c9e42ad 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddReviewerSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddReviewerSender.java
@@ -23,7 +23,7 @@
 
 /** Asks a user to review a change. */
 public class AddReviewerSender extends NewChangeSender {
-  public static interface Factory {
+  public interface Factory {
     AddReviewerSender create(Project.NameKey project, Change.Id id);
   }
 
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 b244ec4..367159d 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
@@ -28,11 +28,13 @@
 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.StarredChangesUtil;
 import com.google.gerrit.server.mail.ProjectWatch.Watchers;
 import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListEntry;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
@@ -304,11 +306,11 @@
     try {
       // BCC anyone who has starred this change.
       //
-      for (Account.Id accountId : args.starredChangesUtil
-          .byChange(change.getId())) {
+      for (Account.Id accountId : args.starredChangesUtil.byChangeFromIndex(
+          change.getId(), StarredChangesUtil.DEFAULT_LABEL)) {
         super.add(RecipientType.BCC, accountId);
       }
-    } catch (OrmException err) {
+    } catch (OrmException | NoSuchChangeException err) {
       // Just don't BCC everyone. Better to send a partial message to those
       // we already have queued up then to fail deliver entirely to people
       // who have a lower interest in the change.
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 c845c28..a39e43a 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
@@ -52,7 +52,7 @@
   private static final Logger log = LoggerFactory
       .getLogger(CommentSender.class);
 
-  public static interface Factory {
+  public interface Factory {
     CommentSender create(Project.NameKey project, Change.Id id);
   }
 
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 87bfa37..2110e37 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
@@ -33,7 +33,7 @@
   private static final Logger log =
       LoggerFactory.getLogger(CreateChangeSender.class);
 
-  public static interface Factory {
+  public interface Factory {
     CreateChangeSender create(Project.NameKey project, Change.Id id);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/DeleteReviewerSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/DeleteReviewerSender.java
new file mode 100644
index 0000000..75f9f82
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/DeleteReviewerSender.java
@@ -0,0 +1,81 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.mail;
+
+import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/** Let users know that a reviewer and possibly her review have
+ * been removed. */
+public class DeleteReviewerSender extends ReplyToChangeSender {
+  private final Set<Account.Id> reviewers = new HashSet<>();
+
+  public interface Factory extends
+      ReplyToChangeSender.Factory<DeleteReviewerSender> {
+    @Override
+    DeleteReviewerSender create(Project.NameKey project, Change.Id change);
+  }
+
+  @Inject
+  public DeleteReviewerSender(EmailArguments ea,
+      @Assisted Project.NameKey project,
+      @Assisted Change.Id id)
+      throws OrmException {
+    super(ea, "deleteReviewer", newChangeData(ea, project, id));
+  }
+
+  public void addReviewers(Collection<Account.Id> cc) {
+    reviewers.addAll(cc);
+  }
+
+  @Override
+  protected void init() throws EmailException {
+    super.init();
+
+    ccAllApprovals();
+    bccStarredBy();
+    ccExistingReviewers();
+    includeWatchers(NotifyType.ALL_COMMENTS);
+    add(RecipientType.TO, reviewers);
+  }
+
+  @Override
+  protected void formatChange() throws EmailException {
+    appendText(velocifyFile("DeleteReviewer.vm"));
+  }
+
+  public List<String> getReviewerNames() {
+    if (reviewers.isEmpty()) {
+      return null;
+    }
+    List<String> names = new ArrayList<>();
+    for (Account.Id id : reviewers) {
+      names.add(getNameFor(id));
+    }
+    return names;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/DeleteVoteSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/DeleteVoteSender.java
index 05937d5..d861109 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/DeleteVoteSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/DeleteVoteSender.java
@@ -24,7 +24,7 @@
 
 /** Send notice about a vote that was removed from a change. */
 public class DeleteVoteSender extends ReplyToChangeSender {
-  public static interface Factory extends
+  public interface Factory extends
       ReplyToChangeSender.Factory<DeleteVoteSender> {
     @Override
     DeleteVoteSender create(Project.NameKey project, Change.Id change);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailModule.java
index a9240cb..7ceb0ae 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailModule.java
@@ -21,6 +21,7 @@
   protected void configure() {
     factory(AbandonedSender.Factory.class);
     factory(CommentSender.Factory.class);
+    factory(DeleteReviewerSender.Factory.class);
     factory(DeleteVoteSender.Factory.class);
     factory(RestoredSender.Factory.class);
     factory(RevertedSender.Factory.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailTokenVerifier.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailTokenVerifier.java
index b6fb006..41e1e2c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailTokenVerifier.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailTokenVerifier.java
@@ -39,7 +39,7 @@
   ParsedToken decode(String tokenString) throws InvalidTokenException;
 
   /** Exception thrown when a token does not parse correctly. */
-  public static class InvalidTokenException extends Exception {
+  class InvalidTokenException extends Exception {
     private static final long serialVersionUID = 1L;
 
     public InvalidTokenException() {
@@ -52,7 +52,7 @@
   }
 
   /** Pair returned from decode to provide the data used during encode. */
-  public static class ParsedToken {
+  class ParsedToken {
     private final Account.Id accountId;
     private final String emailAddress;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergeFailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergeFailSender.java
deleted file mode 100644
index 3dfbcda..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergeFailSender.java
+++ /dev/null
@@ -1,49 +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.mail;
-
-import com.google.gerrit.common.errors.EmailException;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-/** Send notice about a change failing to merged. */
-public class MergeFailSender extends ReplyToChangeSender {
-  public static interface Factory {
-    MergeFailSender create(Project.NameKey project, Change.Id id);
-  }
-
-  @Inject
-  public MergeFailSender(EmailArguments ea,
-      @Assisted Project.NameKey project,
-      @Assisted Change.Id id)
-      throws OrmException {
-    super(ea, "merge-failed", newChangeData(ea, project, id));
-  }
-
-  @Override
-  protected void init() throws EmailException {
-    super.init();
-
-    ccExistingReviewers();
-  }
-
-  @Override
-  protected void formatChange() throws EmailException {
-    appendText(velocifyFile("MergeFail.vm"));
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
index 7271c18..f6c3d0f 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
@@ -31,7 +31,7 @@
 
 /** Send notice about a change successfully merged. */
 public class MergedSender extends ReplyToChangeSender {
-  public static interface Factory {
+  public interface Factory {
     MergedSender create(Project.NameKey project, Change.Id 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 0442e23..4834efd 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
@@ -18,7 +18,6 @@
 import static com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy.DISABLED;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
-import com.google.common.collect.Sets;
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.api.changes.ReviewInput.NotifyHandling;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
@@ -63,7 +62,7 @@
   protected String messageClass;
   private final HashSet<Account.Id> rcptTo = new HashSet<>();
   private final Map<String, EmailHeader> headers;
-  private final Set<Address> smtpRcptTo = Sets.newHashSet();
+  private final Set<Address> smtpRcptTo = new HashSet<>();
   private Address smtpFromAddress;
   private StringBuilder body;
   protected VelocityContext velocityContext;
@@ -303,7 +302,7 @@
     } else if (email != null) {
       return email;
 
-    } else /* (name == null && email == null) */{
+    } else /* (name == null && email == null) */ {
       return args.anonymousCowardName + " #" + accountId;
     }
   }
@@ -494,9 +493,11 @@
         j.remove();
       }
     }
-    for (EmailHeader hdr : headers.values()) {
-      if (hdr instanceof AddressList) {
-        ((AddressList) hdr).remove(fromEmail);
+    for (Map.Entry<String, EmailHeader> entry : headers.entrySet()) {
+      // Don't remove fromEmail from the "From" header though!
+      if (entry.getValue() instanceof AddressList
+          && !entry.getKey().equals("From")) {
+        ((AddressList) entry.getValue()).remove(fromEmail);
       }
     }
   }
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 95b0219..374b2e9 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
@@ -15,8 +15,6 @@
 package com.google.gerrit.server.mail;
 
 import com.google.common.base.Strings;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Sets;
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupDescriptions;
 import com.google.gerrit.common.data.GroupReference;
@@ -41,6 +39,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
@@ -100,8 +99,8 @@
 
   public static class Watchers {
     static class List {
-      protected final Set<Account.Id> accounts = Sets.newHashSet();
-      protected final Set<Address> emails = Sets.newHashSet();
+      protected final Set<Account.Id> accounts = new HashSet<>();
+      protected final Set<Address> emails = new HashSet<>();
     }
     protected final List to = new List();
     protected final List cc = new List();
@@ -141,8 +140,8 @@
       Watchers.List matching,
       AccountGroup.UUID startUUID) throws OrmException {
     ReviewDb db = args.db.get();
-    Set<AccountGroup.UUID> seen = Sets.newHashSet();
-    List<AccountGroup.UUID> q = Lists.newArrayList();
+    Set<AccountGroup.UUID> seen = new HashSet<>();
+    List<AccountGroup.UUID> q = new ArrayList<>();
 
     seen.add(startUUID);
     q.add(startUUID);
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 6bee3e8..df9f20e 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
@@ -31,7 +31,7 @@
 
 /** Send notice of new patch sets for reviewers. */
 public class ReplacePatchSetSender extends ReplyToChangeSender {
-  public static interface Factory {
+  public interface Factory {
     ReplacePatchSetSender create(Project.NameKey project, Change.Id id);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplyToChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplyToChangeSender.java
index 8eadeef..dd922d3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplyToChangeSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplyToChangeSender.java
@@ -22,7 +22,7 @@
 
 /** Alert a user to a reply to a change, usually commentary made during review. */
 public abstract class ReplyToChangeSender extends ChangeEmail {
-  public static interface Factory<T extends ReplyToChangeSender> {
+  public interface Factory<T extends ReplyToChangeSender> {
     T create(Project.NameKey project, Change.Id id);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java
index 0808090..d946eb2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java
@@ -24,7 +24,7 @@
 
 /** Send notice about a change being restored by its owner. */
 public class RestoredSender extends ReplyToChangeSender {
-  public static interface Factory extends
+  public interface Factory extends
       ReplyToChangeSender.Factory<RestoredSender> {
     @Override
     RestoredSender create(Project.NameKey project, Change.Id id);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java
index 55438fe..2c9c37e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java
@@ -24,7 +24,7 @@
 
 /** Send notice about a change being reverted. */
 public class RevertedSender extends ReplyToChangeSender {
-  public static interface Factory {
+  public interface Factory {
     RevertedSender create(Project.NameKey project, Change.Id id);
   }
 
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 aa57247..e263c6a 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
@@ -57,7 +57,7 @@
     }
   }
 
-  public static enum Encryption {
+  public enum Encryption {
     NONE, SSL, TLS
   }
 
@@ -159,10 +159,10 @@
     setMissingHeader(hdrs, "Content-Transfer-Encoding", "8bit");
     setMissingHeader(hdrs, "Content-Disposition", "inline");
     setMissingHeader(hdrs, "User-Agent", "Gerrit/" + Version.getVersion());
-    if(importance != null) {
+    if (importance != null) {
       setMissingHeader(hdrs, "Importance", importance);
     }
-    if(expiryDays > 0) {
+    if (expiryDays > 0) {
       Date expiry = new Date(TimeUtil.nowMs() +
         expiryDays * 24 * 60 * 60 * 1000L );
       setMissingHeader(hdrs, "Expiry-Date",
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
index d3fbb58..e763e0c 100644
--- 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
@@ -26,6 +26,7 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -35,7 +36,6 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevWalk;
 
 import java.io.IOException;
 
@@ -75,8 +75,8 @@
   }
 
   @AutoValue
-  public static abstract class LoadHandle implements AutoCloseable {
-    public static LoadHandle create(RevWalk walk, ObjectId id) {
+  public abstract static class LoadHandle implements AutoCloseable {
+    public static LoadHandle create(ChangeNotesRevWalk walk, ObjectId id) {
       return new AutoValue_AbstractChangeNotes_LoadHandle(
           checkNotNull(walk), id != null ? id.copy() : null);
     }
@@ -85,7 +85,7 @@
       return new AutoValue_AbstractChangeNotes_LoadHandle(null, null);
     }
 
-    @Nullable public abstract RevWalk walk();
+    @Nullable public abstract ChangeNotesRevWalk walk();
     @Nullable public abstract ObjectId id();
 
     @Override
@@ -97,14 +97,16 @@
   }
 
   protected final Args args;
+  protected final boolean autoRebuild;
   private final Change.Id changeId;
 
   private ObjectId revision;
   private boolean loaded;
 
-  AbstractChangeNotes(Args args, Change.Id changeId) {
-    this.args = args;
-    this.changeId = changeId;
+  AbstractChangeNotes(Args args, Change.Id changeId, boolean autoRebuild) {
+    this.args = checkNotNull(args);
+    this.changeId = checkNotNull(changeId);
+    this.autoRebuild = autoRebuild;
   }
 
   public Change.Id getChangeId() {
@@ -120,16 +122,26 @@
     if (loaded) {
       return self();
     }
-    if (!args.migration.readChanges() || changeId == null) {
+    boolean read = args.migration.readChanges();
+    boolean readOrWrite = read || args.migration.writeChanges();
+    if (!readOrWrite && !autoRebuild) {
       loadDefaults();
       return self();
     }
+    if (args.migration.failOnLoad()) {
+      throw new OrmException("Reading from NoteDb is disabled");
+    }
     try (Timer1.Context timer = args.metrics.readLatency.start(CHANGES);
-        Repository repo =
-            args.repoManager.openMetadataRepository(getProjectName());
+        Repository repo = args.repoManager.openRepository(getProjectName());
+        // Call openHandle even if reading is disabled, to trigger
+        // auto-rebuilding before this object may get passed to a ChangeUpdate.
         LoadHandle handle = openHandle(repo)) {
-      revision = handle.id();
-      onLoad(handle);
+      if (read) {
+        revision = handle.id();
+        onLoad(handle);
+      } else {
+        loadDefaults();
+      }
       loaded = true;
     } catch (ConfigInvalidException | IOException e) {
       throw new OrmException(e);
@@ -137,11 +149,17 @@
     return self();
   }
 
-  protected LoadHandle openHandle(Repository repo) throws IOException {
+  protected ObjectId readRef(Repository repo) throws IOException {
     Ref ref = repo.getRefDatabase().exactRef(getRefName());
-    return LoadHandle.create(
-        new RevWalk(repo),
-        ref != null ? ref.getObjectId() : null);
+    return ref != null ? ref.getObjectId() : null;
+  }
+
+  protected LoadHandle openHandle(Repository repo) throws IOException {
+    return openHandle(repo, readRef(repo));
+  }
+
+  protected LoadHandle openHandle(Repository repo, ObjectId id) {
+    return LoadHandle.create(ChangeNotesCommit.newRevWalk(repo), id);
   }
 
   public T reload() throws OrmException {
@@ -155,8 +173,7 @@
     } else if (!args.migration.enabled()) {
       return null;
     }
-    try (Repository repo =
-        args.repoManager.openMetadataRepository(getProjectName())) {
+    try (Repository repo = args.repoManager.openRepository(getProjectName())) {
       Ref ref = repo.getRefDatabase().exactRef(getRefName());
       return ref != null ? ref.getObjectId() : null;
     } catch (IOException e) {
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
index a55888d..b55b416 100644
--- 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
@@ -186,6 +186,8 @@
     if (cb == null) {
       result = z;
       return z; // Impl intends to delete the ref.
+    } else if (cb == NO_OP_UPDATE) {
+      return null; // Impl is a no-op.
     }
     cb.setAuthor(authorIdent);
     cb.setCommitter(new PersonIdent(serverIdent, when));
@@ -214,13 +216,17 @@
    *     the meta ref should be deleted as a result of this update. The parent,
    *     author, and committer fields in the return value are always
    *     overwritten. The tree ID may be unset by this method, which indicates
-   *     to the caller that it should be copied from the parent commit.
+   *     to the caller that it should be copied from the parent commit. To
+   *     indicate that this update is a no-op (but this could not be determined
+   *     by {@link #isEmpty()}), return the sentinel {@link #NO_OP_UPDATE}.
    * @throws OrmException if a Gerrit-level error occurred.
    * @throws IOException if a lower-level error occurred.
    */
   protected abstract CommitBuilder applyImpl(RevWalk rw, ObjectInserter ins,
       ObjectId curr) throws OrmException, IOException;
 
+  protected static final CommitBuilder NO_OP_UPDATE = new CommitBuilder();
+
   ObjectId getResult() {
     return result;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeBundle.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeBundle.java
index 51b80cd..0dfd8c9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeBundle.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeBundle.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.notedb;
 
+import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Preconditions.checkState;
@@ -22,14 +23,25 @@
 import static com.google.gerrit.server.notedb.ChangeBundle.Source.NOTE_DB;
 import static com.google.gerrit.server.notedb.ChangeBundle.Source.REVIEW_DB;
 
-import com.google.common.base.Joiner;
+import com.google.auto.value.AutoValue;
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Collections2;
 import com.google.common.collect.ComparisonChain;
 import com.google.common.collect.ImmutableCollection;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSortedMap;
 import com.google.common.collect.Iterables;
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
 import com.google.common.collect.Ordering;
 import com.google.common.collect.Sets;
+import com.google.gerrit.common.Nullable;
+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.Patch;
@@ -45,7 +57,10 @@
 import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Comparator;
+import java.util.Iterator;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -112,35 +127,38 @@
     return out;
   }
 
+  // Unlike the *Map comparators, which are intended to make key lists diffable,
+  // this comparator sorts first on timestamp, then on every other field.
+  private static final Ordering<ChangeMessage> CHANGE_MESSAGE_ORDER =
+      new Ordering<ChangeMessage>() {
+        final Ordering<Comparable<?>> nullsFirst =
+            Ordering.natural().nullsFirst();
+
+        @Override
+        public int compare(ChangeMessage a, ChangeMessage b) {
+          return ComparisonChain.start()
+              .compare(a.getWrittenOn(), b.getWrittenOn())
+              .compare(a.getKey().getParentKey().get(),
+                  b.getKey().getParentKey().get())
+              .compare(psId(a), psId(b), nullsFirst)
+              .compare(a.getAuthor(), b.getAuthor(), intKeyOrdering())
+              .compare(a.getMessage(), b.getMessage(), nullsFirst)
+              .result();
+        }
+
+        private Integer psId(ChangeMessage m) {
+          return m.getPatchSetId() != null ? m.getPatchSetId().get() : null;
+        }
+      };
+
   private static ImmutableList<ChangeMessage> changeMessageList(
       Iterable<ChangeMessage> in) {
-    // Unlike the *Map comparators, which are intended to make key lists
-    // diffable, this comparator sorts first on timestamp, then on every other
-    // field.
-    final Ordering<Comparable<?>> nullsFirst = Ordering.natural().nullsFirst();
-    return new Ordering<ChangeMessage>() {
-      @Override
-      public int compare(ChangeMessage a, ChangeMessage b) {
-        return ComparisonChain.start()
-            .compare(roundToSecond(a.getWrittenOn()),
-                roundToSecond(b.getWrittenOn()))
-            .compare(a.getKey().getParentKey().get(),
-                b.getKey().getParentKey().get())
-            .compare(psId(a), psId(b), nullsFirst)
-            .compare(a.getAuthor(), b.getAuthor(), intKeyOrdering())
-            .compare(a.getMessage(), b.getMessage(), nullsFirst)
-            .result();
-      }
-
-      private Integer psId(ChangeMessage m) {
-        return m.getPatchSetId() != null ? m.getPatchSetId().get() : null;
-      }
-    }.immutableSortedCopy(in);
+    return CHANGE_MESSAGE_ORDER.immutableSortedCopy(in);
   }
 
 
-  private static Map<PatchSet.Id, PatchSet> patchSetMap(Iterable<PatchSet> in) {
-    Map<PatchSet.Id, PatchSet> out = new TreeMap<>(
+  private static TreeMap<PatchSet.Id, PatchSet> patchSetMap(Iterable<PatchSet> in) {
+    TreeMap<PatchSet.Id, PatchSet> out = new TreeMap<>(
         new Comparator<PatchSet.Id>() {
           @Override
           public int compare(PatchSet.Id a, PatchSet.Id b) {
@@ -222,18 +240,18 @@
         // the Change with the state implied by a ChangeNotes.
         101);
     checkColumns(ChangeMessage.Key.class, 1, 2);
-    checkColumns(ChangeMessage.class, 1, 2, 3, 4, 5);
+    checkColumns(ChangeMessage.class, 1, 2, 3, 4, 5, 6);
     checkColumns(PatchSet.Id.class, 1, 2);
     checkColumns(PatchSet.class, 1, 2, 3, 4, 5, 6, 8);
     checkColumns(PatchSetApproval.Key.class, 1, 2, 3);
-    checkColumns(PatchSetApproval.class, 1, 2, 3);
+    checkColumns(PatchSetApproval.class, 1, 2, 3, 6);
     checkColumns(PatchLineComment.Key.class, 1, 2);
-    checkColumns(PatchLineComment.class, 1, 2, 3, 4, 5, 6, 7, 8, 9);
+    checkColumns(PatchLineComment.class, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
   }
 
   private final Change change;
   private final ImmutableList<ChangeMessage> changeMessages;
-  private final ImmutableMap<PatchSet.Id, PatchSet> patchSets;
+  private final ImmutableSortedMap<PatchSet.Id, PatchSet> patchSets;
   private final ImmutableMap<PatchSetApproval.Key, PatchSetApproval>
       patchSetApprovals;
   private final ImmutableMap<PatchLineComment.Key, PatchLineComment>
@@ -249,7 +267,7 @@
       Source source) {
     this.change = checkNotNull(change);
     this.changeMessages = changeMessageList(changeMessages);
-    this.patchSets = ImmutableMap.copyOf(patchSetMap(patchSets));
+    this.patchSets = ImmutableSortedMap.copyOfSorted(patchSetMap(patchSets));
     this.patchSetApprovals =
         ImmutableMap.copyOf(patchSetApprovalMap(patchSetApprovals));
     this.patchLineComments =
@@ -305,13 +323,214 @@
     return ImmutableList.copyOf(diffs);
   }
 
+  private Timestamp getFirstPatchSetTime() {
+    if (patchSets.isEmpty()) {
+      return change.getCreatedOn();
+    }
+    return patchSets.firstEntry().getValue().getCreatedOn();
+  }
+
+  private Timestamp getLatestTimestamp() {
+    Ordering<Timestamp> o = Ordering.natural().nullsFirst();
+    Timestamp ts = null;
+    for (ChangeMessage cm : getChangeMessages()) {
+      ts = o.max(ts, cm.getWrittenOn());
+    }
+    for (PatchSet ps : getPatchSets()) {
+      ts = o.max(ts, ps.getCreatedOn());
+    }
+    for (PatchSetApproval psa : getPatchSetApprovals()) {
+      ts = o.max(ts, psa.getGranted());
+    }
+    for (PatchLineComment plc : getPatchLineComments()) {
+      // Ignore draft comments, as they do not show up in the change meta graph.
+      if (plc.getStatus() != PatchLineComment.Status.DRAFT) {
+        ts = o.max(ts, plc.getWrittenOn());
+      }
+    }
+    return firstNonNull(ts, change.getLastUpdatedOn());
+  }
+
+  private Map<PatchSetApproval.Key, PatchSetApproval>
+      filterPatchSetApprovals() {
+    return limitToValidPatchSets(patchSetApprovals,
+        new Function<PatchSetApproval.Key, PatchSet.Id>() {
+          @Override
+          public PatchSet.Id apply(PatchSetApproval.Key in) {
+            return in.getParentKey();
+          }
+        });
+  }
+
+  private Map<PatchLineComment.Key, PatchLineComment>
+      filterPatchLineComments() {
+    return limitToValidPatchSets(patchLineComments,
+        new Function<PatchLineComment.Key, PatchSet.Id>() {
+          @Override
+          public PatchSet.Id apply(PatchLineComment.Key in) {
+            return in.getParentKey().getParentKey();
+          }
+        });
+  }
+
+  private <K, V> Map<K, V> limitToValidPatchSets(Map<K, V> in,
+      final Function<K, PatchSet.Id> func) {
+    final Predicate<PatchSet.Id> upToCurrent = upToCurrentPredicate();
+    return Maps.filterKeys(
+        in, new Predicate<K>() {
+          @Override
+          public boolean apply(K in) {
+            PatchSet.Id psId = func.apply(in);
+            return upToCurrent.apply(psId) && patchSets.containsKey(psId);
+          }
+        });
+  }
+
+  private Collection<ChangeMessage> filterChangeMessages() {
+    return Collections2.filter(changeMessages,
+        new Predicate<ChangeMessage>() {
+          @Override
+          public boolean apply(ChangeMessage in) {
+            PatchSet.Id psId = in.getPatchSetId();
+            return psId == null || patchSets.containsKey(psId);
+          }
+        });
+  }
+
+  private Predicate<PatchSet.Id> upToCurrentPredicate() {
+    final int max = change.currentPatchSetId().get();
+    return new Predicate<PatchSet.Id>() {
+      @Override
+      public boolean apply(PatchSet.Id in) {
+        return in.get() <= max;
+      }
+    };
+  }
+
+  private Map<PatchSet.Id, PatchSet> filterPatchSets() {
+    return Maps.filterKeys(patchSets, upToCurrentPredicate());
+  }
+
   private static void diffChanges(List<String> diffs, ChangeBundle bundleA,
       ChangeBundle bundleB) {
     Change a = bundleA.change;
     Change b = bundleB.change;
     String desc = a.getId().equals(b.getId()) ? describe(a.getId()) : "Changes";
+
+    boolean excludeCreatedOn = false;
+    boolean excludeTopic = false;
+    Timestamp aUpdated = a.getLastUpdatedOn();
+    Timestamp bUpdated = b.getLastUpdatedOn();
+
+    CharMatcher s = CharMatcher.is(' ');
+    boolean excludeSubject = false;
+    boolean excludeOrigSubj = false;
+    String aSubj = a.getSubject();
+    String bSubj = b.getSubject();
+
+    // Allow created timestamp in NoteDb to be either the created timestamp of
+    // the change, or the timestamp of the first remaining patch set.
+    //
+    // Ignore subject if the NoteDb subject starts with the ReviewDb subject.
+    // The NoteDb subject is read directly from the commit, whereas the ReviewDb
+    // subject historically may have been truncated to fit in a SQL varchar
+    // column.
+    //
+    // Ignore original subject on the ReviewDb side when comparing to NoteDb.
+    // This field may have any number of values:
+    //  - It may be null, if the change has had no new patch sets pushed since
+    //    migrating to schema 103.
+    //  - It may match the first patch set subject, if the change was created
+    //    after migrating to schema 103.
+    //  - It may match the subject of the first patch set that was pushed after
+    //    the migration to schema 103, even though that is neither the subject
+    //    of the first patch set nor the subject of the last patch set. (See
+    //    Change#setCurrentPatchSet as of 43b10f86 for this behavior.) This
+    //    subject of an intermediate patch set is not available to the
+    //    ChangeBundle; we would have to get the subject from the repo, which is
+    //    inconvenient at this point.
+    //
+    // Ignore original subject on the ReviewDb side if it equals the subject of
+    // the current patch set.
+    //
+    // For all of the above subject comparisons, first trim any leading spaces
+    // from the NoteDb strings. (We actually do represent the leading spaces
+    // faithfully during conversion, but JGit's FooterLine parser trims them
+    // when reading.)
+    //
+    // Ignore empty topic on the ReviewDb side if it is null on the NoteDb side.
+    //
+    // Use max timestamp of all ReviewDb entities when comparing with NoteDb.
+    if (bundleA.source == REVIEW_DB && bundleB.source == NOTE_DB) {
+      excludeCreatedOn = !timestampsDiffer(
+          bundleA, bundleA.getFirstPatchSetTime(), bundleB, b.getCreatedOn());
+      aSubj = s.trimLeadingFrom(aSubj);
+      excludeSubject = bSubj.startsWith(aSubj);
+      excludeOrigSubj = true;
+      excludeTopic = "".equals(a.getTopic()) && b.getTopic() == null;
+      aUpdated = bundleA.getLatestTimestamp();
+    } else if (bundleA.source == NOTE_DB && bundleB.source == REVIEW_DB) {
+      excludeCreatedOn = !timestampsDiffer(
+          bundleA, a.getCreatedOn(), bundleB, bundleB.getFirstPatchSetTime());
+      bSubj = s.trimLeadingFrom(bSubj);
+      excludeSubject = aSubj.startsWith(bSubj);
+      excludeOrigSubj = true;
+      excludeTopic = a.getTopic() == null && "".equals(b.getTopic());
+      bUpdated = bundleB.getLatestTimestamp();
+    }
+
+    String subjectField = "subject";
+    String updatedField = "lastUpdatedOn";
+    List<String> exclude = Lists.newArrayList(
+        subjectField, updatedField, "noteDbState", "rowVersion");
+    if (excludeCreatedOn) {
+      exclude.add("createdOn");
+    }
+    if (excludeOrigSubj) {
+      exclude.add("originalSubject");
+    }
+    if (excludeTopic) {
+      exclude.add("topic");
+    }
     diffColumnsExcluding(diffs, Change.class, desc, bundleA, a, bundleB, b,
-        "rowVersion", "noteDbState");
+        exclude);
+
+    // Allow last updated timestamps to either be exactly equal (within slop),
+    // or the NoteDb timestamp to be equal to the latest entity timestamp in the
+    // whole ReviewDb bundle (within slop).
+    if (timestampsDiffer(bundleA, a.getLastUpdatedOn(),
+          bundleB, b.getLastUpdatedOn())) {
+      diffTimestamps(diffs, desc, bundleA, aUpdated, bundleB, bUpdated,
+          "effective last updated time");
+    }
+    if (!excludeSubject) {
+      diffValues(diffs, desc, aSubj, bSubj, subjectField);
+    }
+  }
+
+  /**
+   * Set of fields that must always exactly match between ReviewDb and NoteDb.
+   * <p>
+   * Used to limit the worst-case quadratic search when pairing off matching
+   * messages below.
+   */
+  @AutoValue
+  abstract static class ChangeMessageCandidate {
+    static ChangeMessageCandidate create(ChangeMessage cm) {
+      return new AutoValue_ChangeBundle_ChangeMessageCandidate(
+          cm.getAuthor(),
+          cm.getMessage(),
+          cm.getTag());
+    }
+
+    @Nullable abstract Account.Id author();
+    @Nullable abstract String message();
+    @Nullable abstract String tag();
+
+    // Exclude:
+    //  - patch set, which may be null on ReviewDb side but not NoteDb
+    //  - UUID, which is always different between ReviewDb and NoteDb
+    //  - writtenOn, which is fuzzy
   }
 
   private static void diffChangeMessages(List<String> diffs,
@@ -319,9 +538,9 @@
     if (bundleA.source == REVIEW_DB && bundleB.source == REVIEW_DB) {
       // Both came from ReviewDb: check all fields exactly.
       Map<ChangeMessage.Key, ChangeMessage> as =
-          changeMessageMap(bundleA.changeMessages);
+          changeMessageMap(bundleA.filterChangeMessages());
       Map<ChangeMessage.Key, ChangeMessage> bs =
-          changeMessageMap(bundleB.changeMessages);
+          changeMessageMap(bundleB.filterChangeMessages());
 
       for (ChangeMessage.Key k : diffKeySets(diffs, as, bs)) {
         ChangeMessage a = as.get(k);
@@ -331,45 +550,109 @@
       }
       return;
     }
-
-    // At least one is from NoteDb, so we need to ignore UUIDs for both, and
-    // allow timestamp slop if the sources differ.
     Change.Id id = bundleA.getChange().getId();
     checkArgument(id.equals(bundleB.getChange().getId()));
-    List<ChangeMessage> as = bundleA.changeMessages;
-    List<ChangeMessage> bs = bundleB.changeMessages;
-    if (as.size() != bs.size()) {
-      Joiner j = Joiner.on("\n");
-      diffs.add("Differing numbers of ChangeMessages for Change.Id " + id
-          + ":\n" + j.join(as) + "\n--- vs. ---\n" + j.join(bs));
-      return;
+
+    // Try to pair up matching ChangeMessages from each side, and succeed only
+    // if both collections are empty at the end. Quadratic in the worst case,
+    // but easy to reason about.
+    List<ChangeMessage> as = new LinkedList<>(bundleA.filterChangeMessages());
+
+    Multimap<ChangeMessageCandidate, ChangeMessage> bs =
+        LinkedListMultimap.create();
+    for (ChangeMessage b : bundleB.filterChangeMessages()) {
+      bs.put(ChangeMessageCandidate.create(b), b);
     }
 
-    for (int i = 0; i < as.size(); i++) {
-      ChangeMessage a = as.get(i);
-      ChangeMessage b = bs.get(i);
-      String desc = "ChangeMessage on " + id + " at index " + i;
-      diffColumnsExcluding(diffs, ChangeMessage.class, desc, bundleA, a,
-          bundleB, b, "key");
+    Iterator<ChangeMessage> ait = as.iterator();
+    A: while (ait.hasNext()) {
+      ChangeMessage a = ait.next();
+      Iterator<ChangeMessage> bit =
+          bs.get(ChangeMessageCandidate.create(a)).iterator();
+      while (bit.hasNext()) {
+        ChangeMessage b = bit.next();
+        if (changeMessagesMatch(bundleA, a, bundleB, b)) {
+          ait.remove();
+          bit.remove();
+          continue A;
+        }
+      }
     }
+
+    if (as.isEmpty() && bs.isEmpty()) {
+      return;
+    }
+    StringBuilder sb = new StringBuilder("ChangeMessages differ for Change.Id ")
+        .append(id).append('\n');
+    if (!as.isEmpty()) {
+      sb.append("Only in A:");
+      for (ChangeMessage cm : as) {
+        sb.append("\n  ").append(cm);
+      }
+      if (!bs.isEmpty()) {
+        sb.append('\n');
+      }
+    }
+    if (!bs.isEmpty()) {
+      sb.append("Only in B:");
+      for (ChangeMessage cm : CHANGE_MESSAGE_ORDER.sortedCopy(bs.values())) {
+        sb.append("\n  ").append(cm);
+      }
+    }
+    diffs.add(sb.toString());
+  }
+
+  private static boolean changeMessagesMatch(
+      ChangeBundle bundleA, ChangeMessage a,
+      ChangeBundle bundleB, ChangeMessage b) {
+    List<String> tempDiffs = new ArrayList<>();
+    String temp = "temp";
+
+    boolean excludePatchSet = false;
+    if (bundleA.source == REVIEW_DB && bundleB.source == NOTE_DB) {
+      excludePatchSet = a.getPatchSetId() == null;
+    } else if (bundleA.source == NOTE_DB && bundleB.source == REVIEW_DB) {
+      excludePatchSet = b.getPatchSetId() == null;
+    }
+
+    List<String> exclude = Lists.newArrayList("key");
+    if (excludePatchSet) {
+      exclude.add("patchset");
+    }
+
+    diffColumnsExcluding(
+        tempDiffs, ChangeMessage.class, temp, bundleA, a, bundleB, b, exclude);
+    return tempDiffs.isEmpty();
   }
 
   private static void diffPatchSets(List<String> diffs, ChangeBundle bundleA,
       ChangeBundle bundleB) {
-    Map<PatchSet.Id, PatchSet> as = bundleA.patchSets;
-    Map<PatchSet.Id, PatchSet> bs = bundleB.patchSets;
+    Map<PatchSet.Id, PatchSet> as = bundleA.filterPatchSets();
+    Map<PatchSet.Id, PatchSet> bs = bundleB.filterPatchSets();
     for (PatchSet.Id id : diffKeySets(diffs, as, bs)) {
       PatchSet a = as.get(id);
       PatchSet b = bs.get(id);
       String desc = describe(id);
-      diffColumns(diffs, PatchSet.class, desc, bundleA, a, bundleB, b);
+      String pushCertField = "pushCertificate";
+      diffColumnsExcluding(diffs, PatchSet.class, desc, bundleA, a, bundleB, b,
+          pushCertField);
+      diffValues(diffs, desc, trimPushCert(a), trimPushCert(b), pushCertField);
     }
   }
 
+  private static String trimPushCert(PatchSet ps) {
+    if (ps.getPushCertificate() == null) {
+      return null;
+    }
+    return CharMatcher.is('\n').trimTrailingFrom(ps.getPushCertificate());
+  }
+
   private static void diffPatchSetApprovals(List<String> diffs,
       ChangeBundle bundleA, ChangeBundle bundleB) {
-    Map<PatchSetApproval.Key, PatchSetApproval> as = bundleA.patchSetApprovals;
-    Map<PatchSetApproval.Key, PatchSetApproval> bs = bundleB.patchSetApprovals;
+    Map<PatchSetApproval.Key, PatchSetApproval> as =
+          bundleA.filterPatchSetApprovals();
+    Map<PatchSetApproval.Key, PatchSetApproval> bs =
+        bundleB.filterPatchSetApprovals();
     for (PatchSetApproval.Key k : diffKeySets(diffs, as, bs)) {
       PatchSetApproval a = as.get(k);
       PatchSetApproval b = bs.get(k);
@@ -380,8 +663,10 @@
 
   private static void diffPatchLineComments(List<String> diffs,
       ChangeBundle bundleA, ChangeBundle bundleB) {
-    Map<PatchLineComment.Key, PatchLineComment> as = bundleA.patchLineComments;
-    Map<PatchLineComment.Key, PatchLineComment> bs = bundleB.patchLineComments;
+    Map<PatchLineComment.Key, PatchLineComment> as =
+        bundleA.filterPatchLineComments();
+    Map<PatchLineComment.Key, PatchLineComment> bs =
+        bundleB.filterPatchLineComments();
     for (PatchLineComment.Key k : diffKeySets(diffs, as, bs)) {
       PatchLineComment a = as.get(k);
       PatchLineComment b = bs.get(k);
@@ -417,7 +702,14 @@
   private static <T> void diffColumnsExcluding(List<String> diffs,
       Class<T> clazz, String desc, ChangeBundle bundleA, T a,
       ChangeBundle bundleB, T b, String... exclude) {
-    Set<String> toExclude = Sets.newLinkedHashSet(Arrays.asList(exclude));
+    diffColumnsExcluding(diffs, clazz, desc, bundleA, a, bundleB, b,
+        Arrays.asList(exclude));
+  }
+
+  private static <T> void diffColumnsExcluding(List<String> diffs,
+      Class<T> clazz, String desc, ChangeBundle bundleA, T a,
+      ChangeBundle bundleB, T b, Iterable<String> exclude) {
+    Set<String> toExclude = Sets.newLinkedHashSet(exclude);
     for (Field f : clazz.getDeclaredFields()) {
       Column col = f.getAnnotation(Column.class);
       if (col == null) {
@@ -447,7 +739,8 @@
     checkArgument(a.getClass() == b.getClass());
     Class<?> clazz = a.getClass();
 
-    Timestamp ta, tb;
+    Timestamp ta;
+    Timestamp tb;
     try {
       Field f = clazz.getDeclaredField(field);
       checkArgument(f.getAnnotation(Column.class) != null);
@@ -458,23 +751,56 @@
         | SecurityException e) {
       throw new IllegalArgumentException(e);
     }
-    if (bundleA.source == bundleB.source || ta == null || tb == null) {
-      diffValues(diffs, desc, ta, tb, field);
-    } else if (bundleA.source == NOTE_DB) {
-      diffTimestamps(diffs, desc, ta, tb, field);
-    } else {
-      diffTimestamps(diffs, desc, tb, ta, field);
-    }
+    diffTimestamps(diffs, desc, bundleA, ta, bundleB, tb, field);
   }
 
   private static void diffTimestamps(List<String> diffs, String desc,
-      Timestamp tsFromNoteDb, Timestamp tsFromReviewDb, String field) {
+      ChangeBundle bundleA, Timestamp ta, ChangeBundle bundleB, Timestamp tb,
+      String fieldDesc) {
+    if (bundleA.source == bundleB.source || ta == null || tb == null) {
+      diffValues(diffs, desc, ta, tb, fieldDesc);
+    } else if (bundleA.source == NOTE_DB) {
+      diffTimestamps(
+          diffs, desc,
+          bundleA.getChange(), ta,
+          bundleB.getChange(), tb,
+          fieldDesc);
+    } else {
+      diffTimestamps(
+          diffs, desc,
+          bundleB.getChange(), tb,
+          bundleA.getChange(), ta,
+          fieldDesc);
+    }
+  }
+
+  private static boolean timestampsDiffer(ChangeBundle bundleA, Timestamp ta,
+      ChangeBundle bundleB, Timestamp tb) {
+    List<String> tempDiffs = new ArrayList<>(1);
+    diffTimestamps(tempDiffs, "temp", bundleA, ta, bundleB, tb, "temp");
+    return !tempDiffs.isEmpty();
+  }
+
+  private static void diffTimestamps(List<String> diffs, String desc,
+      Change changeFromNoteDb, Timestamp tsFromNoteDb,
+      Change changeFromReviewDb, Timestamp tsFromReviewDb,
+      String field) {
     // Because ChangeRebuilder may batch events together that are several
     // seconds apart, the timestamp in NoteDb may actually be several seconds
     // *earlier* than the timestamp in ReviewDb that it was converted from.
     checkArgument(tsFromNoteDb.equals(roundToSecond(tsFromNoteDb)),
         "%s from NoteDb has non-rounded %s timestamp: %s",
         desc, field, tsFromNoteDb);
+
+    if (tsFromReviewDb.before(changeFromReviewDb.getCreatedOn())
+        && tsFromNoteDb.equals(changeFromNoteDb.getCreatedOn())) {
+      // Timestamp predates change creation. These are truncated to change
+      // creation time during NoteDb conversion, so allow this if the timestamp
+      // in NoteDb matches the createdOn time in NoteDb.
+      return;
+    }
+
+
     long delta = tsFromReviewDb.getTime() - tsFromNoteDb.getTime();
     long max = ChangeRebuilderImpl.MAX_WINDOW_MS;
     if (delta < 0 || delta > max) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
index 2644528..84e647e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
@@ -16,7 +16,6 @@
 
 import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.base.Preconditions.checkNotNull;
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 
 import com.google.auto.value.AutoValue;
@@ -42,9 +41,11 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Date;
-import java.util.HashMap;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
@@ -64,7 +65,7 @@
   }
 
   @AutoValue
-  static abstract class Key {
+  abstract static class Key {
     abstract RevId revId();
     abstract PatchLineComment.Key key();
   }
@@ -75,8 +76,7 @@
 
   private final AllUsersName draftsProject;
 
-  // TODO: can go back to a list?
-  private Map<Key, PatchLineComment> put;
+  private List<PatchLineComment> put;
   private Set<Key> delete;
 
   @AssistedInject
@@ -93,7 +93,7 @@
     super(migration, noteUtil, serverIdent, anonymousCowardName, notes,
         accountId, authorIdent, when);
     this.draftsProject = allUsers;
-    this.put = new HashMap<>();
+    this.put = new ArrayList<>();
     this.delete = new HashSet<>();
   }
 
@@ -101,7 +101,7 @@
     verifyComment(c);
     checkArgument(c.getStatus() == PatchLineComment.Status.DRAFT,
         "Cannot insert a published comment into a ChangeDraftUpdate");
-    put.put(key(c), c);
+    put.add(c);
   }
 
   public void deleteComment(PatchLineComment c) {
@@ -119,15 +119,15 @@
         + " this ChangeDraftUpdate (%s): %s", accountId, comment);
   }
 
-  /** @return the tree id for the updated tree */
-  private ObjectId storeCommentsInNotes(RevWalk rw, ObjectInserter ins,
-      ObjectId curr) throws ConfigInvalidException, OrmException, IOException {
+  private CommitBuilder storeCommentsInNotes(RevWalk rw, ObjectInserter ins,
+      ObjectId curr, CommitBuilder cb)
+      throws ConfigInvalidException, OrmException, IOException {
     RevisionNoteMap rnm = getRevisionNoteMap(rw, curr);
     Set<RevId> updatedRevs =
         Sets.newHashSetWithExpectedSize(rnm.revisionNotes.size());
     RevisionNoteBuilder.Cache cache = new RevisionNoteBuilder.Cache(rnm);
 
-    for (PatchLineComment c : put.values()) {
+    for (PatchLineComment c : put) {
       if (!delete.contains(key(c))) {
         cache.get(c.getRevId()).putComment(c);
       }
@@ -137,11 +137,15 @@
     }
 
     Map<RevId, RevisionNoteBuilder> builders = cache.getBuilders();
+    boolean touchedAnyRevs = false;
     boolean hasComments = false;
     for (Map.Entry<RevId, RevisionNoteBuilder> e : builders.entrySet()) {
       updatedRevs.add(e.getKey());
       ObjectId id = ObjectId.fromString(e.getKey().get());
       byte[] data = e.getValue().build(noteUtil);
+      if (!Arrays.equals(data, e.getValue().baseRaw)) {
+        touchedAnyRevs = true;
+      }
       if (data.length == 0) {
         rnm.noteMap.remove(id);
       } else {
@@ -151,6 +155,13 @@
       }
     }
 
+    // If we didn't touch any notes, tell the caller this was a no-op update. We
+    // couldn't have done this in isEmpty() below because we hadn't read the old
+    // data yet.
+    if (!touchedAnyRevs) {
+      return NO_OP_UPDATE;
+    }
+
     // If we touched every revision and there are no comments left, tell the
     // caller to delete the entire ref.
     boolean touchedAllRevs = updatedRevs.equals(rnm.revisionNotes.keySet());
@@ -158,7 +169,8 @@
       return null;
     }
 
-    return rnm.noteMap.writeTree(ins);
+    cb.setTreeId(rnm.noteMap.writeTree(ins));
+    return cb;
   }
 
   private RevisionNoteMap getRevisionNoteMap(RevWalk rw, ObjectId curr)
@@ -172,8 +184,9 @@
       if (draftNotes != null) {
         ObjectId idFromNotes =
             firstNonNull(draftNotes.getRevision(), ObjectId.zeroId());
-        if (idFromNotes.equals(curr)) {
-          return checkNotNull(getNotes().revisionNoteMap);
+        RevisionNoteMap rnm = draftNotes.getRevisionNoteMap();
+        if (idFromNotes.equals(curr) && rnm != null) {
+          return rnm;
         }
       }
     }
@@ -195,15 +208,10 @@
     CommitBuilder cb = new CommitBuilder();
     cb.setMessage("Update draft comments");
     try {
-      ObjectId treeId = storeCommentsInNotes(rw, ins, curr);
-      if (treeId == null) {
-        return null; // Delete ref.
-      }
-      cb.setTreeId(checkNotNull(treeId));
+      return storeCommentsInNotes(rw, ins, curr, cb);
     } catch (ConfigInvalidException e) {
       throw new OrmException(e);
     }
-    return cb;
   }
 
   @Override
@@ -213,7 +221,7 @@
 
   @Override
   protected String getRefName() {
-    return RefNames.refsDraftComments(accountId, getId());
+    return RefNames.refsDraftComments(getId(), accountId);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
index 40bbb29..51155cd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
@@ -15,12 +15,13 @@
 package com.google.gerrit.server.notedb;
 
 import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.gerrit.server.PatchLineCommentsUtil.PLC_ORDER;
 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.ImmutableList;
-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;
@@ -29,10 +30,9 @@
 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.RefNames;
 import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.reviewdb.server.ReviewDbUtil;
 import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.PatchLineCommentsUtil;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.GerritServerId;
@@ -48,15 +48,17 @@
 import org.eclipse.jgit.util.QuotedString;
 import org.eclipse.jgit.util.RawParseUtils;
 
-import java.io.ByteArrayOutputStream;
 import java.io.OutputStream;
 import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
 import java.sql.Timestamp;
 import java.text.ParseException;
+import java.util.ArrayList;
 import java.util.Date;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
+import java.util.Set;
 
 public class ChangeNoteUtil {
   static final FooterKey FOOTER_BRANCH = new FooterKey("Branch");
@@ -72,6 +74,7 @@
   static final FooterKey FOOTER_SUBMITTED_WITH =
       new FooterKey("Submitted-with");
   static final FooterKey FOOTER_TOPIC = new FooterKey("Topic");
+  static final FooterKey FOOTER_TAG = new FooterKey("Tag");
 
   private static final String AUTHOR = "Author";
   private static final String BASE_PATCH_SET = "Base-for-patch-set";
@@ -82,21 +85,7 @@
   private static final String PATCH_SET = "Patch-set";
   private static final String REVISION = "Revision";
   private static final String UUID = "UUID";
-
-  public static String changeRefName(Change.Id id) {
-    StringBuilder r = new StringBuilder();
-    r.append(RefNames.REFS_CHANGES);
-    int n = id.get();
-    int m = n % 100;
-    if (m < 10) {
-      r.append('0');
-    }
-    r.append(m);
-    r.append('/');
-    r.append(n);
-    r.append(RefNames.META_SUFFIX);
-    return r.toString();
-  }
+  private static final String TAG = FOOTER_TAG.getName();
 
   public static String formatTime(PersonIdent ident, Timestamp t) {
     GitDateFormatter dateFormatter = new GitDateFormatter(Format.DEFAULT);
@@ -147,28 +136,50 @@
         serverId, email);
   }
 
+  private static boolean match(byte[] note, MutableInteger p, byte[] expected) {
+    int m = RawParseUtils.match(note, p.value, expected);
+    return m == p.value + expected.length;
+  }
+
   public List<PatchLineComment> parseNote(byte[] note, MutableInteger p,
       Change.Id changeId, Status status) throws ConfigInvalidException {
     if (p.value >= note.length) {
       return ImmutableList.of();
     }
-    List<PatchLineComment> result = Lists.newArrayList();
+    Set<PatchLineComment.Key> seen = new HashSet<>();
+    List<PatchLineComment> result = new ArrayList<>();
     int sizeOfNote = note.length;
+    byte[] psb = PATCH_SET.getBytes(UTF_8);
+    byte[] bpsb = BASE_PATCH_SET.getBytes(UTF_8);
 
-    boolean isForBase =
-        (RawParseUtils.match(note, p.value, PATCH_SET.getBytes(UTF_8))) < 0;
+    RevId revId = new RevId(parseStringField(note, p, changeId, REVISION));
+    String fileName = null;
+    PatchSet.Id psId = null;
+    boolean isForBase = false;
 
-    PatchSet.Id psId = parsePsId(note, p, changeId, isForBase ? BASE_PATCH_SET : PATCH_SET);
-
-    RevId revId =
-        new RevId(parseStringField(note, p, changeId, REVISION));
-
-    PatchLineComment c = null;
     while (p.value < sizeOfNote) {
-      String previousFileName = c == null ?
-          null : c.getKey().getParentKey().getFileName();
-      c = parseComment(note, p, previousFileName, psId, revId,
-          isForBase, status);
+      boolean matchPs = match(note, p, psb);
+      boolean matchBase = match(note, p, bpsb);
+      if (matchPs) {
+        fileName = null;
+        psId = parsePsId(note, p, changeId, PATCH_SET);
+        isForBase = false;
+      } else if (matchBase) {
+        fileName = null;
+        psId = parsePsId(note, p, changeId, BASE_PATCH_SET);
+        isForBase = true;
+      } else if (psId == null) {
+        throw parseException(changeId, "missing %s or %s header",
+            PATCH_SET, BASE_PATCH_SET);
+      }
+
+      PatchLineComment c =
+          parseComment(note, p, fileName, psId, revId, isForBase, status);
+      fileName = c.getKey().getParentKey().getFileName();
+      if (!seen.add(c.getKey())) {
+        throw parseException(
+            changeId, "multiple comments for %s in note", c.getKey());
+      }
       result.add(c);
     }
     return result;
@@ -205,6 +216,14 @@
     }
 
     String uuid = parseStringField(note, curr, changeId, UUID);
+
+    boolean hasTag =
+        (RawParseUtils.match(note, curr.value, TAG.getBytes(UTF_8))) != -1;
+    String tag = null;
+    if (hasTag) {
+      tag = parseStringField(note, curr, changeId, TAG);
+    }
+
     int commentLength = parseCommentLength(note, curr, changeId);
 
     String message = RawParseUtils.decode(
@@ -215,6 +234,7 @@
         new PatchLineComment.Key(new Patch.Key(psId, currentFileName), uuid),
         range.getEndLine(), aId, parentUUID, commentTime);
     plc.setMessage(message);
+    plc.setTag(tag);
     plc.setSide((short) (isForBase ? 0 : 1));
     if (range.getStartCharacter() != -1) {
       plc.setRange(range);
@@ -246,8 +266,11 @@
   private static CommentRange parseCommentRange(byte[] note, MutableInteger ptr) {
     CommentRange range = new CommentRange(-1, -1, -1, -1);
 
+    int last = ptr.value;
     int startLine = RawParseUtils.parseBase10(note, ptr.value, ptr);
-    if (startLine == 0) {
+    if (ptr.value == last) {
+      return null;
+    } else if (startLine == 0) {
       range.setEndLine(0);
       ptr.value += 1;
       return range;
@@ -264,30 +287,33 @@
       return null;
     }
 
+    last = ptr.value;
     int startChar = RawParseUtils.parseBase10(note, ptr.value, ptr);
-    if (note[ptr.value] == '-') {
+    if (ptr.value == last) {
+      return null;
+    } else if (note[ptr.value] == '-') {
       range.setStartCharacter(startChar);
       ptr.value += 1;
     } else {
       return null;
     }
 
+    last = ptr.value;
     int endLine = RawParseUtils.parseBase10(note, ptr.value, ptr);
-    if (endLine == 0) {
+    if (ptr.value == last) {
       return null;
-    }
-    if (note[ptr.value] == ':') {
+    } else if (note[ptr.value] == ':') {
       range.setEndLine(endLine);
       ptr.value += 1;
     } else {
       return null;
     }
 
+    last = ptr.value;
     int endChar = RawParseUtils.parseBase10(note, ptr.value, ptr);
-    if (endChar == 0) {
+    if (ptr.value == last) {
       return null;
-    }
-    if (note[ptr.value] == '\n') {
+    } else if (note[ptr.value] == '\n') {
       range.setEndCharacter(endChar);
       ptr.value += 1;
     } else {
@@ -359,11 +385,15 @@
     int startOfLength =
         RawParseUtils.endOfFooterLineKey(note, curr.value) + 1;
     MutableInteger i = new MutableInteger();
+    i.value = startOfLength;
     int commentLength =
         RawParseUtils.parseBase10(note, startOfLength, i);
+    if (i.value == startOfLength) {
+      throw parseException(changeId, "could not parse %s", LENGTH);
+    }
     int endOfLine = RawParseUtils.nextLF(note, curr.value);
-    if (i.value != endOfLine-1) {
-      throw parseException(changeId, "could not parse %s", PATCH_SET);
+    if (i.value != endOfLine - 1) {
+      throw parseException(changeId, "could not parse %s", LENGTH);
     }
     curr.value = endOfLine;
     return checkResult(commentLength, "comment length", changeId);
@@ -406,101 +436,116 @@
     }
   }
 
-  public byte[] buildNote(List<PatchLineComment> comments) {
-    ByteArrayOutputStream out = new ByteArrayOutputStream();
-    buildNote(comments, out);
-    return out.toByteArray();
-  }
-
   /**
    * Build a note that contains the metadata for and the contents of all of the
-   * comments in the given list of comments.
+   * comments in the given comments.
    *
-   * @param comments A list of the comments to be written to the
-   *            output stream. All of the comments in this list must have the
-   *            same side and must share the same patch set ID.
+   * @param comments Comments to be written to the output stream, keyed by patch
+   *     set ID; multiple patch sets are allowed since base revisions may be
+   *     shared across patch sets. All of the comments must share the same
+   *     RevId, and all the comments for a given patch set must have the same
+   *     side.
    * @param out output stream to write to.
    */
-  void buildNote(List<PatchLineComment> comments, OutputStream out) {
+  void buildNote(Multimap<PatchSet.Id, PatchLineComment> comments,
+      OutputStream out) {
     if (comments.isEmpty()) {
       return;
     }
+
+    List<PatchSet.Id> psIds =
+        ReviewDbUtil.intKeyOrdering().sortedCopy(comments.keySet());
+
     OutputStreamWriter streamWriter = new OutputStreamWriter(out, UTF_8);
     try (PrintWriter writer = new PrintWriter(streamWriter)) {
-      PatchLineComment first = comments.get(0);
+      RevId revId = comments.values().iterator().next().getRevId();
+      appendHeaderField(writer, REVISION, revId.get());
 
-      short side = first.getSide();
-      PatchSet.Id psId = PatchLineCommentsUtil.getCommentPsId(first);
-      appendHeaderField(writer, side == 0
-          ? BASE_PATCH_SET
-          : PATCH_SET,
-          Integer.toString(psId.get()));
-      appendHeaderField(writer, REVISION, first.getRevId().get());
+      for (PatchSet.Id psId : psIds) {
+        List<PatchLineComment> psComments =
+            PLC_ORDER.sortedCopy(comments.get(psId));
+        PatchLineComment first = psComments.get(0);
 
-      String currentFilename = null;
+        short side = first.getSide();
+        appendHeaderField(writer, side == 0
+            ? BASE_PATCH_SET
+            : PATCH_SET,
+            Integer.toString(psId.get()));
 
-      for (PatchLineComment c : comments) {
-        PatchSet.Id currentPsId = PatchLineCommentsUtil.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 "
-            + "(%s).\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 "
-            + "(%s).\n%s", side, c.toString());
-        String commentFilename =
-            QuotedString.GIT_PATH.quote(c.getKey().getParentKey().getFileName());
+        String currentFilename = null;
 
-        if (!commentFilename.equals(currentFilename)) {
-          currentFilename = commentFilename;
-          writer.print("File: ");
-          writer.print(commentFilename);
-          writer.print("\n\n");
+        for (PatchLineComment c : psComments) {
+          checkArgument(revId.equals(c.getRevId()),
+              "All comments being added must have all the same RevId. The "
+              + "comment below does not have the same RevId as the others "
+              + "(%s).\n%s", revId, c);
+          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 "
+              + "(%s).\n%s", side, c);
+          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");
+          }
+
+          appendOneComment(writer, c);
         }
-
-        // 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(), serverIdent, anonymousCowardName);
-        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");
       }
     }
   }
+
+  private void appendOneComment(PrintWriter writer, PatchLineComment c) {
+    // The CommentRange field for a comment is allowed to be null. If it is
+    // 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(), serverIdent, anonymousCowardName);
+    StringBuilder name = new StringBuilder();
+    PersonIdent.appendSanitized(name, ident.getName());
+    name.append(" <");
+    PersonIdent.appendSanitized(name, ident.getEmailAddress());
+    name.append('>');
+    appendHeaderField(writer, AUTHOR, name.toString());
+
+    String parent = c.getParentUuid();
+    if (parent != null) {
+      appendHeaderField(writer, PARENT, parent);
+    }
+
+    appendHeaderField(writer, UUID, c.getKey().get());
+
+    if (c.getTag() != null) {
+      appendHeaderField(writer, TAG, c.getTag());
+    }
+
+    byte[] messageBytes = c.getMessage().getBytes(UTF_8);
+    appendHeaderField(writer, LENGTH,
+        Integer.toString(messageBytes.length));
+
+    writer.print(c.getMessage());
+    writer.print("\n\n");
+  }
 }
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 a6bf5d3..a6cd8fa 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
@@ -17,6 +17,7 @@
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Function;
@@ -40,6 +41,7 @@
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
@@ -53,6 +55,8 @@
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.reviewdb.server.ReviewDbUtil;
+import com.google.gerrit.server.git.RefCache;
+import com.google.gerrit.server.git.RepoRefCache;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -67,7 +71,6 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevWalk;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -161,6 +164,8 @@
     public ChangeNotes create(ReviewDb db, Project.NameKey project,
         Change.Id changeId) throws OrmException {
       Change change = unwrap(db).changes().get(changeId);
+      checkNotNull(change,
+          "change %s not found in ReviewDb", changeId);
       checkArgument(change.getProject().equals(project),
           "passed project %s when creating ChangeNotes for %s, but actual"
           + " project is %s",
@@ -189,15 +194,18 @@
     // TODO(dborowitz): Remove when deleting index schemas <27.
     public ChangeNotes createFromIdOnlyWhenNoteDbDisabled(
         ReviewDb db, Change.Id changeId) throws OrmException {
-    checkState(!args.migration.readChanges(), "do not call"
-        + " createFromIdOnlyWhenNoteDbDisabled when NoteDb is enabled");
+      checkState(!args.migration.readChanges(), "do not call"
+          + " createFromIdOnlyWhenNoteDbDisabled when NoteDb is enabled");
       Change change = unwrap(db).changes().get(changeId);
+      checkNotNull(change,
+          "change %s not found in ReviewDb", changeId);
       return new ChangeNotes(args, change.getProject(), change).load();
     }
 
-    public ChangeNotes createWithAutoRebuildingDisabled(Change change)
-        throws OrmException {
-      return new ChangeNotes(args, change.getProject(), change, false).load();
+    public ChangeNotes createWithAutoRebuildingDisabled(Change change,
+        RefCache refs) throws OrmException {
+      return new ChangeNotes(args, change.getProject(), change, false, refs)
+          .load();
     }
 
     // TODO(ekempin): Remove when database backend is deleted
@@ -369,7 +377,8 @@
 
   private final Project.NameKey project;
   private final Change change;
-  private final boolean autoRebuild;
+  private final RefCache refs;
+
   private ImmutableSortedMap<PatchSet.Id, PatchSet> patchSets;
   private ImmutableListMultimap<PatchSet.Id, PatchSetApproval> approvals;
   private ImmutableSetMultimap<ReviewerStateInternal, Account.Id> reviewers;
@@ -388,15 +397,15 @@
 
   @VisibleForTesting
   public ChangeNotes(Args args, Project.NameKey project, Change change) {
-    this(args, project, change, true);
+    this(args, project, change, true, null);
   }
 
   private ChangeNotes(Args args, Project.NameKey project, Change change,
-      boolean autoRebuild) {
-    super(args, change != null ? change.getId() : null);
+      boolean autoRebuild, @Nullable RefCache refs) {
+    super(args, change.getId(), autoRebuild);
     this.project = project;
-    this.change = change != null ? new Change(change) : null;
-    this.autoRebuild = autoRebuild;
+    this.change = new Change(change);
+    this.refs = refs;
   }
 
   public Change getChange() {
@@ -491,7 +500,8 @@
       throws OrmException {
     if (draftCommentNotes == null ||
         !author.equals(draftCommentNotes.getAuthor())) {
-      draftCommentNotes = new DraftCommentNotes(args, change, author);
+      draftCommentNotes =
+          new DraftCommentNotes(args, change, author, autoRebuild);
       draftCommentNotes.load();
     }
   }
@@ -520,7 +530,7 @@
 
   @Override
   protected String getRefName() {
-    return ChangeNoteUtil.changeRefName(getChangeId());
+    return changeMetaRef(getChangeId());
   }
 
   public PatchSet getCurrentPatchSet() {
@@ -537,54 +547,52 @@
       loadDefaults();
       return;
     }
-    try (ChangeNotesParser parser = new ChangeNotesParser(
-         project, change.getId(), rev, handle.walk(), args.repoManager,
-         args.noteUtil, args.metrics)) {
-      parser.parseAll();
+    ChangeNotesParser parser = new ChangeNotesParser(
+        change.getId(), rev, handle.walk(), args.noteUtil, args.metrics);
+    parser.parseAll();
 
-      if (parser.status != null) {
-        change.setStatus(parser.status);
-      }
-      approvals = parser.buildApprovals();
-      changeMessagesByPatchSet = parser.buildMessagesByPatchSet();
-      allChangeMessages = parser.buildAllMessages();
-      comments = ImmutableListMultimap.copyOf(parser.comments);
-      revisionNoteMap = parser.revisionNoteMap;
-      change.setKey(new Change.Key(parser.changeId));
-      change.setDest(new Branch.NameKey(project, parser.branch));
-      change.setTopic(Strings.emptyToNull(parser.topic));
-      change.setCreatedOn(parser.createdOn);
-      change.setLastUpdatedOn(parser.lastUpdatedOn);
-      change.setOwner(parser.ownerId);
-      change.setSubmissionId(parser.submissionId);
-      patchSets = ImmutableSortedMap.copyOf(
-          parser.patchSets, ReviewDbUtil.intKeyOrdering());
-
-      if (!patchSets.isEmpty()) {
-        change.setCurrentPatchSet(
-            parser.currentPatchSetId, parser.subject, parser.originalSubject);
-      } else {
-        // TODO(dborowitz): This should be an error, but for now it's required
-        // for some tests to pass.
-        change.clearCurrentPatchSet();
-      }
-
-      if (parser.hashtags != null) {
-        hashtags = ImmutableSet.copyOf(parser.hashtags);
-      } else {
-        hashtags = ImmutableSet.of();
-      }
-      ImmutableSetMultimap.Builder<ReviewerStateInternal, Account.Id> reviewers =
-          ImmutableSetMultimap.builder();
-      for (Map.Entry<Account.Id, ReviewerStateInternal> e
-          : parser.reviewers.entrySet()) {
-        reviewers.put(e.getValue(), e.getKey());
-      }
-      this.reviewers = reviewers.build();
-      this.allPastReviewers = ImmutableList.copyOf(parser.allPastReviewers);
-
-      submitRecords = ImmutableList.copyOf(parser.submitRecords);
+    if (parser.status != null) {
+      change.setStatus(parser.status);
     }
+    approvals = parser.buildApprovals();
+    changeMessagesByPatchSet = parser.buildMessagesByPatchSet();
+    allChangeMessages = parser.buildAllMessages();
+    comments = ImmutableListMultimap.copyOf(parser.comments);
+    revisionNoteMap = parser.revisionNoteMap;
+    change.setKey(new Change.Key(parser.changeId));
+    change.setDest(new Branch.NameKey(project, parser.branch));
+    change.setTopic(Strings.emptyToNull(parser.topic));
+    change.setCreatedOn(parser.createdOn);
+    change.setLastUpdatedOn(parser.lastUpdatedOn);
+    change.setOwner(parser.ownerId);
+    change.setSubmissionId(parser.submissionId);
+    patchSets = ImmutableSortedMap.copyOf(
+        parser.patchSets, ReviewDbUtil.intKeyOrdering());
+
+    if (!patchSets.isEmpty()) {
+      change.setCurrentPatchSet(
+          parser.currentPatchSetId, parser.subject, parser.originalSubject);
+    } else {
+      // TODO(dborowitz): This should be an error, but for now it's required for
+      // some tests to pass.
+      change.clearCurrentPatchSet();
+    }
+
+    if (parser.hashtags != null) {
+      hashtags = ImmutableSet.copyOf(parser.hashtags);
+    } else {
+      hashtags = ImmutableSet.of();
+    }
+    ImmutableSetMultimap.Builder<ReviewerStateInternal, Account.Id> reviewers =
+        ImmutableSetMultimap.builder();
+    for (Map.Entry<Account.Id, ReviewerStateInternal> e
+        : parser.reviewers.entrySet()) {
+      reviewers.put(e.getValue(), e.getKey());
+    }
+    this.reviewers = reviewers.build();
+    this.allPastReviewers = ImmutableList.copyOf(parser.allPastReviewers);
+
+    submitRecords = ImmutableList.copyOf(parser.submitRecords);
   }
 
   @Override
@@ -596,6 +604,8 @@
     changeMessagesByPatchSet = ImmutableListMultimap.of();
     comments = ImmutableListMultimap.of();
     hashtags = ImmutableSet.of();
+    patchSets = ImmutableSortedMap.of();
+    allPastReviewers = ImmutableList.of();
   }
 
   @Override
@@ -604,27 +614,41 @@
   }
 
   @Override
+  protected ObjectId readRef(Repository repo) throws IOException {
+    return refs != null
+        ? refs.get(getRefName()).orNull()
+        : super.readRef(repo);
+  }
+
+  @Override
   protected LoadHandle openHandle(Repository repo) throws IOException {
     if (autoRebuild) {
       NoteDbChangeState state = NoteDbChangeState.parse(change);
-      if (state == null || !state.isChangeUpToDate(repo)) {
-        return rebuildAndOpen(repo);
+      ObjectId id = readRef(repo);
+      if (state == null && id == null) {
+        return super.openHandle(repo, id);
+      }
+      RefCache refs = this.refs != null ? this.refs : new RepoRefCache(repo);
+      if (!NoteDbChangeState.isChangeUpToDate(state, refs, getChangeId())) {
+        return rebuildAndOpen(repo, id);
       }
     }
     return super.openHandle(repo);
   }
 
-  private LoadHandle rebuildAndOpen(Repository repo) throws IOException {
+  private LoadHandle rebuildAndOpen(Repository repo, ObjectId oldId)
+      throws IOException {
     try {
       NoteDbChangeState newState =
           args.rebuilder.get().rebuild(args.db.get(), getChangeId());
       if (newState == null) {
-        return super.openHandle(repo); // May be null in tests.
+        return super.openHandle(repo, oldId); // May be null in tests.
       }
       repo.scanForRepoChanges();
-      return LoadHandle.create(new RevWalk(repo), newState.getChangeMetaId());
+      return LoadHandle.create(
+          ChangeNotesCommit.newRevWalk(repo), newState.getChangeMetaId());
     } catch (NoSuchChangeException e) {
-      return super.openHandle(repo);
+      return super.openHandle(repo, oldId);
     } catch (OrmException | ConfigInvalidException e) {
       throw new IOException(e);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesCommit.java
new file mode 100644
index 0000000..5d28454
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesCommit.java
@@ -0,0 +1,106 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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 com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ListMultimap;
+
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.FooterKey;
+import org.eclipse.jgit.revwalk.FooterLine;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Commit implementation with some optimizations for change notes parsing.
+ * <p>
+ * <ul>
+ *   <li>Caches the result of {@link #getFooterLines()}, which is
+ *     otherwise very wasteful with allocations.</li>
+ * </ul>
+ */
+public class ChangeNotesCommit extends RevCommit {
+  public static ChangeNotesRevWalk newRevWalk(Repository repo) {
+    return new ChangeNotesRevWalk(repo);
+  }
+
+  public static class ChangeNotesRevWalk extends RevWalk {
+    private ChangeNotesRevWalk(Repository repo) {
+      super(repo);
+    }
+
+    @Override
+    protected ChangeNotesCommit createCommit(AnyObjectId id) {
+      return new ChangeNotesCommit(id);
+    }
+
+    @Override
+    public ChangeNotesCommit next() throws MissingObjectException,
+         IncorrectObjectTypeException, IOException {
+      return (ChangeNotesCommit) super.next();
+    }
+
+    @Override
+    public void markStart(RevCommit c) throws MissingObjectException,
+        IncorrectObjectTypeException, IOException {
+      checkArgument(c instanceof ChangeNotesCommit);
+      super.markStart(c);
+    }
+
+    @Override
+    public void markUninteresting(RevCommit c) throws MissingObjectException,
+        IncorrectObjectTypeException, IOException {
+      checkArgument(c instanceof ChangeNotesCommit);
+      super.markUninteresting(c);
+    }
+
+    @Override
+    public ChangeNotesCommit lookupCommit(AnyObjectId id) {
+      return (ChangeNotesCommit) super.lookupCommit(id);
+    }
+
+    @Override
+    public ChangeNotesCommit parseCommit(AnyObjectId id)
+        throws MissingObjectException, IncorrectObjectTypeException,
+        IOException {
+      return (ChangeNotesCommit) super.parseCommit(id);
+    }
+  }
+
+  private ListMultimap<String, String> footerLines;
+
+  public ChangeNotesCommit(AnyObjectId id) {
+    super(id);
+  }
+
+  public List<String> getFooterLineValues(FooterKey key) {
+    if (footerLines == null) {
+      List<FooterLine> src = getFooterLines();
+      footerLines = ArrayListMultimap.create(src.size(), 1);
+      for (FooterLine fl : src) {
+        footerLines.put(fl.getKey().toLowerCase(), fl.getValue());
+      }
+    }
+    return footerLines.get(key.getName().toLowerCase());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
index 7bf7960..9d9e180 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -25,6 +25,7 @@
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBJECT;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBMISSION_ID;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBMITTED_WITH;
+import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_TAG;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_TOPIC;
 import static com.google.gerrit.server.notedb.NoteDbTable.CHANGES;
 
@@ -56,24 +57,19 @@
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 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.RefNames;
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDbUtil;
-import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
 import com.google.gerrit.server.util.LabelVote;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.InvalidObjectIdException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectReader;
 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;
@@ -82,14 +78,17 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.Iterator;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.NavigableSet;
 import java.util.Set;
 import java.util.TreeMap;
 
-class ChangeNotesParser implements AutoCloseable {
+class ChangeNotesParser {
   // Sentinel RevId indicating a mutable field on a patch set was parsed, but
   // the parser does not yet know its commit SHA-1.
   private static final RevId PARTIAL_PATCH_SET =
@@ -113,6 +112,7 @@
   String subject;
   String originalSubject;
   String submissionId;
+  String tag;
   PatchSet.Id currentPatchSetId;
   RevisionNoteMap revisionNoteMap;
 
@@ -120,37 +120,28 @@
   private final NoteDbMetrics metrics;
   private final Change.Id id;
   private final ObjectId tip;
-  private final RevWalk walk;
-  private final Repository repo;
+  private final ChangeNotesRevWalk walk;
   private final Map<PatchSet.Id,
-      Table<Account.Id, String, Optional<PatchSetApproval>>> approvals;
+      Table<Account.Id, Entry<String, String>, Optional<PatchSetApproval>>> approvals;
   private final List<ChangeMessage> allChangeMessages;
   private final Multimap<PatchSet.Id, ChangeMessage> changeMessagesByPatchSet;
 
-  ChangeNotesParser(Project.NameKey project, Change.Id changeId, ObjectId tip,
-      RevWalk walk, GitRepositoryManager repoManager,
-      ChangeNoteUtil noteUtil, NoteDbMetrics metrics)
-      throws RepositoryNotFoundException, IOException {
+  ChangeNotesParser(Change.Id changeId, ObjectId tip, ChangeNotesRevWalk walk,
+      ChangeNoteUtil noteUtil, NoteDbMetrics metrics) {
     this.id = changeId;
     this.tip = tip;
     this.walk = walk;
-    this.repo = repoManager.openMetadataRepository(project);
     this.noteUtil = noteUtil;
     this.metrics = metrics;
-    approvals = Maps.newHashMap();
-    reviewers = Maps.newLinkedHashMap();
-    allPastReviewers = Lists.newArrayList();
+    approvals = new HashMap<>();
+    reviewers = new LinkedHashMap<>();
+    allPastReviewers = new ArrayList<>();
     submitRecords = Lists.newArrayListWithExpectedSize(1);
-    allChangeMessages = Lists.newArrayList();
+    allChangeMessages = new ArrayList<>();
     changeMessagesByPatchSet = LinkedListMultimap.create();
     comments = ArrayListMultimap.create();
     patchSets = Maps.newTreeMap(ReviewDbUtil.intKeyOrdering());
-    patchSetStates = Maps.newHashMap();
-  }
-
-  @Override
-  public void close() {
-    repo.close();
+    patchSetStates = new HashMap<>();
   }
 
   void parseAll() throws ConfigInvalidException, IOException {
@@ -160,7 +151,8 @@
     walk.markStart(walk.parseCommit(tip));
 
     try (Timer1.Context timer = metrics.parseLatency.start(CHANGES)) {
-      for (RevCommit commit : walk) {
+      ChangeNotesCommit commit;
+      while ((commit = walk.next()) != null) {
         parse(commit);
       }
       parseNotes();
@@ -200,19 +192,18 @@
     return ImmutableListMultimap.copyOf(changeMessagesByPatchSet);
   }
 
-  private void parse(RevCommit commit) throws ConfigInvalidException {
+  private void parse(ChangeNotesCommit commit) throws ConfigInvalidException {
     Timestamp ts =
         new Timestamp(commit.getCommitterIdent().getWhen().getTime());
 
-    boolean updateTs = commit.getParentCount() == 0;
     createdOn = ts;
+    parseTag(commit);
+
     if (branch == null) {
       branch = parseBranch(commit);
-      updateTs |= branch != null;
     }
     if (status == null) {
       status = parseStatus(commit);
-      updateTs |= status != null;
     }
 
     PatchSet.Id psId = parsePatchSetId(commit);
@@ -232,7 +223,6 @@
 
     if (changeId == null) {
       changeId = parseChangeId(commit);
-      updateTs |= changeId != null;
     }
 
     String currSubject = parseSubject(commit);
@@ -241,83 +231,77 @@
         subject = currSubject;
       }
       originalSubject = currSubject;
-      updateTs = true;
     }
 
-    updateTs |= parseChangeMessage(psId, accountId, commit, ts) != null;
+    parseChangeMessage(psId, accountId, commit, ts);
     if (topic == null) {
       topic = parseTopic(commit);
-      updateTs |= topic != null;
     }
 
-    Set<String> oldHashtags = hashtags;
     parseHashtags(commit);
-    updateTs |= hashtags != oldHashtags;
 
     if (submissionId == null) {
       submissionId = parseSubmissionId(commit);
-      updateTs |= submissionId != null;
     }
 
     ObjectId currRev = parseRevision(commit);
     if (currRev != null) {
       parsePatchSet(psId, currRev, accountId, ts);
-      updateTs = true;
     }
     parseGroups(psId, commit);
 
     if (submitRecords.isEmpty()) {
       // Only parse the most recent set of submit records; any older ones are
       // still there, but not currently used.
-      parseSubmitRecords(commit.getFooterLines(FOOTER_SUBMITTED_WITH));
-      updateTs |= !submitRecords.isEmpty();
+      parseSubmitRecords(commit.getFooterLineValues(FOOTER_SUBMITTED_WITH));
     }
 
-    for (String line : commit.getFooterLines(FOOTER_LABEL)) {
+    for (String line : commit.getFooterLineValues(FOOTER_LABEL)) {
       parseApproval(psId, accountId, ts, line);
-      updateTs = true;
     }
 
     for (ReviewerStateInternal state : ReviewerStateInternal.values()) {
-      for (String line : commit.getFooterLines(state.getFooterKey())) {
+      for (String line : commit.getFooterLineValues(state.getFooterKey())) {
         parseReviewer(state, line);
       }
       // Don't update timestamp when a reviewer was added, matching RevewDb
       // behavior.
     }
 
-    if (updateTs) {
-      if (lastUpdatedOn == null || ts.after(lastUpdatedOn)) {
-        lastUpdatedOn = ts;
-      }
+    if (lastUpdatedOn == null || ts.after(lastUpdatedOn)) {
+      lastUpdatedOn = ts;
     }
   }
 
-  private String parseSubmissionId(RevCommit commit)
+  private String parseSubmissionId(ChangeNotesCommit commit)
       throws ConfigInvalidException {
     return parseOneFooter(commit, FOOTER_SUBMISSION_ID);
   }
 
-  private String parseBranch(RevCommit commit) throws ConfigInvalidException {
+  private String parseBranch(ChangeNotesCommit commit)
+      throws ConfigInvalidException {
     String branch = parseOneFooter(commit, FOOTER_BRANCH);
     return branch != null ? RefNames.fullName(branch) : null;
   }
 
-  private String parseChangeId(RevCommit commit) throws ConfigInvalidException {
+  private String parseChangeId(ChangeNotesCommit commit)
+      throws ConfigInvalidException {
     return parseOneFooter(commit, FOOTER_CHANGE_ID);
   }
 
-  private String parseSubject(RevCommit commit) throws ConfigInvalidException {
+  private String parseSubject(ChangeNotesCommit commit)
+      throws ConfigInvalidException {
     return parseOneFooter(commit, FOOTER_SUBJECT);
   }
 
-  private String parseTopic(RevCommit commit) throws ConfigInvalidException {
+  private String parseTopic(ChangeNotesCommit commit)
+      throws ConfigInvalidException {
     return parseOneFooter(commit, FOOTER_TOPIC);
   }
 
-  private String parseOneFooter(RevCommit commit, FooterKey footerKey)
+  private String parseOneFooter(ChangeNotesCommit commit, FooterKey footerKey)
       throws ConfigInvalidException {
-    List<String> footerLines = commit.getFooterLines(footerKey);
+    List<String> footerLines = commit.getFooterLineValues(footerKey);
     if (footerLines.isEmpty()) {
       return null;
     } else if (footerLines.size() > 1) {
@@ -326,8 +310,8 @@
     return footerLines.get(0);
   }
 
-  private String parseExactlyOneFooter(RevCommit commit, FooterKey footerKey)
-      throws ConfigInvalidException {
+  private String parseExactlyOneFooter(ChangeNotesCommit commit,
+      FooterKey footerKey) throws ConfigInvalidException {
     String line = parseOneFooter(commit, footerKey);
     if (line == null) {
       throw expectedOneFooter(footerKey, Collections.<String> emptyList());
@@ -335,7 +319,7 @@
     return line;
   }
 
-  private ObjectId parseRevision(RevCommit commit)
+  private ObjectId parseRevision(ChangeNotesCommit commit)
       throws ConfigInvalidException {
     String sha = parseOneFooter(commit, FOOTER_COMMIT);
     if (sha == null) {
@@ -371,7 +355,7 @@
     ps.setCreatedOn(ts);
   }
 
-  private void parseGroups(PatchSet.Id psId, RevCommit commit)
+  private void parseGroups(PatchSet.Id psId, ChangeNotesCommit commit)
       throws ConfigInvalidException {
     String groupsStr = parseOneFooter(commit, FOOTER_GROUPS);
     if (groupsStr == null) {
@@ -388,12 +372,14 @@
     ps.setGroups(PatchSet.splitGroups(groupsStr));
   }
 
-  private void parseHashtags(RevCommit commit) throws ConfigInvalidException {
-    // Commits are parsed in reverse order and only the last set of hashtags should be used.
+  private void parseHashtags(ChangeNotesCommit commit)
+      throws ConfigInvalidException {
+    // Commits are parsed in reverse order and only the last set of hashtags
+    // should be used.
     if (hashtags != null) {
       return;
     }
-    List<String> hashtagsLines = commit.getFooterLines(FOOTER_HASHTAGS);
+    List<String> hashtagsLines = commit.getFooterLineValues(FOOTER_HASHTAGS);
     if (hashtagsLines.isEmpty()) {
       return;
     } else if (hashtagsLines.size() > 1) {
@@ -405,9 +391,22 @@
     }
   }
 
-  private Change.Status parseStatus(RevCommit commit)
+  private void parseTag(ChangeNotesCommit commit)
       throws ConfigInvalidException {
-    List<String> statusLines = commit.getFooterLines(FOOTER_STATUS);
+    tag = null;
+    List<String> tagLines = commit.getFooterLineValues(FOOTER_TAG);
+    if (tagLines.isEmpty()) {
+      return;
+    } else if (tagLines.size() == 1) {
+      tag = tagLines.get(0);
+    } else {
+      throw expectedOneFooter(FOOTER_TAG, tagLines);
+    }
+  }
+
+  private Change.Status parseStatus(ChangeNotesCommit commit)
+      throws ConfigInvalidException {
+    List<String> statusLines = commit.getFooterLineValues(FOOTER_STATUS);
     if (statusLines.isEmpty()) {
       return null;
     } else if (statusLines.size() > 1) {
@@ -421,7 +420,7 @@
     return status.get();
   }
 
-  private PatchSet.Id parsePatchSetId(RevCommit commit)
+  private PatchSet.Id parsePatchSetId(ChangeNotesCommit commit)
       throws ConfigInvalidException {
     String psIdLine = parseExactlyOneFooter(commit, FOOTER_PATCH_SET);
     int s = psIdLine.indexOf(' ');
@@ -433,7 +432,7 @@
     return new PatchSet.Id(id, psId);
   }
 
-  private PatchSetState parsePatchSetState(RevCommit commit)
+  private PatchSetState parsePatchSetState(ChangeNotesCommit commit)
       throws ConfigInvalidException {
     String psIdLine = parseExactlyOneFooter(commit, FOOTER_PATCH_SET);
     int s = psIdLine.indexOf(' ');
@@ -451,20 +450,20 @@
     throw invalidFooter(FOOTER_PATCH_SET, psIdLine);
   }
 
-  private ChangeMessage parseChangeMessage(PatchSet.Id psId,
-      Account.Id accountId, RevCommit commit, Timestamp ts) {
+  private void parseChangeMessage(PatchSet.Id psId,
+      Account.Id accountId, ChangeNotesCommit commit, Timestamp ts) {
     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 null;
+      return;
     }
 
     int subjectEnd = RawParseUtils.endOfParagraph(raw, subjectStart);
     if (subjectEnd == size) {
-      return null;
+      return;
     }
 
     int changeMessageStart;
@@ -474,12 +473,12 @@
     } else if (raw[subjectEnd] == '\r') {
       changeMessageStart = subjectEnd + 4; //\r\n\r\n ends paragraph
     } else {
-      return null;
+      return;
     }
 
     int ptr = size - 1;
     int changeMessageEnd = -1;
-    while(ptr > changeMessageStart) {
+    while (ptr > changeMessageStart) {
       ptr = RawParseUtils.prevLF(raw, ptr, '\r');
       if (ptr == -1) {
         break;
@@ -494,7 +493,7 @@
     }
 
     if (ptr <= changeMessageStart) {
-      return null;
+      return;
     }
 
     String changeMsgString = RawParseUtils.decode(enc, raw,
@@ -505,15 +504,15 @@
         ts,
         psId);
     changeMessage.setMessage(changeMsgString);
+    changeMessage.setTag(tag);
     changeMessagesByPatchSet.put(psId, changeMessage);
     allChangeMessages.add(changeMessage);
-    return changeMessage;
   }
 
   private void parseNotes()
       throws IOException, ConfigInvalidException {
     ObjectReader reader = walk.getObjectReader();
-    RevCommit tipCommit = walk.parseCommit(tip);
+    ChangeNotesCommit tipCommit = walk.parseCommit(tip);
     revisionNoteMap = RevisionNoteMap.parse(
         noteUtil, id, reader, NoteMap.read(reader, tipCommit), false);
     Map<RevId, RevisionNote> rns = revisionNoteMap.revisionNotes;
@@ -570,36 +569,39 @@
       throw pe;
     }
 
-    Table<Account.Id, String, Optional<PatchSetApproval>> curr =
-        getApprovalsTableIfNoVotePresent(psId, accountId, l.label());
+    Entry<String, String> label = Maps.immutableEntry(l.label(), tag);
+    Table<Account.Id, Entry<String, String>, Optional<PatchSetApproval>> curr =
+        getApprovalsTableIfNoVotePresent(psId, accountId, label);
     if (curr != null) {
-      curr.put(accountId, l.label(), Optional.of(new PatchSetApproval(
+      PatchSetApproval psa = new PatchSetApproval(
           new PatchSetApproval.Key(
               psId,
               accountId,
               new LabelId(l.label())),
           l.value(),
-          ts)));
+          ts);
+      psa.setTag(tag);
+      curr.put(accountId, label, Optional.of(psa));
     }
   }
 
   private void parseRemoveApproval(PatchSet.Id psId, Account.Id committerId,
       String line) throws ConfigInvalidException {
     Account.Id accountId;
-    String label;
+    Entry<String, String> label;
     int s = line.indexOf(' ');
     if (s > 0) {
-      label = line.substring(1, s);
+      label = Maps.immutableEntry(line.substring(1, s), tag);
       PersonIdent ident = RawParseUtils.parsePersonIdent(line.substring(s + 1));
       checkFooter(ident != null, FOOTER_LABEL, line);
       accountId = noteUtil.parseIdent(ident, id);
     } else {
-      label = line.substring(1);
+      label = Maps.immutableEntry(line.substring(1), tag);
       accountId = committerId;
     }
 
     try {
-      LabelType.checkNameInternal(label);
+      LabelType.checkNameInternal(label.getKey());
     } catch (IllegalArgumentException e) {
       ConfigInvalidException pe =
           parseException("invalid %s: %s", FOOTER_LABEL, line);
@@ -607,18 +609,18 @@
       throw pe;
     }
 
-    Table<Account.Id, String, Optional<PatchSetApproval>> curr =
+    Table<Account.Id, Entry<String, String>, Optional<PatchSetApproval>> curr =
         getApprovalsTableIfNoVotePresent(psId, accountId, label);
     if (curr != null) {
       curr.put(accountId, label, Optional.<PatchSetApproval> absent());
     }
   }
 
-  private Table<Account.Id, String, Optional<PatchSetApproval>>
+  private Table<Account.Id, Entry<String, String>, Optional<PatchSetApproval>>
       getApprovalsTableIfNoVotePresent(PatchSet.Id psId, Account.Id accountId,
-        String label) {
+        Entry<String, String> label) {
 
-    Table<Account.Id, String, Optional<PatchSetApproval>> curr =
+    Table<Account.Id, Entry<String, String>, Optional<PatchSetApproval>> curr =
         approvals.get(psId);
     if (curr != null) {
       if (curr.contains(accountId, label)) {
@@ -626,12 +628,12 @@
       }
     } else {
       curr = Tables.newCustomTable(
-          Maps.<Account.Id, Map<String, Optional<PatchSetApproval>>>
+          Maps.<Account.Id, Map<Entry<String, String>, Optional<PatchSetApproval>>>
               newHashMapWithExpectedSize(2),
-          new Supplier<Map<String, Optional<PatchSetApproval>>>() {
+          new Supplier<Map<Entry<String, String>, Optional<PatchSetApproval>>>() {
             @Override
-            public Map<String, Optional<PatchSetApproval>> get() {
-              return Maps.newLinkedHashMap();
+            public Map<Entry<String, String>, Optional<PatchSetApproval>> get() {
+              return new LinkedHashMap<>();
             }
           });
       approvals.put(psId, curr);
@@ -661,7 +663,7 @@
         checkFooter(rec != null, FOOTER_SUBMITTED_WITH, line);
         SubmitRecord.Label label = new SubmitRecord.Label();
         if (rec.labels == null) {
-          rec.labels = Lists.newArrayList();
+          rec.labels = new ArrayList<>();
         }
         rec.labels.add(label);
 
@@ -683,7 +685,7 @@
     }
   }
 
-  private Account.Id parseIdent(RevCommit commit)
+  private Account.Id parseIdent(ChangeNotesCommit commit)
       throws ConfigInvalidException {
     // Check if the author name/email is the same as the committer name/email,
     // i.e. was the server ident at the time this commit was made.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilder.java
index 31c33da..e3f73c5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilder.java
@@ -14,15 +14,18 @@
 
 package com.google.gerrit.server.notedb;
 
+import com.google.common.collect.ImmutableMultimap;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Repository;
 
 import java.io.IOException;
 import java.util.concurrent.Callable;
@@ -53,4 +56,10 @@
   public abstract NoteDbChangeState rebuild(NoteDbUpdateManager manager,
       ChangeBundle bundle) throws NoSuchChangeException, IOException,
       OrmException, ConfigInvalidException;
+
+  public abstract boolean rebuildProject(ReviewDb db,
+      ImmutableMultimap<Project.NameKey, Change.Id> allChanges,
+      Project.NameKey project, Repository allUsersRepo)
+      throws NoSuchChangeException, IOException, OrmException,
+      ConfigInvalidException;
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilderImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilderImpl.java
index d6dfd76..6daf457 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilderImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilderImpl.java
@@ -14,25 +14,30 @@
 
 package com.google.gerrit.server.notedb;
 
+import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
 import static com.google.gerrit.server.PatchLineCommentsUtil.setCommentRevId;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_HASHTAGS;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET;
 import static java.util.concurrent.TimeUnit.SECONDS;
 
 import com.google.common.base.MoreObjects;
+import com.google.common.base.Optional;
 import com.google.common.base.Predicate;
 import com.google.common.base.Splitter;
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ComparisonChain;
 import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableMultimap;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Lists;
 import com.google.common.collect.Multimap;
 import com.google.common.collect.Ordering;
 import com.google.common.collect.Sets;
 import com.google.common.primitives.Ints;
+import com.google.gerrit.common.FormatUtil;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
@@ -41,7 +46,9 @@
 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.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.reviewdb.server.ReviewDbUtil;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.PatchLineCommentsUtil;
 import com.google.gerrit.server.account.AccountCache;
@@ -59,13 +66,19 @@
 import org.eclipse.jgit.errors.InvalidObjectIdException;
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.ProgressMonitor;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.TextProgressMonitor;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.ReceiveCommand;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.io.PrintWriter;
 import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -77,6 +90,9 @@
 import java.util.regex.Pattern;
 
 public class ChangeRebuilderImpl extends ChangeRebuilder {
+  private static final Logger log =
+      LoggerFactory.getLogger(ChangeRebuilderImpl.class);
+
   /**
    * The maximum amount of time between the ReviewDb timestamp of the first and
    * last events batched together into a single NoteDb update.
@@ -142,7 +158,9 @@
     NoteDbUpdateManager manager =
         updateManagerFactory.create(change.getProject());
     buildUpdates(manager, ChangeBundle.fromReviewDb(db, changeId));
-    return execute(db, changeId, manager);
+    NoteDbChangeState result = execute(db, changeId, manager);
+    manager.execute();
+    return result;
   }
 
   private NoteDbChangeState execute(ReviewDb db, Change.Id changeId,
@@ -172,29 +190,72 @@
       OrmException, ConfigInvalidException {
     Change change = new Change(bundle.getChange());
     buildUpdates(manager, bundle);
-    NoteDbChangeState newState = NoteDbChangeState.applyDelta(
+    return NoteDbChangeState.applyDelta(
         change, manager.stage().get(change.getId()));
-    manager.execute();
-    return newState;
+  }
+
+  @Override
+  public boolean rebuildProject(ReviewDb db,
+      ImmutableMultimap<Project.NameKey, Change.Id> allChanges,
+      Project.NameKey project, Repository allUsersRepo)
+      throws NoSuchChangeException, IOException, OrmException,
+      ConfigInvalidException {
+    checkArgument(allChanges.containsKey(project));
+    boolean ok = true;
+    ProgressMonitor pm = new TextProgressMonitor(new PrintWriter(System.out));
+    NoteDbUpdateManager manager = updateManagerFactory.create(project);
+    pm.beginTask(
+        FormatUtil.elide(project.get(), 50), allChanges.get(project).size());
+    try (ObjectInserter allUsersInserter = allUsersRepo.newObjectInserter();
+        RevWalk allUsersRw = new RevWalk(allUsersInserter.newReader())) {
+      manager.setAllUsersRepo(allUsersRepo, allUsersRw, allUsersInserter,
+          new ChainedReceiveCommands(allUsersRepo));
+      for (Change.Id changeId : allChanges.get(project)) {
+        try {
+          buildUpdates(manager, ChangeBundle.fromReviewDb(db, changeId));
+        } catch (Throwable t) {
+          log.error("Failed to rebuild change " + changeId, t);
+          ok = false;
+        }
+        pm.update(1);
+      }
+      manager.execute();
+    } finally {
+      pm.endTask();
+    }
+    return ok;
   }
 
   private void buildUpdates(NoteDbUpdateManager manager, ChangeBundle bundle)
-      throws IOException, OrmException, ConfigInvalidException {
+      throws IOException, OrmException {
+    manager.setCheckExpectedState(false);
     Change change = new Change(bundle.getChange());
+    PatchSet.Id currPsId = change.currentPatchSetId();
     // We will rebuild all events, except for draft comments, in buckets based
     // on author and timestamp.
-    List<Event> events = Lists.newArrayList();
+    List<Event> events = new ArrayList<>();
     Multimap<Account.Id, PatchLineCommentEvent> draftCommentEvents =
         ArrayListMultimap.create();
 
-    Repository changeMetaRepo = manager.getChangeRepo().repo;
     events.addAll(getHashtagsEvents(change, manager));
 
     // Delete ref only after hashtags have been read
-    deleteRef(change, changeMetaRepo, manager.getChangeRepo().cmds);
+    deleteRef(change, manager.getChangeRepo().cmds);
+
+    Integer minPsNum = getMinPatchSetNum(bundle);
+    Set<PatchSet.Id> psIds =
+        Sets.newHashSetWithExpectedSize(bundle.getPatchSets().size());
 
     for (PatchSet ps : bundle.getPatchSets()) {
-      events.add(new PatchSetEvent(change, ps, manager.getCodeRepo().rw));
+      if (ps.getId().get() > currPsId.get()) {
+        log.info(
+            "Skipping patch set {}, which is higher than current patch set {}",
+            ps.getId(), currPsId);
+        continue;
+      }
+      psIds.add(ps.getId());
+      events.add(new PatchSetEvent(
+          change, ps, manager.getChangeRepo().rw));
       for (PatchLineComment c : getPatchLineComments(bundle, ps)) {
         PatchLineCommentEvent e =
             new PatchLineCommentEvent(c, change, ps, patchListCache);
@@ -207,18 +268,20 @@
     }
 
     for (PatchSetApproval psa : bundle.getPatchSetApprovals()) {
-      events.add(new ApprovalEvent(psa, change.getCreatedOn()));
+      if (psIds.contains(psa.getPatchSetId())) {
+        events.add(new ApprovalEvent(psa, change.getCreatedOn()));
+      }
     }
 
     Change noteDbChange = new Change(null, null, null, null, null);
     for (ChangeMessage msg : bundle.getChangeMessages()) {
-      events.add(
-          new ChangeMessageEvent(msg, noteDbChange, change.getCreatedOn()));
+      if (msg.getPatchSetId() == null || psIds.contains(msg.getPatchSetId())) {
+        events.add(
+            new ChangeMessageEvent(msg, noteDbChange, change.getCreatedOn()));
+      }
     }
 
-    Collections.sort(events, EVENT_ORDER);
-
-    events.add(new FinalUpdatesEvent(change, noteDbChange));
+    sortAndFillEvents(change, noteDbChange, events, minPsNum);
 
     EventList<Event> el = new EventList<>();
     for (Event e : events) {
@@ -232,7 +295,8 @@
 
     EventList<PatchLineCommentEvent> plcel = new EventList<>();
     for (Account.Id author : draftCommentEvents.keys()) {
-      for (PatchLineCommentEvent e : draftCommentEvents.get(author)) {
+      for (PatchLineCommentEvent e :
+          EVENT_ORDER.sortedCopy(draftCommentEvents.get(author))) {
         if (!plcel.canAdd(e)) {
           flushEventsToDraftUpdate(manager, plcel, change);
           checkState(plcel.canAdd(e));
@@ -243,6 +307,17 @@
     }
   }
 
+  private static Integer getMinPatchSetNum(ChangeBundle bundle) {
+    Integer minPsNum = null;
+    for (PatchSet ps : bundle.getPatchSets()) {
+      int n = ps.getId().get();
+      if (minPsNum == null || n < minPsNum) {
+        minPsNum = n;
+      }
+    }
+    return minPsNum;
+  }
+
   private static List<PatchLineComment> getPatchLineComments(ChangeBundle bundle,
       final PatchSet ps) {
     return FluentIterable.from(bundle.getPatchLineComments())
@@ -254,6 +329,40 @@
         }).toSortedList(PatchLineCommentsUtil.PLC_ORDER);
   }
 
+  private void sortAndFillEvents(Change change, Change noteDbChange,
+      List<Event> events, Integer minPsNum) {
+    Collections.sort(events, EVENT_ORDER);
+    events.add(new FinalUpdatesEvent(change, noteDbChange));
+
+    // Ensure the first event in the list creates the change, setting the author
+    // and any required footers.
+    Event first = events.get(0);
+    if (first instanceof PatchSetEvent && change.getOwner().equals(first.who)) {
+      ((PatchSetEvent) first).createChange = true;
+    } else {
+      events.add(0, new CreateChangeEvent(change, minPsNum));
+    }
+
+    // Fill in any missing patch set IDs using the latest patch set of the
+    // change at the time of the event, because NoteDb can't represent actions
+    // with no associated patch set ID. This workaround is as if a user added a
+    // ChangeMessage on the change by replying from the latest patch set.
+    //
+    // Start with the first patch set that actually exists. If there are no
+    // patch sets at all, minPsNum will be null, so just bail and use 1 as the
+    // patch set ID. The corresponding patch set won't exist, but this change is
+    // probably corrupt anyway, as deleting the last draft patch set should have
+    // deleted the whole change.
+    int ps = firstNonNull(minPsNum, 1);
+    for (Event e : events) {
+      if (e.psId == null) {
+        e.psId = new PatchSet.Id(change.getId(), ps);
+      } else {
+        ps = Math.max(ps, e.psId.get());
+      }
+    }
+  }
+
   private void flushEventsToUpdate(NoteDbUpdateManager manager,
       EventList<Event> events, Change change) throws OrmException, IOException {
     if (events.isEmpty()) {
@@ -269,13 +378,15 @@
       labelNameComparator = Ordering.natural();
     }
     ChangeUpdate update = updateFactory.create(
-        notesFactory.createWithAutoRebuildingDisabled(change),
+        notesFactory.createWithAutoRebuildingDisabled(
+            change, manager.getChangeRepo().cmds),
         events.getAccountId(),
         events.newAuthorIdent(),
         events.getWhen(),
         labelNameComparator);
     update.setAllowWriteToNewRef(true);
     update.setPatchSetId(events.getPatchSetId());
+    update.setTag(events.getTag());
     for (Event e : events) {
       e.apply(update);
     }
@@ -285,12 +396,13 @@
 
   private void flushEventsToDraftUpdate(NoteDbUpdateManager manager,
       EventList<PatchLineCommentEvent> events, Change change)
-      throws OrmException {
+      throws OrmException, IOException {
     if (events.isEmpty()) {
       return;
     }
     ChangeDraftUpdate update = draftUpdateFactory.create(
-        notesFactory.createWithAutoRebuildingDisabled(change),
+        notesFactory.createWithAutoRebuildingDisabled(
+            change, manager.getChangeRepo().cmds),
         events.getAccountId(),
         events.newAuthorIdent(),
         events.getWhen());
@@ -303,20 +415,25 @@
   }
 
   private List<HashtagsEvent> getHashtagsEvents(Change change,
-      NoteDbUpdateManager manager) throws IOException, ConfigInvalidException {
-    String refName = ChangeNoteUtil.changeRefName(change.getId());
-    ObjectId old = manager.getChangeRepo().getObjectId(refName);
-    if (old == null) {
+      NoteDbUpdateManager manager) throws IOException {
+    String refName = changeMetaRef(change.getId());
+    Optional<ObjectId> old = manager.getChangeRepo().getObjectId(refName);
+    if (!old.isPresent()) {
       return Collections.emptyList();
     }
 
     RevWalk rw = manager.getChangeRepo().rw;
     List<HashtagsEvent> events = new ArrayList<>();
     rw.reset();
-    rw.markStart(rw.parseCommit(old));
+    rw.markStart(rw.parseCommit(old.get()));
     for (RevCommit commit : rw) {
-      Account.Id authorId =
-          changeNoteUtil.parseIdent(commit.getAuthorIdent(), change.getId());
+      Account.Id authorId;
+      try {
+        authorId =
+            changeNoteUtil.parseIdent(commit.getAuthorIdent(), change.getId());
+      } catch (ConfigInvalidException e) {
+        continue; // Corrupt data, no valid hashtags in this commit.
+      }
       PatchSet.Id psId = parsePatchSetId(change, commit);
       Set<String> hashtags = parseHashtags(commit);
       if (authorId == null || psId == null || hashtags == null) {
@@ -356,12 +473,12 @@
     return new PatchSet.Id(change.getId(), psId);
   }
 
-  private void deleteRef(Change change, Repository repo,
-      ChainedReceiveCommands cmds) throws IOException {
-    String refName = ChangeNoteUtil.changeRefName(change.getId());
-    ObjectId old = cmds.getObjectId(repo, refName);
-    if (old != null) {
-      cmds.add(new ReceiveCommand(old, ObjectId.zeroId(), refName));
+  private void deleteRef(Change change, ChainedReceiveCommands cmds)
+      throws IOException {
+    String refName = changeMetaRef(change.getId());
+    Optional<ObjectId> old = cmds.get(refName);
+    if (old.isPresent()) {
+      cmds.add(new ReceiveCommand(old.get(), ObjectId.zeroId(), refName));
     }
   }
 
@@ -369,27 +486,34 @@
     @Override
     public int compare(Event a, Event b) {
       return ComparisonChain.start()
-          .compareTrueFirst(a.predatesChange, b.predatesChange)
           .compare(a.when, b.when)
-          .compare(a.who.get(), b.who.get())
-          .compare(a.psId.get(), b.psId.get())
+          .compareTrueFirst(isPatchSet(a), isPatchSet(b))
+          .compareTrueFirst(a.predatesChange, b.predatesChange)
+          .compare(a.who, b.who, ReviewDbUtil.intKeyOrdering())
+          .compare(a.psId, b.psId, ReviewDbUtil.intKeyOrdering().nullsLast())
           .result();
     }
+
+    private boolean isPatchSet(Event e) {
+      return e instanceof PatchSetEvent;
+    }
   };
 
   private abstract static class Event {
     // NOTE: EventList only supports direct subclasses, not an arbitrary
     // hierarchy.
 
-    final PatchSet.Id psId;
     final Account.Id who;
     final Timestamp when;
+    final String tag;
     final boolean predatesChange;
+    PatchSet.Id psId;
 
     protected Event(PatchSet.Id psId, Account.Id who, Timestamp when,
-        Timestamp changeCreatedOn) {
+        Timestamp changeCreatedOn, String tag) {
       this.psId = psId;
       this.who = who;
+      this.tag = tag;
       // Truncate timestamps at the change's createdOn timestamp.
       predatesChange = when.before(changeCreatedOn);
       this.when = predatesChange ? changeCreatedOn : when;
@@ -450,8 +574,9 @@
 
       Event last = getLast();
       if (!Objects.equals(e.who, last.who)
-          || !Objects.equals(e.psId, last.psId)) {
-        return false; // Different patch set or author.
+          || !e.psId.equals(last.psId)
+          || !Objects.equals(e.tag, last.tag)) {
+        return false; // Different patch set, author, or tag.
       }
 
       long t = e.when.getTime();
@@ -483,9 +608,9 @@
     }
 
     PatchSet.Id getPatchSetId() {
-      PatchSet.Id id = get(0).psId;
+      PatchSet.Id id = checkNotNull(get(0).psId);
       for (int i = 1; i < size(); i++) {
-        checkState(Objects.equals(id, get(i).psId),
+        checkState(get(i).psId.equals(id),
             "mismatched patch sets in EventList: %s != %s", id, get(i).psId);
       }
       return id;
@@ -509,6 +634,50 @@
           accountCache.get(id).getAccount(), getWhen(), serverIdent,
           anonymousCowardName);
     }
+
+    String getTag() {
+      return getLast().tag;
+    }
+  }
+
+  private static void createChange(ChangeUpdate update, Change change) {
+    update.setSubjectForCommit("Create change");
+    update.setChangeId(change.getKey().get());
+    update.setBranch(change.getDest().get());
+    update.setSubject(change.getOriginalSubject());
+  }
+
+  private static class CreateChangeEvent extends Event {
+    private final Change change;
+
+    private static PatchSet.Id psId(Change change, Integer minPsNum) {
+      int n;
+      if (minPsNum == null) {
+        // There were no patch sets for the change at all, so something is very
+        // wrong. Bail and use 1 as the patch set.
+        n = 1;
+      } else {
+        n = minPsNum;
+      }
+      return new PatchSet.Id(change.getId(), n);
+    }
+
+    CreateChangeEvent(Change change, Integer minPsNum) {
+      super(psId(change, minPsNum), change.getOwner(), change.getCreatedOn(),
+          change.getCreatedOn(), null);
+      this.change = change;
+    }
+
+    @Override
+    boolean uniquePerUpdate() {
+      return true;
+    }
+
+    @Override
+    void apply(ChangeUpdate update) throws IOException, OrmException {
+      checkUpdate(update);
+      createChange(update, change);
+    }
   }
 
   private static class ApprovalEvent extends Event {
@@ -516,7 +685,7 @@
 
     ApprovalEvent(PatchSetApproval psa, Timestamp changeCreatedOn) {
       super(psa.getPatchSetId(), psa.getAccountId(), psa.getGranted(),
-          changeCreatedOn);
+          changeCreatedOn, psa.getTag());
       this.psa = psa;
     }
 
@@ -536,10 +705,11 @@
     private final Change change;
     private final PatchSet ps;
     private final RevWalk rw;
+    private boolean createChange;
 
     PatchSetEvent(Change change, PatchSet ps, RevWalk rw) {
       super(ps.getId(), ps.getUploader(), ps.getCreatedOn(),
-          change.getCreatedOn());
+          change.getCreatedOn(), null);
       this.change = change;
       this.ps = ps;
       this.rw = rw;
@@ -553,16 +723,17 @@
     @Override
     void apply(ChangeUpdate update) throws IOException, OrmException {
       checkUpdate(update);
-      update.setSubject(change.getSubject());
-      if (ps.getPatchSetId() == 1) {
-        update.setSubjectForCommit("Create change");
-        update.setChangeId(change.getKey().get());
-        update.setBranch(change.getDest().get());
+      if (createChange) {
+        createChange(update, change);
       } else {
+        update.setSubject(change.getSubject());
         update.setSubjectForCommit("Create patch set " + ps.getPatchSetId());
       }
       setRevision(update, ps);
-      update.setGroups(ps.getGroups());
+      List<String> groups = ps.getGroups();
+      if (!groups.isEmpty()) {
+        update.setGroups(ps.getGroups());
+      }
       if (ps.isDraft()) {
         update.setPatchSetState(PatchSetState.DRAFT);
       }
@@ -597,7 +768,7 @@
     PatchLineCommentEvent(PatchLineComment c, Change change, PatchSet ps,
         PatchListCache cache) {
       super(PatchLineCommentsUtil.getCommentPsId(c), c.getAuthor(),
-          c.getWrittenOn(), change.getCreatedOn());
+          c.getWrittenOn(), change.getCreatedOn(), c.getTag());
       this.c = c;
       this.change = change;
       this.ps = ps;
@@ -631,7 +802,10 @@
 
     HashtagsEvent(PatchSet.Id psId, Account.Id who, Timestamp when,
         Set<String> hashtags, Timestamp changeCreatdOn) {
-      super(psId, who, when, changeCreatdOn);
+      super(psId, who, when, changeCreatdOn,
+          // Somewhat confusingly, hashtags do not use the setTag method on
+          // AbstractChangeUpdate, so pass null as the tag.
+          null);
       this.hashtags = hashtags;
     }
 
@@ -667,7 +841,7 @@
     ChangeMessageEvent(ChangeMessage message, Change noteDbChange,
         Timestamp changeCreatedOn) {
       super(message.getPatchSetId(), message.getAuthor(),
-          message.getWrittenOn(), changeCreatedOn);
+          message.getWrittenOn(), changeCreatedOn, message.getTag());
       this.message = message;
       this.noteDbChange = noteDbChange;
     }
@@ -736,7 +910,7 @@
 
     FinalUpdatesEvent(Change change, Change noteDbChange) {
       super(change.currentPatchSetId(), change.getOwner(),
-          change.getLastUpdatedOn(), change.getCreatedOn());
+          change.getLastUpdatedOn(), change.getCreatedOn(), null);
       this.change = change;
       this.noteDbChange = noteDbChange;
     }
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 b67fb59..3c7f909 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
@@ -18,6 +18,7 @@
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_BRANCH;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_CHANGE_ID;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_COMMIT;
@@ -29,6 +30,7 @@
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBJECT;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBMISSION_ID;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBMITTED_WITH;
+import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_TAG;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_TOPIC;
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 
@@ -121,6 +123,7 @@
   private String commit;
   private Set<String> hashtags;
   private String changeMessage;
+  private String tag;
   private PatchSetState psState;
   private Iterable<String> groups;
   private String pushCert;
@@ -293,6 +296,10 @@
     this.changeMessage = changeMessage;
   }
 
+  public void setTag(String tag) {
+    this.tag = tag;
+  }
+
   public void putComment(PatchLineComment c) {
     verifyComment(c);
     createDraftUpdateIfNull();
@@ -393,6 +400,7 @@
 
     RevisionNoteBuilder.Cache cache = new RevisionNoteBuilder.Cache(rnm);
     for (PatchLineComment c : comments) {
+      c.setTag(tag);
       cache.get(c.getRevId()).putComment(c);
     }
     if (pushCert != null) {
@@ -473,7 +481,7 @@
 
   @Override
   protected String getRefName() {
-    return ChangeNoteUtil.changeRefName(getId());
+    return changeMetaRef(getId());
   }
 
   @Override
@@ -526,6 +534,10 @@
       addFooter(msg, FOOTER_HASHTAGS, comma.join(hashtags));
     }
 
+    if (tag != null) {
+      addFooter(msg, FOOTER_TAG, tag);
+    }
+
     if (groups != null) {
       addFooter(msg, FOOTER_GROUPS, comma.join(groups));
     }
@@ -621,7 +633,8 @@
         && topic == null
         && commit == null
         && psState == null
-        && groups == null;
+        && groups == null
+        && tag == null;
   }
 
   ChangeDraftUpdate getDraftUpdate() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
index e7d933c..74c27bd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.server.git.RepoRefCache;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.assistedinject.Assisted;
@@ -35,7 +36,6 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.notes.NoteMap;
 import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
 
 import java.io.IOException;
 
@@ -61,9 +61,7 @@
       Args args,
       @Assisted Change change,
       @Assisted Account.Id author) {
-    super(args, change.getId());
-    this.change = change;
-    this.author = author;
+    this(args, change, author, true);
   }
 
   @AssistedInject
@@ -71,11 +69,21 @@
       Args args,
       @Assisted Change.Id changeId,
       @Assisted Account.Id author) {
-    super(args, changeId);
+    super(args, changeId, true);
     this.change = null;
     this.author = author;
   }
 
+  DraftCommentNotes(
+      Args args,
+      Change change,
+      Account.Id author,
+      boolean autoRebuild) {
+    super(args, change.getId(), autoRebuild);
+    this.change = change;
+    this.author = author;
+  }
+
   RevisionNoteMap getRevisionNoteMap() {
     return revisionNoteMap;
   }
@@ -100,7 +108,7 @@
 
   @Override
   protected String getRefName() {
-    return RefNames.refsDraftComments(author, getChangeId());
+    return RefNames.refsDraftComments(getChangeId(), author);
   }
 
   @Override
@@ -138,11 +146,12 @@
 
   @Override
   protected LoadHandle openHandle(Repository repo) throws IOException {
-    if (change != null) {
+    if (change != null && autoRebuild) {
       NoteDbChangeState state = NoteDbChangeState.parse(change);
       // Only check if this particular user's drafts are up to date, to avoid
       // reading unnecessary refs.
-      if (state == null || !state.areDraftsUpToDate(repo, author)) {
+      if (!NoteDbChangeState.areDraftsUpToDate(
+          state, new RepoRefCache(repo), getChangeId(), author)) {
         return rebuildAndOpen(repo);
       }
     }
@@ -158,7 +167,7 @@
       }
       ObjectId draftsId = newState.getDraftIds().get(author);
       repo.scanForRepoChanges();
-      return LoadHandle.create(new RevWalk(repo), draftsId);
+      return LoadHandle.create(ChangeNotesCommit.newRevWalk(repo), draftsId);
     } catch (NoSuchChangeException e) {
       return super.openHandle(repo);
     } catch (OrmException | ConfigInvalidException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbChangeState.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbChangeState.java
index 055cba9..c08bdd8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbChangeState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbChangeState.java
@@ -16,6 +16,8 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
+import static com.google.gerrit.reviewdb.client.RefNames.refsDraftComments;
 
 import com.google.auto.value.AutoValue;
 import com.google.common.annotations.VisibleForTesting;
@@ -24,14 +26,13 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDbUtil;
+import com.google.gerrit.server.git.RefCache;
 
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
 
 import java.io.IOException;
 import java.util.Collections;
@@ -140,7 +141,25 @@
     return state;
   }
 
-  private static String toString(ObjectId changeMetaId,
+  public static boolean isChangeUpToDate(@Nullable NoteDbChangeState state,
+      RefCache changeRepoRefs, Change.Id changeId) throws IOException {
+    if (state == null) {
+      return !changeRepoRefs.get(changeMetaRef(changeId)).isPresent();
+    }
+    return state.isChangeUpToDate(changeRepoRefs);
+  }
+
+  public static boolean areDraftsUpToDate(@Nullable NoteDbChangeState state,
+      RefCache draftsRepoRefs, Change.Id changeId, Account.Id accountId)
+      throws IOException {
+    if (state == null) {
+      return !draftsRepoRefs.get(refsDraftComments(changeId, accountId))
+          .isPresent();
+    }
+    return state.areDraftsUpToDate(draftsRepoRefs, accountId);
+  }
+
+  public static String toString(ObjectId changeMetaId,
       Map<Account.Id, ObjectId> draftIds) {
     List<Account.Id> accountIds = Lists.newArrayList(draftIds.keySet());
     Collections.sort(accountIds, ReviewDbUtil.intKeyOrdering());
@@ -158,29 +177,29 @@
   private final ObjectId changeMetaId;
   private final ImmutableMap<Account.Id, ObjectId> draftIds;
 
-  NoteDbChangeState(Change.Id changeId, ObjectId changeMetaId,
+  public NoteDbChangeState(Change.Id changeId, ObjectId changeMetaId,
       Map<Account.Id, ObjectId> draftIds) {
     this.changeId = checkNotNull(changeId);
     this.changeMetaId = checkNotNull(changeMetaId);
     this.draftIds = ImmutableMap.copyOf(draftIds);
   }
 
-  public boolean isChangeUpToDate(Repository changeRepo) throws IOException {
-    Ref ref = changeRepo.exactRef(ChangeNoteUtil.changeRefName(changeId));
-    if (ref == null) {
+  public boolean isChangeUpToDate(RefCache changeRepoRefs) throws IOException {
+    Optional<ObjectId> id = changeRepoRefs.get(changeMetaRef(changeId));
+    if (!id.isPresent()) {
       return changeMetaId.equals(ObjectId.zeroId());
     }
-    return ref.getObjectId().equals(changeMetaId);
+    return id.get().equals(changeMetaId);
   }
 
-  public boolean areDraftsUpToDate(Repository draftsRepo, Account.Id accountId)
+  public boolean areDraftsUpToDate(RefCache draftsRepoRefs, Account.Id accountId)
       throws IOException {
-    Ref ref = draftsRepo.exactRef(
-        RefNames.refsDraftComments(accountId, changeId));
-    if (ref == null) {
+    Optional<ObjectId> id =
+        draftsRepoRefs.get(refsDraftComments(changeId, accountId));
+    if (!id.isPresent()) {
       return !draftIds.containsKey(accountId);
     }
-    return ref.getObjectId().equals(draftIds.get(accountId));
+    return id.get().equals(draftIds.get(accountId));
   }
 
   @VisibleForTesting
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbModule.java
index e2b3ae0..99c5fa6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbModule.java
@@ -14,13 +14,17 @@
 
 package com.google.gerrit.server.notedb;
 
+import com.google.common.collect.ImmutableMultimap;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Change.Id;
+import com.google.gerrit.reviewdb.client.Project.NameKey;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gwtorm.server.OrmException;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Repository;
 
 import java.io.IOException;
 
@@ -61,6 +65,14 @@
             OrmException, ConfigInvalidException {
           return null;
         }
+
+        @Override
+        public boolean rebuildProject(ReviewDb db,
+            ImmutableMultimap<NameKey, Id> allChanges, NameKey project,
+            Repository allUsersRepo) throws NoSuchChangeException, IOException,
+            OrmException, ConfigInvalidException {
+          return false;
+        }
       });
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
index 425072b..edee73a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.notedb;
 
-import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Preconditions.checkState;
@@ -33,6 +32,7 @@
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.ChainedReceiveCommands;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gwtorm.server.OrmConcurrencyException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
@@ -55,8 +55,8 @@
 /**
  * Object to manage a single sequence of updates to NoteDb.
  * <p>
- * Instances are one-time-use. Handles updating both the change meta repo and
- * the All-Users meta repo for any affected changes, with proper ordering.
+ * Instances are one-time-use. Handles updating both the change repo and the
+ * All-Users repo for any affected changes, with proper ordering.
  * <p>
  * To see the state that would be applied prior to executing the full sequence
  * of updates, use {@link #stage()}.
@@ -82,8 +82,8 @@
       this.close = close;
     }
 
-    ObjectId getObjectId(String refName) throws IOException {
-      return cmds.getObjectId(repo, refName);
+    Optional<ObjectId> getObjectId(String refName) throws IOException {
+      return cmds.get(refName);
     }
 
     @Override
@@ -104,10 +104,10 @@
   private final ListMultimap<String, ChangeUpdate> changeUpdates;
   private final ListMultimap<String, ChangeDraftUpdate> draftUpdates;
 
-  private OpenRepo codeRepo;
   private OpenRepo changeRepo;
   private OpenRepo allUsersRepo;
   private Map<Change.Id, NoteDbChangeState.Delta> staged;
+  private boolean checkExpectedState;
 
   @AssistedInject
   NoteDbUpdateManager(GitRepositoryManager repoManager,
@@ -131,12 +131,6 @@
     return this;
   }
 
-  public NoteDbUpdateManager setCodeRepo(Repository repo, RevWalk rw) {
-    checkState(codeRepo == null, "code repo already initialized");
-    codeRepo = new OpenRepo(repo, rw, null, null, false);
-    return this;
-  }
-
   public NoteDbUpdateManager setAllUsersRepo(Repository repo, RevWalk rw,
       ObjectInserter ins, ChainedReceiveCommands cmds) {
     checkState(allUsersRepo == null, "All-Users repo already initialized");
@@ -144,46 +138,38 @@
     return this;
   }
 
+  NoteDbUpdateManager setCheckExpectedState(boolean checkExpectedState) {
+    this.checkExpectedState = checkExpectedState;
+    return this;
+  }
+
   OpenRepo getChangeRepo() throws IOException {
     initChangeRepo();
     return changeRepo;
   }
 
-  OpenRepo getCodeRepo() throws IOException {
-    initCodeRepo();
-    return codeRepo;
-  }
-
   OpenRepo getAllUsersRepo() throws IOException {
     initAllUsersRepo();
     return allUsersRepo;
   }
 
-  private void initCodeRepo() throws IOException {
-    if (codeRepo == null) {
-      codeRepo = openRepo(projectName, false);
-    }
-  }
-
   private void initChangeRepo() throws IOException {
     if (changeRepo == null) {
-      changeRepo = openRepo(projectName, true);
+      changeRepo = openRepo(projectName);
     }
   }
 
   private void initAllUsersRepo() throws IOException {
     if (allUsersRepo == null) {
-      allUsersRepo = openRepo(allUsersName, true);
+      allUsersRepo = openRepo(allUsersName);
     }
   }
 
-  private OpenRepo openRepo(Project.NameKey p, boolean meta) throws IOException {
-    Repository repo = meta
-        ? repoManager.openMetadataRepository(p)
-        : repoManager.openRepository(p);
+  private OpenRepo openRepo(Project.NameKey p) throws IOException {
+    Repository repo = repoManager.openRepository(p);
     ObjectInserter ins = repo.newObjectInserter();
     return new OpenRepo(repo, new RevWalk(ins.newReader()), ins,
-        new ChainedReceiveCommands(), true);
+        new ChainedReceiveCommands(repo), true);
   }
 
   private boolean isEmpty() {
@@ -242,6 +228,7 @@
       if (!draftUpdates.isEmpty()) {
         initAllUsersRepo();
       }
+      checkExpectedState();
       addCommands();
 
       Table<Change.Id, Account.Id, ObjectId> allDraftIds = getDraftIds();
@@ -278,12 +265,10 @@
     for (ReceiveCommand cmd : allUsersRepo.cmds.getCommands().values()) {
       String r = cmd.getRefName();
       if (r.startsWith(REFS_DRAFT_COMMENTS)) {
-        Account.Id accountId =
-            Account.Id.fromRefPart(r.substring(REFS_DRAFT_COMMENTS.length()));
-        checkDraftRef(accountId != null, r);
-        int s = r.lastIndexOf('/');
-        checkDraftRef(s >= 0 && s < r.length() - 1, r);
-        Change.Id changeId = Change.Id.parse(r.substring(s + 1));
+        Change.Id changeId =
+            Change.Id.fromRefPart(r.substring(REFS_DRAFT_COMMENTS.length()));
+        Account.Id accountId = Account.Id.fromRefSuffix(r);
+        checkDraftRef(accountId != null && changeId != null, r);
         draftIds.put(changeId, accountId, cmd.getNewId());
       }
     }
@@ -345,6 +330,62 @@
     if (!draftUpdates.isEmpty()) {
       addUpdates(draftUpdates, allUsersRepo);
     }
+    checkExpectedState();
+  }
+
+  private void checkExpectedState() throws OrmException, IOException {
+    if (!checkExpectedState) {
+      return;
+    }
+
+    // Refuse to apply an update unless the state in NoteDb matches the state
+    // claimed in the ref. This means we may have failed a NoteDb ref update,
+    // and it would be incorrect to claim that the ref is up to date after this
+    // pipeline.
+    //
+    // Generally speaking, this case should be rare; in most cases, we should
+    // have detected and auto-fixed the stale state when creating ChangeNotes
+    // that got passed into the ChangeUpdate.
+    for (Collection<ChangeUpdate> us : changeUpdates.asMap().values()) {
+      ChangeUpdate u = us.iterator().next();
+      NoteDbChangeState expectedState = NoteDbChangeState.parse(u.getChange());
+
+      if (expectedState == null) {
+        // No previous state means we haven't previously written NoteDb graphs
+        // for this change yet. This means either:
+        //  - The change is new, and we'll be creating its ref.
+        //  - We short-circuited before adding any commands that update this
+        //    ref, and we won't stage a delta for this change either.
+        // Either way, it is safe to proceed here rather than throwing
+        // OrmConcurrencyException.
+        continue;
+      }
+
+      if (!expectedState.isChangeUpToDate(changeRepo.cmds)) {
+        throw new OrmConcurrencyException(String.format(
+            "cannot apply NoteDb updates for change %s;"
+            + " change meta ref does not match %s",
+            u.getId(), expectedState.getChangeMetaId().name()));
+      }
+    }
+
+    for (Collection<ChangeDraftUpdate> us : draftUpdates.asMap().values()) {
+      ChangeDraftUpdate u = us.iterator().next();
+      NoteDbChangeState expectedState = NoteDbChangeState.parse(u.getChange());
+
+      if (expectedState == null) {
+        continue; // See above.
+      }
+
+      Account.Id accountId = u.getAccountId();
+      if (!expectedState.areDraftsUpToDate(
+          allUsersRepo.cmds, accountId)) {
+        throw new OrmConcurrencyException(String.format(
+            "cannot apply NoteDb updates for change %s;"
+            + " draft ref for account %s does not match %s",
+            u.getId(), accountId, expectedState.getChangeMetaId().name()));
+      }
+    }
   }
 
   private static <U extends AbstractChangeUpdate> void addUpdates(
@@ -353,8 +394,7 @@
     for (Map.Entry<String, Collection<U>> e : all.asMap().entrySet()) {
       String refName = e.getKey();
       Collection<U> updates = e.getValue();
-      ObjectId old = firstNonNull(
-          or.cmds.getObjectId(or.repo, refName), ObjectId.zeroId());
+      ObjectId old = or.cmds.get(refName).or(ObjectId.zeroId());
       // Only actually write to the ref if one of the updates explicitly allows
       // us to do so, i.e. it is known to represent a new change. This avoids
       // writing partial change meta if the change hasn't been backfilled yet.
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 2e79359..097fdf5 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
@@ -42,6 +42,15 @@
 
   public abstract boolean writeAccounts();
 
+  /**
+   * Whether to fail when reading any data from NoteDb.
+   * <p>
+   * Used in conjunction with {@link #readChanges()} for tests.
+   */
+  public boolean failOnLoad() {
+    return false;
+  }
+
   public boolean enabled() {
     return writeChanges() || readChanges()
         || writeAccounts() || readAccounts();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RepoSequence.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RepoSequence.java
index 382b1eb..3d17131 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RepoSequence.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RepoSequence.java
@@ -129,7 +129,7 @@
   }
 
   private void acquire() throws OrmException {
-    try (Repository repo = repoManager.openMetadataRepository(projectName);
+    try (Repository repo = repoManager.openRepository(projectName);
         RevWalk rw = new RevWalk(repo)) {
       TryAcquire attempt = new TryAcquire(repo, rw);
       RefUpdate.Result result = retryer.call(attempt);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ReviewerStateInternal.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ReviewerStateInternal.java
index 3bf2135..be7f8d5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ReviewerStateInternal.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ReviewerStateInternal.java
@@ -49,7 +49,7 @@
   private final FooterKey footerKey;
   private final ReviewerState state;
 
-  private ReviewerStateInternal(FooterKey footerKey, ReviewerState state) {
+  ReviewerStateInternal(FooterKey footerKey, ReviewerState state) {
     this.footerKey = footerKey;
     this.state = state;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNote.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNote.java
index e6d7107..73ad68e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNote.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNote.java
@@ -37,7 +37,7 @@
       "certificate version ".getBytes(UTF_8);
   // See org.eclipse.jgit.transport.PushCertificateParser.END_SIGNATURE
   private static final byte[] END_SIGNATURE =
-      "-----END PGP SIGNATURE-----".getBytes(UTF_8);
+      "-----END PGP SIGNATURE-----\n".getBytes(UTF_8);
 
   private static void trimLeadingEmptyLines(byte[] bytes, MutableInteger p) {
     while (p.value < bytes.length && bytes[p.value] == '\n') {
@@ -60,18 +60,19 @@
     return new String(bytes, start, p.value);
   }
 
+  final byte[] raw;
   final ImmutableList<PatchLineComment> comments;
   final String pushCert;
 
   RevisionNote(ChangeNoteUtil noteUtil, Change.Id changeId,
       ObjectReader reader, ObjectId noteId, boolean draftsOnly)
       throws ConfigInvalidException, IOException {
-    byte[] bytes = reader.open(noteId, OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ);
+    raw = reader.open(noteId, OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ);
     MutableInteger p = new MutableInteger();
-    trimLeadingEmptyLines(bytes, p);
+    trimLeadingEmptyLines(raw, p);
     if (!draftsOnly) {
-      pushCert = parsePushCert(changeId, bytes, p);
-      trimLeadingEmptyLines(bytes, p);
+      pushCert = parsePushCert(changeId, raw, p);
+      trimLeadingEmptyLines(raw, p);
     } else {
       pushCert = null;
     }
@@ -79,6 +80,6 @@
         ? PatchLineComment.Status.DRAFT
         : PatchLineComment.Status.PUBLISHED;
     comments = ImmutableList.copyOf(
-        noteUtil.parseNote(bytes, p, changeId, status));
+        noteUtil.parseNote(raw, p, changeId, status));
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNoteBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNoteBuilder.java
index 5179146..c8364d3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNoteBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNoteBuilder.java
@@ -15,16 +15,16 @@
 package com.google.gerrit.server.notedb;
 
 import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.gerrit.server.PatchLineCommentsUtil.PLC_ORDER;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
-import com.google.common.collect.Iterables;
+import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.RevId;
 
 import java.io.ByteArrayOutputStream;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -57,6 +57,7 @@
     }
   }
 
+  final byte[] baseRaw;
   final List<PatchLineComment> baseComments;
   final Map<PatchLineComment.Key, PatchLineComment> put;
   final Set<PatchLineComment.Key> delete;
@@ -65,10 +66,12 @@
 
   RevisionNoteBuilder(RevisionNote base) {
     if (base != null) {
+      baseRaw = base.raw;
       baseComments = base.comments;
       put = Maps.newHashMapWithExpectedSize(base.comments.size());
       pushCert = base.pushCert;
     } else {
+      baseRaw = new byte[0];
       baseComments = Collections.emptyList();
       put = new HashMap<>();
       pushCert = null;
@@ -99,14 +102,17 @@
       out.write('\n');
     }
 
-    List<PatchLineComment> all =
-        new ArrayList<>(baseComments.size() + put.size());
-    for (PatchLineComment c : Iterables.concat(baseComments, put.values())) {
-      if (!delete.contains(c.getKey())) {
-        all.add(c);
+    Multimap<PatchSet.Id, PatchLineComment> all = ArrayListMultimap.create();
+    for (PatchLineComment c : baseComments) {
+      if (!delete.contains(c.getKey()) && !put.containsKey(c.getKey())) {
+        all.put(c.getPatchSetId(), c);
       }
     }
-    Collections.sort(all, PLC_ORDER);
+    for (PatchLineComment c : put.values()) {
+      if (!delete.contains(c.getKey())) {
+        all.put(c.getPatchSetId(), c);
+      }
+    }
     noteUtil.buildNote(all, out);
     return out.toByteArray();
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/AutoMerger.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/AutoMerger.java
new file mode 100644
index 0000000..fd02042
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/AutoMerger.java
@@ -0,0 +1,235 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.diff.Sequence;
+import org.eclipse.jgit.dircache.DirCache;
+import org.eclipse.jgit.dircache.DirCacheBuilder;
+import org.eclipse.jgit.dircache.DirCacheEntry;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Config;
+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.merge.MergeFormatter;
+import org.eclipse.jgit.merge.MergeResult;
+import org.eclipse.jgit.merge.ResolveMerger;
+import org.eclipse.jgit.merge.ThreeWayMergeStrategy;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.util.TemporaryBuffer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+public class AutoMerger {
+  private static final Logger log = LoggerFactory.getLogger(AutoMerger.class);
+
+  private final PersonIdent gerritIdent;
+  private final boolean save;
+
+  @Inject
+  AutoMerger(
+      @GerritServerConfig Config cfg,
+      @GerritPersonIdent PersonIdent gerritIdent) {
+    save = cfg.getBoolean("change", null, "cacheAutomerge", true);
+    this.gerritIdent = gerritIdent;
+  }
+
+  /**
+   * Perform an auto-merge of the parents of the given merge commit.
+   *
+   * @return auto-merge commit or {@code null} if an auto-merge commit
+   *     couldn't be created. Headers of the returned RevCommit are parsed.
+   */
+  public RevCommit merge(Repository repo, RevWalk rw, final ObjectInserter ins,
+      RevCommit merge, ThreeWayMergeStrategy mergeStrategy)
+      throws IOException {
+    rw.parseHeaders(merge);
+    String hash = merge.name();
+    String refName = RefNames.REFS_CACHE_AUTOMERGE
+        + hash.substring(0, 2)
+        + "/"
+        + hash.substring(2);
+    Ref ref = repo.getRefDatabase().exactRef(refName);
+    if (ref != null && ref.getObjectId() != null) {
+      RevObject obj = rw.parseAny(ref.getObjectId());
+      if (obj instanceof RevCommit) {
+        return (RevCommit) obj;
+      }
+      return commit(repo, rw, ins, refName, obj, merge);
+    }
+
+    ResolveMerger m = (ResolveMerger) mergeStrategy.newMerger(repo, true);
+    DirCache dc = DirCache.newInCore();
+    m.setDirCache(dc);
+    m.setObjectInserter(new ObjectInserter.Filter() {
+      @Override
+      protected ObjectInserter delegate() {
+        return ins;
+      }
+
+      @Override
+      public void flush() {
+      }
+
+      @Override
+      public void close() {
+      }
+    });
+
+    boolean couldMerge;
+    try {
+      couldMerge = m.merge(merge.getParents());
+    } catch (IOException e) {
+      // It is not safe to continue further down in this method as throwing
+      // 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();
+
+    } else {
+      RevCommit ours = merge.getParent(0);
+      RevCommit theirs = merge.getParent(1);
+      rw.parseBody(ours);
+      rw.parseBody(theirs);
+      String oursMsg = ours.getShortMessage();
+      String theirsMsg = theirs.getShortMessage();
+
+      String oursName = String.format("HEAD   (%s %s)",
+          ours.abbreviate(6).name(),
+          oursMsg.substring(0, Math.min(oursMsg.length(), 60)));
+      String theirsName = String.format("BRANCH (%s %s)",
+          theirs.abbreviate(6).name(),
+          theirsMsg.substring(0, Math.min(theirsMsg.length(), 60)));
+
+      MergeFormatter fmt = new MergeFormatter();
+      Map<String, MergeResult<? extends Sequence>> r = m.getMergeResults();
+      Map<String, ObjectId> resolved = new HashMap<>();
+      for (Map.Entry<String, MergeResult<? extends Sequence>> entry : r.entrySet()) {
+        MergeResult<? extends Sequence> p = entry.getValue();
+        try (TemporaryBuffer buf =
+            new TemporaryBuffer.LocalFile(null, 10 * 1024 * 1024)) {
+          fmt.formatMerge(buf, p, "BASE", oursName, theirsName, UTF_8.name());
+          buf.close();
+
+          try (InputStream in = buf.openInputStream()) {
+            resolved.put(entry.getKey(), ins.insert(Constants.OBJ_BLOB, buf.length(), in));
+          }
+        }
+      }
+
+      DirCacheBuilder builder = dc.builder();
+      int cnt = dc.getEntryCount();
+      for (int i = 0; i < cnt;) {
+        DirCacheEntry entry = dc.getEntry(i);
+        if (entry.getStage() == 0) {
+          builder.add(entry);
+          i++;
+          continue;
+        }
+
+        int next = dc.nextEntry(i);
+        String path = entry.getPathString();
+        DirCacheEntry res = new DirCacheEntry(path);
+        if (resolved.containsKey(path)) {
+          // For a file with content merge conflict that we produced a result
+          // above on, collapse the file down to a single stage 0 with just
+          // the blob content, and a randomly selected mode (the lowest stage,
+          // which should be the merge base, or ours).
+          res.setFileMode(entry.getFileMode());
+          res.setObjectId(resolved.get(path));
+
+        } else if (next == i + 1) {
+          // If there is exactly one stage present, shouldn't be a conflict...
+          res.setFileMode(entry.getFileMode());
+          res.setObjectId(entry.getObjectId());
+
+        } else if (next == i + 2) {
+          // Two stages suggests a delete/modify conflict. Pick the higher
+          // stage as the automatic result.
+          entry = dc.getEntry(i + 1);
+          res.setFileMode(entry.getFileMode());
+          res.setObjectId(entry.getObjectId());
+
+        } else {
+          // 3 stage conflict, no resolve above
+          // Punt on the 3-stage conflict and show the base, for now.
+          res.setFileMode(entry.getFileMode());
+          res.setObjectId(entry.getObjectId());
+        }
+        builder.add(res);
+        i = next;
+      }
+      builder.finish();
+      treeId = dc.writeTree(ins);
+    }
+    ins.flush();
+
+    return commit(repo, rw, ins, refName, treeId, merge);
+  }
+
+  private RevCommit commit(Repository repo, RevWalk rw, ObjectInserter ins,
+      String refName, ObjectId tree, RevCommit merge) throws IOException {
+    rw.parseHeaders(merge);
+    // For maximum stability, choose a single ident using the committer time of
+    // the input commit, using the server name and timezone.
+    PersonIdent ident = new PersonIdent(
+        gerritIdent,
+        merge.getCommitterIdent().getWhen(),
+        gerritIdent.getTimeZone());
+    CommitBuilder cb = new CommitBuilder();
+    cb.setAuthor(ident);
+    cb.setCommitter(ident);
+    cb.setTreeId(tree);
+    cb.setMessage("Auto-merge of " + merge.name() + '\n');
+    for (RevCommit p : merge.getParents()) {
+      cb.addParentId(p);
+    }
+    ObjectId commitId;
+    commitId = ins.insert(cb);
+    if (save) {
+      ins.flush();
+
+      RefUpdate ru = repo.updateRef(refName);
+      ru.setNewObjectId(commitId);
+      ru.disableRefLog();
+      ru.forceUpdate();
+    }
+    return rw.parseCommit(commitId);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiff.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiff.java
index 46def59..60b97c7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiff.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiff.java
@@ -37,7 +37,7 @@
 public class IntraLineDiff implements Serializable {
   static final long serialVersionUID = IntraLineDiffKey.serialVersionUID;
 
-  public static enum Status implements CodedEnum {
+  public enum Status implements CodedEnum {
     EDIT_LIST('e'), DISABLED('D'), TIMEOUT('T'), ERROR('E');
 
     private final char code;
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 de70478..dd15cfc 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
@@ -40,7 +40,7 @@
 class IntraLineLoader implements Callable<IntraLineDiff> {
   static final Logger log = LoggerFactory.getLogger(IntraLineLoader.class);
 
-  static interface Factory {
+  interface Factory {
     IntraLineLoader create(IntraLineDiffKey key, IntraLineDiffArgs args);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineWeigher.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineWeigher.java
index f6cff15..7088fe8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineWeigher.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineWeigher.java
@@ -21,8 +21,8 @@
     Weigher<IntraLineDiffKey, IntraLineDiff> {
   @Override
   public int weigh(IntraLineDiffKey key, IntraLineDiff value) {
-    return 16 + 8*8 + 2*36     // Size of IntraLineDiffKey, 64 bit JVM
-        + 16 + 2*8 + 16+8+4+20 // Size of IntraLineDiff, 64 bit JVM
-        + (8 + 16 + 4*4) * value.getEdits().size();
+    return 16 + 8 * 8 + 2 * 36     // Size of IntraLineDiffKey, 64 bit JVM
+        + 16 + 2 * 8 + 16 + 8 + 4 + 20 // Size of IntraLineDiff, 64 bit JVM
+        + (8 + 16 + 4 * 4) * value.getEdits().size();
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java
index 2704be8..e570b3a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.patch;
 
-import com.google.gerrit.common.errors.CorruptEntityException;
 import com.google.gerrit.common.errors.NoSuchEntityException;
 import com.google.gerrit.reviewdb.client.Patch;
 
@@ -87,12 +86,11 @@
    * @param file the file index to extract.
    * @param line the line number to extract (1 based; 1 is the first line).
    * @return the string version of the file line.
-   * @throws CorruptEntityException the patch cannot be read.
    * @throws IOException the patch or complete file content cannot be read.
    * @throws NoSuchEntityException
    */
   public String getLine(final int file, final int line)
-      throws CorruptEntityException, IOException, NoSuchEntityException {
+      throws IOException, NoSuchEntityException {
     switch (file) {
       case 0:
         if (a == null) {
@@ -116,12 +114,11 @@
    *
    * @param file the file index to extract.
    * @return number of lines in file.
-   * @throws CorruptEntityException the patch cannot be read.
    * @throws IOException the patch or complete file content cannot be read.
    * @throws NoSuchEntityException the file is not exist.
    */
   public int getLineCount(final int file)
-      throws CorruptEntityException, IOException, NoSuchEntityException {
+      throws IOException, NoSuchEntityException {
     switch (file) {
       case 0:
         if (a == null) {
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 2b5e235..3266f01 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
@@ -130,17 +130,17 @@
   }
 
   int weigh() {
-    int size = 16 + 6*8 + 2*4 + 20 + 16+8+4+20;
+    int size = 16 + 6 * 8 + 2 * 4 + 20 + 16 + 8 + 4 + 20;
     size += stringSize(oldName);
     size += stringSize(newName);
     size += header.length;
-    size += (8 + 16 + 4*4) * edits.size();
+    size += (8 + 16 + 4 * 4) * edits.size();
     return size;
   }
 
   private static int stringSize(String str) {
     if (str != null) {
-      return 16 + 3*4 + 16 + str.length() * 2;
+      return 16 + 3 * 4 + 16 + str.length() * 2;
     }
     return 0;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
index 0200fa5..b04558d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
@@ -34,7 +34,7 @@
 import java.io.Serializable;
 
 public class PatchListKey implements Serializable {
-  static final long serialVersionUID = 19L;
+  static final long serialVersionUID = 20L;
 
   public static final BiMap<Whitespace, Character> WHITESPACE_TYPES = ImmutableBiMap.of(
       Whitespace.IGNORE_NONE, 'N',
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 63693e6..d316d33 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
@@ -24,7 +24,6 @@
 import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.Project;
-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;
@@ -39,22 +38,13 @@
 import org.eclipse.jgit.diff.HistogramDiff;
 import org.eclipse.jgit.diff.RawText;
 import org.eclipse.jgit.diff.RawTextComparator;
-import org.eclipse.jgit.diff.Sequence;
-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;
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.ObjectReader;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.merge.MergeFormatter;
-import org.eclipse.jgit.merge.MergeResult;
-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;
@@ -63,18 +53,14 @@
 import org.eclipse.jgit.revwalk.RevTree;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.treewalk.TreeWalk;
-import org.eclipse.jgit.util.TemporaryBuffer;
 import org.eclipse.jgit.util.io.DisabledOutputStream;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
-import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
@@ -94,6 +80,7 @@
   private final PatchListCache patchListCache;
   private final ThreeWayMergeStrategy mergeStrategy;
   private final ExecutorService diffExecutor;
+  private final AutoMerger autoMerger;
   private final PatchListKey key;
   private final Project.NameKey project;
   private final long timeoutMillis;
@@ -104,12 +91,14 @@
       PatchListCache plc,
       @GerritServerConfig Config cfg,
       @DiffExecutor ExecutorService de,
+      AutoMerger am,
       @Assisted PatchListKey k,
       @Assisted Project.NameKey p) {
     repoManager = mgr;
     patchListCache = plc;
     mergeStrategy = MergeUtil.getMergeStrategy(cfg);
     diffExecutor = de;
+    autoMerger = am;
     key = k;
     project = p;
     lock = new Object();
@@ -147,11 +136,12 @@
   private PatchList readPatchList(final PatchListKey key, final Repository repo)
       throws IOException, PatchListNotAvailableException {
     final RawTextComparator cmp = comparatorFor(key.getWhitespace());
-    try (ObjectReader reader = repo.newObjectReader();
+    try (ObjectInserter ins = repo.newObjectInserter();
+        ObjectReader reader = ins.newReader();
         RevWalk rw = new RevWalk(reader);
         DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE)) {
       final RevCommit b = rw.parseCommit(key.getNewId());
-      final RevObject a = aFor(key, repo, rw, b);
+      final RevObject a = aFor(key, repo, rw, ins, b);
 
       if (a == null) {
         // TODO(sop) Remove this case.
@@ -328,8 +318,8 @@
     }
   }
 
-  private RevObject aFor(final PatchListKey key,
-      final Repository repo, final RevWalk rw, final RevCommit b)
+  private RevObject aFor(PatchListKey key,
+      Repository repo, RevWalk rw, ObjectInserter ins, RevCommit b)
       throws IOException {
     if (key.getOldId() != null) {
       return rw.parseAny(key.getOldId());
@@ -344,153 +334,13 @@
         return r;
       }
       case 2:
-        return automerge(repo, rw, b, mergeStrategy);
+        return autoMerger.merge(repo, rw, ins, b, mergeStrategy);
       default:
         // TODO(sop) handle an octopus merge.
         return null;
     }
   }
 
-  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,
-      ThreeWayMergeStrategy mergeStrategy, boolean save) throws IOException {
-    String hash = b.name();
-    String refName = RefNames.REFS_CACHE_AUTOMERGE
-        + hash.substring(0, 2)
-        + "/"
-        + hash.substring(2);
-    Ref ref = repo.getRefDatabase().exactRef(refName);
-    if (ref != null && ref.getObjectId() != null) {
-      return rw.parseTree(ref.getObjectId());
-    }
-
-    ResolveMerger m = (ResolveMerger) mergeStrategy.newMerger(repo, true);
-    try (ObjectInserter ins = repo.newObjectInserter()) {
-      DirCache dc = DirCache.newInCore();
-      m.setDirCache(dc);
-      m.setObjectInserter(new ObjectInserter.Filter() {
-        @Override
-        protected ObjectInserter delegate() {
-          return ins;
-        }
-
-        @Override
-        public void flush() {
-        }
-
-        @Override
-        public void close() {
-        }
-      });
-
-      boolean couldMerge;
-      try {
-        couldMerge = m.merge(b.getParents());
-      } catch (IOException e) {
-        // It is not safe to continue further down in this method as throwing
-        // 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();
-
-      } else {
-        RevCommit ours = b.getParent(0);
-        RevCommit theirs = b.getParent(1);
-        rw.parseBody(ours);
-        rw.parseBody(theirs);
-        String oursMsg = ours.getShortMessage();
-        String theirsMsg = theirs.getShortMessage();
-
-        String oursName = String.format("HEAD   (%s %s)",
-            ours.abbreviate(6).name(),
-            oursMsg.substring(0, Math.min(oursMsg.length(), 60)));
-        String theirsName = String.format("BRANCH (%s %s)",
-            theirs.abbreviate(6).name(),
-            theirsMsg.substring(0, Math.min(theirsMsg.length(), 60)));
-
-        MergeFormatter fmt = new MergeFormatter();
-        Map<String, MergeResult<? extends Sequence>> r = m.getMergeResults();
-        Map<String, ObjectId> resolved = new HashMap<>();
-        for (Map.Entry<String, MergeResult<? extends Sequence>> entry : r.entrySet()) {
-          MergeResult<? extends Sequence> p = entry.getValue();
-          try (TemporaryBuffer buf =
-              new TemporaryBuffer.LocalFile(null, 10 * 1024 * 1024)) {
-            fmt.formatMerge(buf, p, "BASE", oursName, theirsName, UTF_8.name());
-            buf.close();
-
-            try (InputStream in = buf.openInputStream()) {
-              resolved.put(entry.getKey(), ins.insert(Constants.OBJ_BLOB, buf.length(), in));
-            }
-          }
-        }
-
-        DirCacheBuilder builder = dc.builder();
-        int cnt = dc.getEntryCount();
-        for (int i = 0; i < cnt;) {
-          DirCacheEntry entry = dc.getEntry(i);
-          if (entry.getStage() == 0) {
-            builder.add(entry);
-            i++;
-            continue;
-          }
-
-          int next = dc.nextEntry(i);
-          String path = entry.getPathString();
-          DirCacheEntry res = new DirCacheEntry(path);
-          if (resolved.containsKey(path)) {
-            // For a file with content merge conflict that we produced a result
-            // above on, collapse the file down to a single stage 0 with just
-            // the blob content, and a randomly selected mode (the lowest stage,
-            // which should be the merge base, or ours).
-            res.setFileMode(entry.getFileMode());
-            res.setObjectId(resolved.get(path));
-
-          } else if (next == i + 1) {
-            // If there is exactly one stage present, shouldn't be a conflict...
-            res.setFileMode(entry.getFileMode());
-            res.setObjectId(entry.getObjectId());
-
-          } else if (next == i + 2) {
-            // Two stages suggests a delete/modify conflict. Pick the higher
-            // stage as the automatic result.
-            entry = dc.getEntry(i + 1);
-            res.setFileMode(entry.getFileMode());
-            res.setObjectId(entry.getObjectId());
-
-          } else { // 3 stage conflict, no resolve above
-            // Punt on the 3-stage conflict and show the base, for now.
-            res.setFileMode(entry.getFileMode());
-            res.setObjectId(entry.getObjectId());
-          }
-          builder.add(res);
-          i = next;
-        }
-        builder.finish();
-        treeId = dc.writeTree(ins);
-      }
-      ins.flush();
-
-      if (save) {
-        RefUpdate update = repo.updateRef(refName);
-        update.setNewObjectId(treeId);
-        update.disableRefLog();
-        update.forceUpdate();
-      }
-
-      return rw.lookupTree(treeId);
-    }
-  }
-
   private static ObjectId emptyTree(final Repository repo) throws IOException {
     try (ObjectInserter oi = repo.newObjectInserter()) {
       ObjectId id = oi.insert(Constants.OBJ_TREE, new byte[] {});
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListWeigher.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListWeigher.java
index d715246..2362986 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListWeigher.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListWeigher.java
@@ -20,8 +20,8 @@
 public class PatchListWeigher implements Weigher<PatchListKey, PatchList> {
   @Override
   public int weigh(PatchListKey key, PatchList value) {
-    int size = 16 + 4*8 + 2*36 // Size of PatchListKey, 64 bit JVM
-        + 16 + 3*8 + 3*4 + 20; // Size of PatchList, 64 bit JVM
+    int size = 16 + 4 * 8 + 2 * 36 // Size of PatchListKey, 64 bit JVM
+        + 16 + 3 * 8 + 3 * 4 + 20; // Size of PatchList, 64 bit JVM
     for (PatchListEntry e : value.getPatches()) {
       size += e.weigh();
     }
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
index e239654..ee8f963 100644
--- 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
@@ -17,7 +17,6 @@
 import static com.google.common.base.Preconditions.checkState;
 
 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;
@@ -26,6 +25,7 @@
 import java.io.IOException;
 import java.lang.annotation.Annotation;
 import java.lang.reflect.Modifier;
+import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 import java.util.jar.Manifest;
@@ -85,7 +85,7 @@
         ImmutableMap.builder();
 
     for (Class<? extends Annotation> annotation : annotations) {
-      Set<ExtensionMetaData> classMetaDataSet = Sets.newHashSet();
+      Set<ExtensionMetaData> classMetaDataSet = new HashSet<>();
       result.put(annotation, classMetaDataSet);
 
       for (Class<?> clazz : preloadedClasses) {
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 0eaddb3..438add6 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
@@ -14,16 +14,18 @@
 
 package com.google.gerrit.server.plugins;
 
+import static com.google.gerrit.extensions.webui.JavaScriptPlugin.STATIC_INIT_JS;
 import static com.google.gerrit.server.plugins.AutoRegisterUtil.calculateBindAnnotation;
 import static com.google.gerrit.server.plugins.PluginGuiceEnvironment.is;
 
 import com.google.common.collect.LinkedListMultimap;
 import com.google.common.collect.Multimap;
-import com.google.common.collect.Sets;
 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.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.webui.JavaScriptPlugin;
+import com.google.gerrit.extensions.webui.WebUiPlugin;
 import com.google.gerrit.server.plugins.PluginContentScanner.ExtensionMetaData;
 import com.google.inject.AbstractModule;
 import com.google.inject.Module;
@@ -37,6 +39,7 @@
 import java.lang.annotation.Annotation;
 import java.lang.reflect.ParameterizedType;
 import java.util.Arrays;
+import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 
@@ -48,10 +51,11 @@
   private final PluginContentScanner scanner;
   private final ClassLoader classLoader;
   private final ModuleGenerator sshGen;
-  private final HttpModuleGenerator httpGen;
+  private final ModuleGenerator httpGen;
 
   private Set<Class<?>> sysSingletons;
   private Multimap<TypeLiteral<?>, Class<?>> sysListen;
+  private String initJs;
 
   Module sysModule;
   Module sshModule;
@@ -70,19 +74,20 @@
         : new ModuleGenerator.NOP();
     this.httpGen = env.hasHttpModule()
         ? env.newHttpModuleGenerator()
-        : new HttpModuleGenerator.NOP();
+        : new ModuleGenerator.NOP();
   }
 
   AutoRegisterModules discover() throws InvalidPluginException {
-    sysSingletons = Sets.newHashSet();
+    sysSingletons = new HashSet<>();
     sysListen = LinkedListMultimap.create();
+    initJs = null;
 
     sshGen.setPluginName(pluginName);
     httpGen.setPluginName(pluginName);
 
     scan();
 
-    if (!sysSingletons.isEmpty() || !sysListen.isEmpty()) {
+    if (!sysSingletons.isEmpty() || !sysListen.isEmpty() || initJs != null) {
       sysModule = makeSystemModule();
     }
     sshModule = sshGen.create();
@@ -107,6 +112,10 @@
           Annotation n = calculateBindAnnotation(impl);
           bind(type).annotatedWith(n).to(impl);
         }
+        if (initJs != null) {
+          DynamicSet.bind(binder(), WebUiPlugin.class)
+              .toInstance(new JavaScriptPlugin(initJs));
+        }
       }
     };
   }
@@ -120,18 +129,20 @@
     for (ExtensionMetaData listener : extensions.get(Listen.class)) {
       listen(listener);
     }
-    exportInitJs();
+    if (env.hasHttpModule()) {
+      exportInitJs();
+    }
   }
 
   private void exportInitJs() {
     try {
-      if (scanner.getEntry(JavaScriptPlugin.STATIC_INIT_JS).isPresent()) {
-        httpGen.export(JavaScriptPlugin.INIT_JS);
+      if (scanner.getEntry(STATIC_INIT_JS).isPresent()) {
+        initJs = STATIC_INIT_JS;
       }
     } catch (IOException e) {
       log.warn(String.format("Cannot access %s from plugin %s: "
           + "JavaScript auto-discovered plugin will not be registered",
-          JavaScriptPlugin.STATIC_INIT_JS, pluginName), e);
+          STATIC_INIT_JS, pluginName), e);
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/HttpModuleGenerator.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/HttpModuleGenerator.java
deleted file mode 100644
index dd0ce67..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/HttpModuleGenerator.java
+++ /dev/null
@@ -1,28 +0,0 @@
-// 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;
-
-
-public interface HttpModuleGenerator extends ModuleGenerator {
-  void export(String javascript);
-
-  static class NOP extends ModuleGenerator.NOP
-      implements HttpModuleGenerator {
-    @Override
-    public void export(String javascript) {
-      // do nothing
-    }
-  }
-}
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 d94df9c..1f612a3 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
@@ -27,7 +27,6 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Multimap;
-import com.google.common.collect.Sets;
 
 import org.eclipse.jgit.util.IO;
 import org.objectweb.asm.AnnotationVisitor;
@@ -47,6 +46,8 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -77,10 +78,10 @@
   public Map<Class<? extends Annotation>, Iterable<ExtensionMetaData>> scan(
       String pluginName, Iterable<Class<? extends Annotation>> annotations)
       throws InvalidPluginException {
-    Set<String> descriptors = Sets.newHashSet();
+    Set<String> descriptors = new HashSet<>();
     Multimap<String, JarScanner.ClassData> rawMap = ArrayListMultimap.create();
     Map<Class<? extends Annotation>, String> classObjToClassDescr =
-        Maps.newHashMap();
+        new HashMap<>();
 
     for (Class<? extends Annotation> annotation : annotations) {
       String descriptor = Type.getType(annotation).getDescriptor();
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 ea81f17..544cc5b 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
@@ -30,7 +30,7 @@
 import java.nio.file.Path;
 
 class JsPlugin extends Plugin {
-  private Injector httpInjector;
+  private Injector sysInjector;
 
   JsPlugin(String name, Path srcFile, PluginUser pluginUser,
       FileSnapshot snapshot) {
@@ -52,7 +52,7 @@
   public void start(PluginGuiceEnvironment env) throws Exception {
     manager = new LifecycleManager();
     String fileName = getSrcFile().getFileName().toString();
-    httpInjector =
+    sysInjector =
         Guice.createInjector(new StandaloneJsPluginModule(getName(), fileName));
     manager.start();
   }
@@ -61,13 +61,13 @@
   protected void stop(PluginGuiceEnvironment env) {
     if (manager != null) {
       manager.stop();
-      httpInjector = null;
+      sysInjector = null;
     }
   }
 
   @Override
   public Injector getSysInjector() {
-    return null;
+    return sysInjector;
   }
 
   @Override
@@ -79,7 +79,7 @@
   @Override
   @Nullable
   public Injector getHttpInjector() {
-    return httpInjector;
+    return null;
   }
 
   @Override
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 0146f84..ff29785 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
@@ -16,7 +16,6 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.restapi.RestReadView;
@@ -34,6 +33,7 @@
 import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
+import java.util.TreeMap;
 
 /** List the installed plugins. */
 @RequiresCapability(GlobalCapability.VIEW_PLUGINS)
@@ -63,7 +63,7 @@
     });
 
     if (stdout == null) {
-      Map<String, PluginInfo> output = Maps.newTreeMap();
+      Map<String, PluginInfo> output = new TreeMap<>();
       for (Plugin p : plugins) {
         PluginInfo info = new PluginInfo(p);
         output.put(p.getName(), info);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ModuleGenerator.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ModuleGenerator.java
index ae8bb0c..7818591 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ModuleGenerator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ModuleGenerator.java
@@ -27,7 +27,7 @@
 
   Module create() throws InvalidPluginException;
 
-  static class NOP implements ModuleGenerator {
+  class NOP implements ModuleGenerator {
 
     @Override
     public void setPluginName(String name) {
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 588bc6d..4fe0c2a 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
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.plugins;
 
 import com.google.common.base.Strings;
-import com.google.common.collect.Lists;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.extensions.registration.ReloadableRegistrationHandle;
@@ -26,13 +25,14 @@
 import org.eclipse.jgit.internal.storage.file.FileSnapshot;
 
 import java.nio.file.Path;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.jar.Attributes;
 import java.util.jar.Manifest;
 
 public abstract class Plugin {
-  public static enum ApiType {
+  public enum ApiType {
     EXTENSION, PLUGIN, JS
   }
 
@@ -146,7 +146,7 @@
     if (manager != null) {
       if (handle instanceof ReloadableRegistrationHandle) {
         if (reloadableHandles == null) {
-          reloadableHandles = Lists.newArrayList();
+          reloadableHandles = new ArrayList<>();
         }
         reloadableHandles.add((ReloadableRegistrationHandle<?>) handle);
       }
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
index 1d9cd0e..15bb92f 100644
--- 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
@@ -74,7 +74,7 @@
    * provided by a plugin to extend an existing
    * extension point in Gerrit.
    */
-  public static class ExtensionMetaData {
+  class ExtensionMetaData {
     public final String className;
     public final String annotationValue;
 
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 87eca8b..5e7d9ee 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
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.plugins;
 
+import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.gerrit.extensions.registration.PrivateInternals_DynamicTypes.dynamicItemsOf;
 import static com.google.gerrit.extensions.registration.PrivateInternals_DynamicTypes.dynamicMapsOf;
 import static com.google.gerrit.extensions.registration.PrivateInternals_DynamicTypes.dynamicSetsOf;
@@ -21,8 +22,6 @@
 import com.google.common.collect.LinkedListMultimap;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Sets;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.annotations.RootRelative;
 import com.google.gerrit.extensions.events.LifecycleListener;
@@ -34,6 +33,7 @@
 import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.extensions.registration.ReloadableRegistrationHandle;
 import com.google.gerrit.extensions.systemstatus.ServerInformation;
+import com.google.gerrit.extensions.webui.WebUiPlugin;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.server.util.PluginRequestContext;
 import com.google.gerrit.server.util.RequestContext;
@@ -52,7 +52,11 @@
 
 import java.lang.annotation.Annotation;
 import java.lang.reflect.ParameterizedType;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Iterator;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -85,7 +89,7 @@
   private Module httpModule;
 
   private Provider<ModuleGenerator> sshGen;
-  private Provider<HttpModuleGenerator> httpGen;
+  private Provider<ModuleGenerator> httpGen;
 
   private Map<TypeLiteral<?>, DynamicItem<?>> sysItems;
   private Map<TypeLiteral<?>, DynamicItem<?>> sshItems;
@@ -197,15 +201,27 @@
 
   public void setHttpInjector(Injector injector) {
     httpModule = copy(injector);
-    httpGen = injector.getProvider(HttpModuleGenerator.class);
+    httpGen = injector.getProvider(ModuleGenerator.class);
     httpItems = dynamicItemsOf(injector);
-    httpSets = dynamicSetsOf(injector);
+    httpSets = httpDynamicSetsOf(injector);
     httpMaps = dynamicMapsOf(injector);
     onStart.addAll(listeners(injector, StartPluginListener.class));
     onStop.addAll(listeners(injector, StopPluginListener.class));
     onReload.addAll(listeners(injector, ReloadPluginListener.class));
   }
 
+  private Map<TypeLiteral<?>, DynamicSet<?>> httpDynamicSetsOf(Injector i) {
+    // Copy binding of DynamicSet<WebUiPlugin> from sysInjector to HTTP.
+    // This supports older plugins that bound a plugin in the HttpModule.
+    TypeLiteral<WebUiPlugin> key = TypeLiteral.get(WebUiPlugin.class);
+    DynamicSet<?> web = sysSets.get(key);
+    checkNotNull(web, "DynamicSet<WebUiPlugin> exists in sysInjector");
+
+    Map<TypeLiteral<?>, DynamicSet<?>> m = new HashMap<>(dynamicSetsOf(i));
+    m.put(key, web);
+    return Collections.unmodifiableMap(m);
+  }
+
   boolean hasHttpModule() {
     return httpModule != null;
   }
@@ -214,7 +230,7 @@
     return httpModule;
   }
 
-  HttpModuleGenerator newHttpModuleGenerator() {
+  ModuleGenerator newHttpModuleGenerator() {
     return httpGen.get();
   }
 
@@ -331,7 +347,7 @@
       PrivateInternals_DynamicMapImpl<Object> map =
           (PrivateInternals_DynamicMapImpl<Object>) e.getValue();
 
-      Map<Annotation, ReloadableRegistrationHandle<?>> am = Maps.newHashMap();
+      Map<Annotation, ReloadableRegistrationHandle<?>> am = new HashMap<>();
       for (ReloadableRegistrationHandle<?> h : oldHandles.get(type)) {
         Annotation a = h.getKey().getAnnotation();
         if (a != null && !UNIQUE_ANNOTATION.isInstance(a)) {
@@ -386,7 +402,7 @@
       // Index all old handles that match this DynamicSet<T> keyed by
       // annotations. Ignore the unique annotations, thereby favoring
       // the @Named annotations or some other non-unique naming.
-      Map<Annotation, ReloadableRegistrationHandle<?>> am = Maps.newHashMap();
+      Map<Annotation, ReloadableRegistrationHandle<?>> am = new HashMap<>();
       List<ReloadableRegistrationHandle<?>> old = oldHandles.get(type);
       Iterator<ReloadableRegistrationHandle<?>> oi = old.iterator();
       while (oi.hasNext()) {
@@ -472,7 +488,7 @@
   private static <T> void replace(Plugin newPlugin,
       ReloadableRegistrationHandle<T> h, Binding<T> b) {
     RegistrationHandle n = h.replace(b.getKey(), b.getProvider());
-    if (n != null){
+    if (n != null) {
       newPlugin.add(n);
     }
   }
@@ -494,8 +510,8 @@
   }
 
   private Module copy(Injector src) {
-    Set<TypeLiteral<?>> dynamicTypes = Sets.newHashSet();
-    Set<TypeLiteral<?>> dynamicItemTypes = Sets.newHashSet();
+    Set<TypeLiteral<?>> dynamicTypes = new HashSet<>();
+    Set<TypeLiteral<?>> dynamicItemTypes = new HashSet<>();
     for (Map.Entry<Key<?>, Binding<?>> e : src.getBindings().entrySet()) {
       TypeLiteral<?> type = e.getKey().getTypeLiteral();
       if (type.getRawType() == DynamicItem.class) {
@@ -508,7 +524,7 @@
       }
     }
 
-    final Map<Key<?>, Binding<?>> bindings = Maps.newLinkedHashMap();
+    final Map<Key<?>, Binding<?>> bindings = new LinkedHashMap<>();
     for (Map.Entry<Key<?>, Binding<?>> e : src.getBindings().entrySet()) {
       if (dynamicTypes.contains(e.getKey().getTypeLiteral())
           && e.getKey().getAnnotation() != null) {
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 5006401..075069f 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
@@ -25,7 +25,6 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Multimap;
-import com.google.common.collect.Queues;
 import com.google.common.collect.Sets;
 import com.google.common.io.ByteStreams;
 import com.google.gerrit.extensions.annotations.PluginName;
@@ -56,10 +55,12 @@
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.AbstractMap;
+import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
@@ -72,7 +73,6 @@
 
 @Singleton
 public class PluginLoader implements LifecycleListener {
-  static final String PLUGIN_TMP_PREFIX = "plugin_";
   static final Logger log = LoggerFactory.getLogger(PluginLoader.class);
 
   public String getPluginName(Path srcPath) {
@@ -114,8 +114,8 @@
     pluginUserFactory = puf;
     running = Maps.newConcurrentMap();
     disabled = Maps.newConcurrentMap();
-    broken = Maps.newHashMap();
-    toCleanup = Queues.newArrayDeque();
+    broken = new HashMap<>();
+    toCleanup = new ArrayDeque<>();
     cleanupHandles = Maps.newConcurrentMap();
     cleaner = pct;
     urlProvider = provider;
@@ -623,7 +623,7 @@
     for (String name : pluginPaths.keys()) {
       for (Path pluginPath : pluginPaths.asMap().get(name)) {
         if (!pluginPath.getFileName().toString().endsWith(".disabled")) {
-          assert(!activePlugins.containsKey(name));
+          assert !activePlugins.containsKey(name);
           activePlugins.put(name, pluginPath);
         }
       }
@@ -656,11 +656,11 @@
         continue;
       }
       Path winner = Iterables.getFirst(enabled, null);
-      assert(winner != null);
+      assert winner != null;
       // Disable all loser plugins by renaming their file names to
       // "file.disabled" and replace the disabled files in the multimap.
-      Collection<Path> elementsToRemove = Lists.newArrayList();
-      Collection<Path> elementsToAdd = Lists.newArrayList();
+      Collection<Path> elementsToRemove = new ArrayList<>();
+      Collection<Path> elementsToAdd = new ArrayList<>();
       for (Path loser : Iterables.skip(enabled, 1)) {
         log.warn(String.format("Plugin <%s> was disabled, because"
              + " another plugin <%s>"
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPlugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPlugin.java
index 1ae9351..59ed261 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPlugin.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPlugin.java
@@ -30,27 +30,13 @@
 
 import java.io.IOException;
 import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.jar.Attributes;
 import java.util.jar.Manifest;
 
 public class ServerPlugin extends Plugin {
-
-  /** Unique key that changes whenever a plugin reloads. */
-  public static final class CacheKey {
-    private final String name;
-
-    CacheKey(String name) {
-      this.name = name;
-    }
-
-    @Override
-    public String toString() {
-      int id = System.identityHashCode(this);
-      return String.format("Plugin[%s@%x]", name, id);
-    }
-  }
-
   private final Manifest manifest;
   private final PluginContentScanner scanner;
   private final Path dataDir;
@@ -100,7 +86,7 @@
       this.sysModule = load(sysName, classLoader);
       this.sshModule = load(sshName, classLoader);
       this.httpModule = load(httpName, classLoader);
-    } catch(ClassNotFoundException e) {
+    } catch (ClassNotFoundException e) {
       throw new InvalidPluginException("Unable to load plugin Guice Modules", e);
     }
   }
@@ -122,10 +108,6 @@
     return (Class<? extends Module>) clazz;
   }
 
-  Path getSrcJar() {
-    return getSrcFile();
-  }
-
   Path getDataDir() {
     return dataDir;
   }
@@ -198,7 +180,7 @@
     }
 
     if (env.hasSshModule()) {
-      List<Module> modules = Lists.newLinkedList();
+      List<Module> modules = new LinkedList<>();
       if (getApiType() == ApiType.PLUGIN) {
         modules.add(env.getSshModule());
       }
@@ -214,7 +196,7 @@
     }
 
     if (env.hasHttpModule()) {
-      List<Module> modules = Lists.newLinkedList();
+      List<Module> modules = new LinkedList<>();
       if (getApiType() == ApiType.PLUGIN) {
         modules.add(env.getHttpModule());
       }
@@ -279,7 +261,7 @@
     if (serverManager != null) {
       if (handle instanceof ReloadableRegistrationHandle) {
         if (reloadableHandles == null) {
-          reloadableHandles = Lists.newArrayList();
+          reloadableHandles = new ArrayList<>();
         }
         reloadableHandles.add((ReloadableRegistrationHandle<?>) handle);
       }
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
index bc2432b..068d73c 100644
--- 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
@@ -37,7 +37,7 @@
   /**
    * Descriptor of the Plugin that ServerPluginProvider has to load.
    */
-  public class PluginDescription {
+  class PluginDescription {
     public final PluginUser user;
     public final String canonicalUrl;
     public final Path dataDir;
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 0c540a7..d24a7e5 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
@@ -16,7 +16,6 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
-import com.google.common.collect.Maps;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ActionInfo;
@@ -34,6 +33,7 @@
 import com.google.inject.util.Providers;
 
 import java.util.Arrays;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.TreeMap;
@@ -131,7 +131,7 @@
     this.submitType = p.getSubmitType();
     this.state = p.getState() != com.google.gerrit.extensions.client.ProjectState.ACTIVE ? p.getState() : null;
 
-    this.commentlinks = Maps.newLinkedHashMap();
+    this.commentlinks = new LinkedHashMap<>();
     for (CommentLinkInfo cl : projectState.getCommentLinks()) {
       this.commentlinks.put(cl.name, cl);
     }
@@ -140,7 +140,7 @@
         getPluginConfig(control.getProjectState(), pluginConfigEntries,
             cfgFactory, allProjects);
 
-    actions = Maps.newTreeMap();
+    actions = new TreeMap<>();
     for (UiAction.Description d : UiActions.from(
         views, new ProjectResource(control),
         Providers.of(control.getUser()))) {
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 83f69ee..bc4d8f3 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
@@ -64,7 +64,7 @@
     public String revision;
   }
 
-  public static interface Factory {
+  public interface Factory {
     CreateBranch create(String ref);
   }
 
@@ -259,7 +259,7 @@
 
     public static final String MESSAGE = "Invalid Revision";
 
-    public InvalidRevisionException() {
+    InvalidRevisionException() {
       super(MESSAGE);
     }
   }
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 fec858a..a73ed0e 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
@@ -80,7 +80,7 @@
 
 @RequiresCapability(GlobalCapability.CREATE_PROJECT)
 public class CreateProject implements RestModifyView<TopLevelResource, ProjectInput> {
-  public static interface Factory {
+  public interface Factory {
     CreateProject create(String name);
   }
 
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 4646e3b..459f20c 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
@@ -49,6 +49,7 @@
 import org.eclipse.jgit.lib.Repository;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.List;
 
 @Singleton
@@ -207,7 +208,7 @@
     Boolean isDefault;
 
     String title;
-    List<Section> sections = Lists.newArrayList();
+    List<Section> sections = new ArrayList<>();
 
     DashboardInfo(String ref, String name) {
       this.ref = ref;
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 bda2c71..b4c25b4 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
@@ -39,7 +39,7 @@
 import java.io.IOException;
 
 @Singleton
-public class DeleteBranch implements RestModifyView<BranchResource, Input>{
+public class DeleteBranch implements RestModifyView<BranchResource, Input> {
   private static final Logger log = LoggerFactory.getLogger(DeleteBranch.class);
   private static final int MAX_LOCK_FAILURE_CALLS = 10;
   private static final long SLEEP_ON_LOCK_FAILURE_MS = 15;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetAccess.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetAccess.java
new file mode 100644
index 0000000..b8c5fd8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetAccess.java
@@ -0,0 +1,270 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
+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.common.data.RefConfigSection;
+import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.extensions.api.access.AccessSectionInfo;
+import com.google.gerrit.extensions.api.access.PermissionInfo;
+import com.google.gerrit.extensions.api.access.PermissionRuleInfo;
+import com.google.gerrit.extensions.api.access.ProjectAccessInfo;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+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.account.GroupBackend;
+import com.google.gerrit.server.account.GroupControl;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+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;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+
+@Singleton
+public class GetAccess implements RestReadView<ProjectResource> {
+
+  private static final ImmutableMap<PermissionRule.Action, PermissionRuleInfo.Action> ACTION_TYPE =
+      Maps.immutableEnumMap(
+          new ImmutableMap.Builder<PermissionRule.Action, PermissionRuleInfo.Action>()
+              .put(PermissionRule.Action.ALLOW, PermissionRuleInfo.Action.ALLOW)
+              .put(PermissionRule.Action.BATCH, PermissionRuleInfo.Action.BATCH)
+              .put(PermissionRule.Action.BLOCK, PermissionRuleInfo.Action.BLOCK)
+              .put(PermissionRule.Action.DENY, PermissionRuleInfo.Action.DENY)
+              .put(PermissionRule.Action.INTERACTIVE,
+                  PermissionRuleInfo.Action.INTERACTIVE)
+              .build());
+
+  private final Provider<CurrentUser> self;
+  private final GroupControl.Factory groupControlFactory;
+  private final AllProjectsName allProjectsName;
+  private final ProjectJson projectJson;
+  private final ProjectCache projectCache;
+  private final MetaDataUpdate.Server metaDataUpdateFactory;
+  private final ProjectControl.GenericFactory projectControlFactory;
+  private final GroupBackend groupBackend;
+
+  @Inject
+  public GetAccess(Provider<CurrentUser> self,
+      GroupControl.Factory groupControlFactory,
+      AllProjectsName allProjectsName,
+      ProjectCache projectCache,
+      MetaDataUpdate.Server metaDataUpdateFactory,
+      ProjectJson projectJson,
+      ProjectControl.GenericFactory projectControlFactory,
+      GroupBackend groupBackend) {
+    this.self = self;
+    this.groupControlFactory = groupControlFactory;
+    this.allProjectsName = allProjectsName;
+    this.projectJson = projectJson;
+    this.projectCache = projectCache;
+    this.projectControlFactory = projectControlFactory;
+    this.metaDataUpdateFactory = metaDataUpdateFactory;
+    this.groupBackend = groupBackend;
+  }
+
+  public ProjectAccessInfo apply(Project.NameKey nameKey)
+      throws ResourceNotFoundException, ResourceConflictException, IOException {
+    try {
+      return this.apply(new ProjectResource(
+          projectControlFactory.controlFor(nameKey, self.get())));
+    } catch (NoSuchProjectException e) {
+      throw new ResourceNotFoundException(nameKey.get());
+    }
+  }
+
+  @Override
+  public ProjectAccessInfo apply(ProjectResource rsrc)
+      throws ResourceNotFoundException, ResourceConflictException, IOException {
+    // Load the current configuration from the repository, ensuring it's the most
+    // recent version available. If it differs from what was in the project
+    // state, force a cache flush now.
+    //
+    Project.NameKey projectName = rsrc.getNameKey();
+    ProjectAccessInfo info = new ProjectAccessInfo();
+    ProjectConfig config;
+    ProjectControl pc = open(projectName);
+    RefControl metaConfigControl = pc.controlForRef(RefNames.REFS_CONFIG);
+    try (MetaDataUpdate md = metaDataUpdateFactory.create(projectName)) {
+      config = ProjectConfig.read(md);
+
+      if (config.updateGroupNames(groupBackend)) {
+        md.setMessage("Update group names\n");
+        config.commit(md);
+        projectCache.evict(config.getProject());
+        pc = open(projectName);
+      } else if (config.getRevision() != null
+          && !config.getRevision().equals(
+          pc.getProjectState().getConfig().getRevision())) {
+        projectCache.evict(config.getProject());
+        pc = open(projectName);
+      }
+    } catch (ConfigInvalidException e) {
+      throw new ResourceConflictException(e.getMessage());
+    } catch (RepositoryNotFoundException e) {
+      throw new ResourceNotFoundException(rsrc.getName());
+    }
+
+    info.local = new HashMap<>();
+    info.ownerOf = new HashSet<>();
+    Map<AccountGroup.UUID, Boolean> visibleGroups = new HashMap<>();
+
+    for (AccessSection section : config.getAccessSections()) {
+      String name = section.getName();
+      if (AccessSection.GLOBAL_CAPABILITIES.equals(name)) {
+        if (pc.isOwner()) {
+          info.local.put(name, createAccessSection(section));
+          info.ownerOf.add(name);
+
+        } else if (metaConfigControl.isVisible()) {
+          info.local.put(section.getName(), createAccessSection(section));
+        }
+
+      } else if (RefConfigSection.isValid(name)) {
+        RefControl rc = pc.controlForRef(name);
+        if (rc.isOwner()) {
+          info.local.put(name, createAccessSection(section));
+          info.ownerOf.add(name);
+
+        } else if (metaConfigControl.isVisible()) {
+          info.local.put(name, createAccessSection(section));
+
+        } else if (rc.isVisible()) {
+          // Filter the section to only add rules describing groups that
+          // are visible to the current-user. This includes any group the
+          // user is a member of, as well as groups they own or that
+          // are visible to all users.
+
+          AccessSection dst = null;
+          for (Permission srcPerm : section.getPermissions()) {
+            Permission dstPerm = null;
+
+            for (PermissionRule srcRule : srcPerm.getRules()) {
+              AccountGroup.UUID group = srcRule.getGroup().getUUID();
+              if (group == null) {
+                continue;
+              }
+
+              Boolean canSeeGroup = visibleGroups.get(group);
+              if (canSeeGroup == null) {
+                try {
+                  canSeeGroup = groupControlFactory.controlFor(group)
+                      .isVisible();
+                } catch (NoSuchGroupException e) {
+                  canSeeGroup = Boolean.FALSE;
+                }
+                visibleGroups.put(group, canSeeGroup);
+              }
+
+              if (canSeeGroup) {
+                if (dstPerm == null) {
+                  if (dst == null) {
+                    dst = new AccessSection(name);
+                    info.local.put(name, createAccessSection(dst));
+                  }
+                  dstPerm = dst.getPermission(srcPerm.getName(), true);
+                }
+                dstPerm.add(srcRule);
+              }
+            }
+          }
+        }
+      }
+    }
+
+    if (info.ownerOf.isEmpty() && pc.isOwnerAnyRef()) {
+      // Special case: If the section list is empty, this project has no current
+      // access control information. Rely on what ProjectControl determines
+      // is ownership, which probably means falling back to site administrators.
+      info.ownerOf.add(AccessSection.ALL);
+    }
+
+    if (config.getRevision() != null) {
+      info.revision = config.getRevision().name();
+    }
+
+    ProjectState parent =
+        Iterables.getFirst(pc.getProjectState().parents(), null);
+    if (parent != null) {
+      info.inheritsFrom = projectJson.format(parent.getProject());
+    }
+
+    if (pc.getProject().getNameKey().equals(allProjectsName)) {
+      if (pc.isOwner()) {
+        info.ownerOf.add(AccessSection.GLOBAL_CAPABILITIES);
+      }
+    }
+
+    info.isOwner = toBoolean(pc.isOwner());
+    info.canUpload = toBoolean(pc.isOwner()
+        || (metaConfigControl.isVisible() && metaConfigControl.canUpload()));
+    info.canAdd = toBoolean(pc.canAddRefs());
+    info.configVisible = pc.isOwner() || metaConfigControl.isVisible();
+
+    return info;
+  }
+
+  private AccessSectionInfo createAccessSection(AccessSection section) {
+    AccessSectionInfo accessSectionInfo = new AccessSectionInfo();
+    accessSectionInfo.permissions = new HashMap<>();
+    for (Permission p : section.getPermissions()) {
+      PermissionInfo pInfo = new PermissionInfo(p.getLabel(),
+          p.getExclusiveGroup() ? true : null);
+      pInfo.rules = new HashMap<>();
+      for (PermissionRule r : p.getRules()) {
+        PermissionRuleInfo info = new PermissionRuleInfo(
+            ACTION_TYPE.get(r.getAction()), r.getForce());
+        if (r.hasRange()) {
+          info.max = r.getMax();
+          info.min = r.getMin();
+        }
+        pInfo.rules.put(r.getGroup().getUUID().get(), info);
+      }
+      accessSectionInfo.permissions.put(p.getName(), pInfo);
+    }
+    return accessSectionInfo;
+  }
+
+  private ProjectControl open(Project.NameKey projectName)
+      throws ResourceNotFoundException, IOException {
+    try {
+      return projectControlFactory.validateFor(projectName,
+          ProjectControl.OWNER | ProjectControl.VISIBLE, self.get());
+    } catch (NoSuchProjectException e) {
+      throw new ResourceNotFoundException(projectName.get());
+    }
+  }
+
+  private static Boolean toBoolean(boolean value) {
+    return value ? true : null;
+  }
+}
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 0c417d9..1bf763f 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
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.server.project;
 
-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;
@@ -25,7 +23,9 @@
 
 import org.kohsuke.args4j.Option;
 
+import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -64,7 +64,7 @@
   }
 
   private List<ProjectInfo> getDirectChildProjects(Project.NameKey parent) {
-    List<ProjectInfo> childProjects = Lists.newArrayList();
+    List<ProjectInfo> childProjects = new ArrayList<>();
     for (Project.NameKey projectName : projectCache.all()) {
       ProjectState e = projectCache.get(projectName);
       if (e == null) {
@@ -80,7 +80,7 @@
 
   private List<ProjectInfo> getChildProjectsRecursively(Project.NameKey parent,
       CurrentUser user) {
-    Map<Project.NameKey, ProjectNode> projects = Maps.newHashMap();
+    Map<Project.NameKey, ProjectNode> projects = new HashMap<>();
     for (Project.NameKey name : projectCache.all()) {
       ProjectState p = projectCache.get(name);
       if (p == null) {
@@ -106,7 +106,7 @@
   }
 
   private List<ProjectInfo> getChildProjectsRecursively(ProjectNode p) {
-    List<ProjectInfo> allChildren = Lists.newArrayList();
+    List<ProjectInfo> allChildren = new ArrayList<>();
     for (ProjectNode c : p.getChildren()) {
       if (c.isVisible()) {
         allChildren.add(json.format(c.getProject()));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java
index b4bd9a3..2546ac6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java
@@ -16,7 +16,6 @@
 
 import static com.google.gerrit.reviewdb.client.RefNames.REFS_DASHBOARDS;
 
-import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Project;
@@ -37,6 +36,7 @@
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.List;
 
 class ListDashboards implements RestReadView<ProjectResource> {
@@ -61,7 +61,7 @@
       return scan(resource.getControl(), project, true);
     }
 
-    List<List<DashboardInfo>> all = Lists.newArrayList();
+    List<List<DashboardInfo>> all = new ArrayList<>();
     boolean setDefault = true;
     for (ProjectState ps : ctl.getProjectState().tree()) {
       ctl = ps.controlFor(ctl.getUser());
@@ -85,7 +85,7 @@
     Project.NameKey projectName = ctl.getProject().getNameKey();
     try (Repository git = gitManager.openRepository(projectName);
         RevWalk rw = new RevWalk(git)) {
-      List<DashboardInfo> all = Lists.newArrayList();
+      List<DashboardInfo> all = new ArrayList<>();
       for (Ref ref : git.getRefDatabase().getRefs(REFS_DASHBOARDS).values()) {
         if (ctl.controlForRef(ref.getName()).canRead()) {
           all.addAll(scanDashboards(ctl.getProject(), git, rw, ref,
@@ -101,7 +101,7 @@
   private List<DashboardInfo> scanDashboards(Project definingProject,
       Repository git, RevWalk rw, Ref ref, String project, boolean setDefault)
       throws IOException {
-    List<DashboardInfo> list = Lists.newArrayList();
+    List<DashboardInfo> list = new ArrayList<>();
     try (TreeWalk tw = new TreeWalk(rw.getObjectReader())) {
       tw.addTree(rw.parseTree(ref.getObjectId()));
       tw.setRecursive(true);
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 b0ab94b..bf17a37 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,8 +21,6 @@
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.extensions.common.ProjectInfo;
@@ -61,8 +59,11 @@
 import java.io.OutputStream;
 import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -76,7 +77,7 @@
 public class ListProjects implements RestReadView<TopLevelResource> {
   private static final Logger log = LoggerFactory.getLogger(ListProjects.class);
 
-  public static enum FilterType {
+  public enum FilterType {
     CODE {
       @Override
       boolean matches(Repository git) throws IOException {
@@ -179,7 +180,7 @@
     this.groupUuid = groupUuid;
   }
 
-  private final List<String> showBranch = Lists.newArrayList();
+  private final List<String> showBranch = new ArrayList<>();
   private boolean showTree;
   private FilterType type = FilterType.ALL;
   private boolean showDescription;
@@ -256,8 +257,8 @@
 
     int foundIndex = 0;
     int found = 0;
-    TreeMap<String, ProjectInfo> output = Maps.newTreeMap();
-    Map<String, String> hiddenNames = Maps.newHashMap();
+    TreeMap<String, ProjectInfo> output = new TreeMap<>();
+    Map<String, String> hiddenNames = new HashMap<>();
     Set<String> rejected = new HashSet<>();
 
     final TreeMap<Project.NameKey, ProjectNode> treeMap = new TreeMap<>();
@@ -357,7 +358,7 @@
                   Ref ref = refs.get(i);
                   if (ref != null && ref.getObjectId() != null) {
                     if (info.branches == null) {
-                      info.branches = Maps.newLinkedHashMap();
+                      info.branches = new LinkedHashMap<>();
                     }
                     info.branches.put(showBranch.get(i), ref.getObjectId().name());
                   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListTags.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListTags.java
index 927d205..bbc7b77 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListTags.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListTags.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.project;
 
 import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.api.projects.TagInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.IdString;
@@ -43,6 +42,7 @@
 import org.kohsuke.args4j.Option;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
@@ -93,7 +93,7 @@
   @Override
   public List<TagInfo> apply(ProjectResource resource) throws IOException,
       ResourceNotFoundException, BadRequestException {
-    List<TagInfo> tags = Lists.newArrayList();
+    List<TagInfo> tags = new ArrayList<>();
 
     try (Repository repo = getRepository(resource.getNameKey());
         RevWalk rw = new RevWalk(repo)) {
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 3ad6f8f..469312d 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
@@ -42,6 +42,7 @@
     put(PROJECT_KIND).to(PutProject.class);
     get(PROJECT_KIND).to(GetProject.class);
     get(PROJECT_KIND, "description").to(GetDescription.class);
+    get(PROJECT_KIND, "access").to(GetAccess.class);
     put(PROJECT_KIND, "description").to(PutDescription.class);
     delete(PROJECT_KIND, "description").to(PutDescription.class);
 
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 a5cf5d6..9a9c5bb 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
@@ -35,6 +35,7 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -81,7 +82,7 @@
 
       Collection<String> usernames = null;
       boolean perUser = false;
-      Map<AccessSection, Project.NameKey> sectionToProject = Maps.newLinkedHashMap();
+      Map<AccessSection, Project.NameKey> sectionToProject = new LinkedHashMap<>();
       for (SectionMatcher sm : matcherList) {
         // If the matcher has to expand parameters and its prefix matches the
         // reference there is a very good chance the reference is actually user
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 ad94d64..308a258 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
@@ -40,6 +40,7 @@
 
 import java.io.IOException;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.NoSuchElementException;
 import java.util.Set;
@@ -210,7 +211,7 @@
 
   @Override
   public Set<AccountGroup.UUID> guessRelevantGroupUUIDs() {
-    Set<AccountGroup.UUID> groups = Sets.newHashSet();
+    Set<AccountGroup.UUID> groups = new HashSet<>();
     for (Project.NameKey n : all()) {
       ProjectState p = byName.getIfPresent(n.get());
       if (p != null) {
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 b610854..6fab775 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.project;
 
-import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.PageLinks;
@@ -385,7 +384,7 @@
     }
     final IdentifiedUser iUser = user.asIdentifiedUser();
 
-    List<AccountGroup.UUID> okGroupIds = Lists.newArrayList();
+    List<AccountGroup.UUID> okGroupIds = new ArrayList<>();
     for (ContributorAgreement ca : contributorAgreements) {
       List<AccountGroup.UUID> groupIds;
       groupIds = okGroupIds;
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 7094828..29f97fb 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
@@ -21,7 +21,6 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.LabelType;
@@ -61,8 +60,10 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -126,7 +127,7 @@
     this.rulesCache = rulesCache;
     this.commentLinks = commentLinks;
     this.config = config;
-    this.configs = Maps.newHashMap();
+    this.configs = new HashMap<>();
     this.capabilities = isAllProjects
       ? new CapabilityCollection(config.getAccessSection(AccessSection.GLOBAL_CAPABILITIES))
       : null;
@@ -277,7 +278,7 @@
       return getLocalAccessSections();
     }
 
-    List<SectionMatcher> all = Lists.newArrayList();
+    List<SectionMatcher> all = new ArrayList<>();
     for (ProjectState s : tree()) {
       all.addAll(s.getLocalAccessSections());
     }
@@ -423,7 +424,7 @@
   }
 
   public LabelTypes getLabelTypes() {
-    Map<String, LabelType> types = Maps.newLinkedHashMap();
+    Map<String, LabelType> types = new LinkedHashMap<>();
     for (ProjectState s : treeInOrder()) {
       for (LabelType type : s.getConfig().getLabelSections().values()) {
         String lower = type.getName().toLowerCase();
@@ -443,7 +444,7 @@
   }
 
   public List<CommentLinkInfo> getCommentLinks() {
-    Map<String, CommentLinkInfo> cls = Maps.newLinkedHashMap();
+    Map<String, CommentLinkInfo> cls = new LinkedHashMap<>();
     for (CommentLinkInfo cl : commentLinks) {
       cls.put(cl.name.toLowerCase(), cl);
     }
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 8922df5..cbe7fba 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
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.server.project;
 
-import com.google.common.collect.Maps;
-import com.google.common.collect.Sets;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRange;
@@ -45,6 +43,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -128,8 +127,8 @@
   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();
+    Set<ProjectRef> allows = new HashSet<>();
+    Set<ProjectRef> blocks = new HashSet<>();
     for (PermissionRule rule : access) {
       if (rule.isBlock()) {
         blocks.add(relevant.getRuleProps(rule));
@@ -473,8 +472,8 @@
   }
 
   private static class AllowedRange {
-    private int allowMin = 0;
-    private int allowMax = 0;
+    private int allowMin;
+    private int allowMax;
     private int blockMin = Integer.MIN_VALUE;
     private int blockMax = Integer.MAX_VALUE;
 
@@ -506,7 +505,7 @@
 
   private PermissionRange toRange(String permissionName,
       List<PermissionRule> ruleList) {
-    Map<ProjectRef, AllowedRange> ranges = Maps.newHashMap();
+    Map<ProjectRef, AllowedRange> ranges = new HashMap<>();
     for (PermissionRule rule : ruleList) {
       ProjectRef p = relevant.getRuleProps(rule);
       AllowedRange r = ranges.get(p);
@@ -546,8 +545,8 @@
   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();
+    Set<ProjectRef> allows = new HashSet<>();
+    Set<ProjectRef> blocks = new HashSet<>();
     for (PermissionRule rule : access) {
       if (rule.isBlock() && !rule.getForce()) {
         blocks.add(relevant.getRuleProps(rule));
@@ -566,8 +565,8 @@
   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();
+    Set<ProjectRef> allows = new HashSet<>();
+    Set<ProjectRef> blocks = new HashSet<>();
     for (PermissionRule rule : access) {
       if (rule.isBlock()) {
         blocks.add(relevant.getRuleProps(rule));
@@ -588,8 +587,8 @@
   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();
+    Set<ProjectRef> allows = new HashSet<>();
+    Set<ProjectRef> blocks = new HashSet<>();
     for (PermissionRule rule : access) {
       if (rule.isBlock()) {
         blocks.add(relevant.getRuleProps(rule));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RepositoryStatistics.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RepositoryStatistics.java
index d8294c0..de045b7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/RepositoryStatistics.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RepositoryStatistics.java
@@ -23,7 +23,7 @@
 class RepositoryStatistics extends TreeMap<String, Object> {
   private static final long serialVersionUID = 1L;
 
-  public RepositoryStatistics(Properties p) {
+  RepositoryStatistics(Properties p) {
     for (Entry<Object, Object> e : p.entrySet()) {
       put(CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE,
           e.getKey().toString()), e.getValue());
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 571ddb3..aa06024 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
@@ -90,7 +90,7 @@
         final RefUpdate u = repo.updateRef(Constants.HEAD, true);
         u.setRefLogIdent(identifiedUser.get().newRefLogIdent());
         RefUpdate.Result res = u.link(newHead);
-        switch(res) {
+        switch (res) {
           case NO_CHANGE:
           case RENAMED:
           case FORCED:
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 2ea7e82..93236b0 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
@@ -17,7 +17,6 @@
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkState;
 
-import com.google.common.collect.Lists;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.data.SubmitTypeRecord;
@@ -83,7 +82,7 @@
   private static class UserTermExpected extends Exception {
     private static final long serialVersionUID = 1L;
 
-    public UserTermExpected(SubmitRecord.Label label) {
+    UserTermExpected(SubmitRecord.Label label) {
       super(String.format("A label with the status %s must contain a user.",
           label.toString()));
     }
@@ -485,7 +484,7 @@
       }
       List<Term> r;
       if (resultsTerm instanceof ListTerm) {
-        r = Lists.newArrayList();
+        r = new ArrayList<>();
         for (Term t = resultsTerm; t instanceof ListTerm;) {
           ListTerm l = (ListTerm) t;
           r.add(l.car().dereference());
@@ -614,6 +613,9 @@
   private void initPatchSet() throws OrmException {
     if (patchSet == null) {
       patchSet = cd.currentPatchSet();
+      if (patchSet == null) {
+        throw new OrmException("No patch set found");
+      }
     }
   }
 
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
deleted file mode 100644
index 9bbc02f..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AbstractResultSet.java
+++ /dev/null
@@ -1,31 +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.query.change;
-
-import com.google.gwtorm.server.ResultSet;
-
-import java.util.ArrayList;
-import java.util.List;
-
-abstract class AbstractResultSet<T> implements ResultSet<T> {
-  @Override
-  public List<T> toList() {
-    ArrayList<T> r = new ArrayList<>();
-    for (T t : this) {
-      r.add(t);
-    }
-    return r;
-  }
-}
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 9ed0447..9e68595 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
@@ -21,7 +21,6 @@
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
 import com.google.gerrit.server.query.AndPredicate;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gwtorm.server.ListResultSet;
@@ -108,7 +107,7 @@
     if (source == null) {
       throw new OrmException("No ChangeDataSource: " + this);
     }
-    List<ChangeData> r = Lists.newArrayList();
+    List<ChangeData> r = new ArrayList<>();
     ChangeData last = null;
     int nextStart = 0;
     boolean skipped = false;
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 40fadb4..ed7b134 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
@@ -14,18 +14,24 @@
 
 package com.google.gerrit.server.query.change;
 
+import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.server.ApprovalsUtil.sortApprovals;
 
 import com.google.auto.value.AutoValue;
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Optional;
+import com.google.common.base.Predicate;
+import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMultimap;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
 import com.google.common.collect.SetMultimap;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.data.SubmitTypeRecord;
 import com.google.gerrit.reviewdb.client.Account;
@@ -44,6 +50,7 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchLineCommentsUtil;
 import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.change.MergeabilityCache;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MergeUtil;
@@ -120,7 +127,7 @@
       return;
     }
 
-    Map<Change.Id, ChangeData> missing = Maps.newHashMap();
+    Map<Change.Id, ChangeData> missing = new HashMap<>();
     for (ChangeData cd : changes) {
       if (cd.change == null) {
         missing.put(cd.getId(), cd);
@@ -178,7 +185,7 @@
       return;
     }
 
-    Map<PatchSet.Id, ChangeData> missing = Maps.newHashMap();
+    Map<PatchSet.Id, ChangeData> missing = new HashMap<>();
     for (ChangeData cd : changes) {
       if (cd.currentPatchSet == null && cd.patchSets == null) {
         missing.put(cd.change().currentPatchSetId(), cd);
@@ -296,7 +303,7 @@
   public static ChangeData createForTest(Project.NameKey project, Change.Id id,
       int currentPatchSetId) {
     ChangeData cd = new ChangeData(null, null, null, null, null, null, null,
-        null, null, null, null, null, null, null, project, id);
+        null, null, null, null, null, null, null, null, project, id);
     cd.currentPatchSet = new PatchSet(new PatchSet.Id(id, currentPatchSetId));
     return cd;
   }
@@ -315,6 +322,7 @@
   private final PatchListCache patchListCache;
   private final NotesMigration notesMigration;
   private final MergeabilityCache mergeabilityCache;
+  private final StarredChangesUtil starredChangesUtil;
   private final Change.Id legacyId;
   private DataSource<ChangeData> returnedBySource;
   private Project.NameKey project;
@@ -335,9 +343,13 @@
   private ChangedLines changedLines;
   private SubmitTypeRecord submitTypeRecord;
   private Boolean mergeable;
+  private Set<String> hashtags;
   private Set<Account.Id> editsByUser;
   private Set<Account.Id> reviewedBy;
   private Set<Account.Id> draftsByUser;
+  @Deprecated
+  private Set<Account.Id> starredByUser;
+  private ImmutableMultimap<Account.Id, String> stars;
   private PersonIdent author;
   private PersonIdent committer;
 
@@ -356,6 +368,7 @@
       PatchListCache patchListCache,
       NotesMigration notesMigration,
       MergeabilityCache mergeabilityCache,
+      @Nullable StarredChangesUtil starredChangesUtil,
       @Assisted ReviewDb db,
       @Assisted Project.NameKey project,
       @Assisted Change.Id id) {
@@ -373,6 +386,7 @@
     this.patchListCache = patchListCache;
     this.notesMigration = notesMigration;
     this.mergeabilityCache = mergeabilityCache;
+    this.starredChangesUtil = starredChangesUtil;
     this.project = project;
     this.legacyId = id;
   }
@@ -392,6 +406,7 @@
       PatchListCache patchListCache,
       NotesMigration notesMigration,
       MergeabilityCache mergeabilityCache,
+      @Nullable StarredChangesUtil starredChangesUtil,
       @Assisted ReviewDb db,
       @Assisted Change c) {
     this.db = db;
@@ -408,6 +423,7 @@
     this.patchListCache = patchListCache;
     this.notesMigration = notesMigration;
     this.mergeabilityCache = mergeabilityCache;
+    this.starredChangesUtil = starredChangesUtil;
     legacyId = c.getId();
     change = c;
     project = c.getProject();
@@ -428,6 +444,7 @@
       PatchListCache patchListCache,
       NotesMigration notesMigration,
       MergeabilityCache mergeabilityCache,
+      @Nullable StarredChangesUtil starredChangesUtil,
       @Assisted ReviewDb db,
       @Assisted ChangeNotes cn) {
     this.db = db;
@@ -444,6 +461,7 @@
     this.patchListCache = patchListCache;
     this.notesMigration = notesMigration;
     this.mergeabilityCache = mergeabilityCache;
+    this.starredChangesUtil = starredChangesUtil;
     legacyId = cn.getChangeId();
     change = cn.getChange();
     project = cn.getProjectName();
@@ -465,6 +483,7 @@
       PatchListCache patchListCache,
       NotesMigration notesMigration,
       MergeabilityCache mergeabilityCache,
+      @Nullable StarredChangesUtil starredChangesUtil,
       @Assisted ReviewDb db,
       @Assisted ChangeControl c) {
     this.db = db;
@@ -481,6 +500,7 @@
     this.patchListCache = patchListCache;
     this.notesMigration = notesMigration;
     this.mergeabilityCache = mergeabilityCache;
+    this.starredChangesUtil = starredChangesUtil;
     legacyId = c.getId();
     change = c.getChange();
     changeControl = c;
@@ -503,6 +523,7 @@
       PatchListCache patchListCache,
       NotesMigration notesMigration,
       MergeabilityCache mergeabilityCache,
+      @Nullable StarredChangesUtil starredChangesUtil,
       @Assisted ReviewDb db,
       @Assisted Change.Id id) {
     checkState(!notesMigration.readChanges(),
@@ -521,6 +542,7 @@
     this.patchListCache = patchListCache;
     this.notesMigration = notesMigration;
     this.mergeabilityCache = mergeabilityCache;
+    this.starredChangesUtil = starredChangesUtil;
     this.legacyId = id;
     this.project = null;
   }
@@ -817,13 +839,31 @@
     return patchSets;
   }
 
-  public void setPatchSets(List<PatchSet> patchSets) {
+  /**
+   * @return patches for the change visible to the current user.
+   * @throws OrmException an error occurred reading the database.
+   */
+  public Collection<PatchSet> visiblePatchSets() throws OrmException {
+    Predicate<PatchSet> predicate = new Predicate<PatchSet>() {
+      @Override
+      public boolean apply(PatchSet input) {
+        try {
+          return changeControl().isPatchVisible(input, db);
+        } catch (OrmException e) {
+          return false;
+        }
+      }
+    };
+    return FluentIterable.from(patchSets()).filter(predicate).toList();
+  }
+
+public void setPatchSets(Collection<PatchSet> patchSets) {
     this.currentPatchSet = null;
     this.patchSets = patchSets;
   }
 
   /**
-   * @return patch set with the given ID, or null if it does not exist.
+   * @return patch with the given ID, or null if it does not exist.
    * @throws OrmException an error occurred reading the database.
    */
   public PatchSet patchSet(PatchSet.Id psId) throws OrmException {
@@ -1005,6 +1045,42 @@
     this.reviewedBy = reviewedBy;
   }
 
+  public Set<String> hashtags() throws OrmException {
+    if (hashtags == null) {
+      hashtags = notes().getHashtags();
+    }
+    return hashtags;
+  }
+
+  public void setHashtags(Set<String> hashtags) {
+    this.hashtags = hashtags;
+  }
+
+  @Deprecated
+  public Set<Account.Id> starredBy() throws OrmException {
+    if (starredByUser == null) {
+      starredByUser = checkNotNull(starredChangesUtil).byChange(
+          legacyId, StarredChangesUtil.DEFAULT_LABEL);
+    }
+    return starredByUser;
+  }
+
+  @Deprecated
+  public void setStarredBy(Set<Account.Id> starredByUser) {
+    this.starredByUser = starredByUser;
+  }
+
+  public ImmutableMultimap<Account.Id, String> stars() throws OrmException {
+    if (stars == null) {
+      stars = checkNotNull(starredChangesUtil).byChange(legacyId);
+    }
+    return stars;
+  }
+
+  public void setStars(Multimap<Account.Id, String> stars) {
+    this.stars = ImmutableMultimap.copyOf(stars);
+  }
+
   @AutoValue
   abstract static class ReviewedByEvent {
     private static ReviewedByEvent create(ChangeMessage msg) {
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 cb27de4..07c8c72 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
@@ -35,6 +35,7 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchLineCommentsUtil;
+import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.GroupBackend;
@@ -102,12 +103,11 @@
   // 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_AUTHOR = "author";
   public static final String FIELD_BEFORE = "before";
-  public static final String FIELD_BRANCH = "branch";
   public static final String FIELD_CHANGE = "change";
+  public static final String FIELD_CHANGE_ID = "change_id";
   public static final String FIELD_COMMENT = "comment";
   public static final String FIELD_COMMENTBY = "commentby";
   public static final String FIELD_COMMIT = "commit";
@@ -118,14 +118,15 @@
   public static final String FIELD_DESTINATION = "destination";
   public static final String FIELD_DRAFTBY = "draftby";
   public static final String FIELD_EDITBY = "editby";
+  public static final String FIELD_EXACTCOMMIT = "exactcommit";
   public static final String FIELD_FILE = "file";
-  public static final String FIELD_IS = "is";
-  public static final String FIELD_HAS = "has";
+  public static final String FIELD_FILEPART = "filepart";
+  public static final String FIELD_GROUP = "group";
   public static final String FIELD_HASHTAG = "hashtag";
   public static final String FIELD_LABEL = "label";
   public static final String FIELD_LIMIT = "limit";
   public static final String FIELD_MERGE = "merge";
-  public static final String FIELD_MERGEABLE = "mergeable";
+  public static final String FIELD_MERGEABLE = "mergeable2";
   public static final String FIELD_MESSAGE = "message";
   public static final String FIELD_OWNER = "owner";
   public static final String FIELD_OWNERIN = "ownerin";
@@ -133,14 +134,15 @@
   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_QUERY = "query";
   public static final String FIELD_REF = "ref";
   public static final String FIELD_REVIEWEDBY = "reviewedby";
   public static final String FIELD_REVIEWER = "reviewer";
   public static final String FIELD_REVIEWERIN = "reviewerin";
+  public static final String FIELD_STAR = "star";
+  public static final String FIELD_STARBY = "starby";
   public static final String FIELD_STARREDBY = "starredby";
   public static final String FIELD_STATUS = "status";
-  public static final String FIELD_TOPIC = "topic";
+  public static final String FIELD_SUBMISSIONID = "submissionid";
   public static final String FIELD_TR = "tr";
   public static final String FIELD_VISIBLETO = "visibleto";
   public static final String FIELD_WATCHEDBY = "watchedby";
@@ -419,7 +421,11 @@
   @Operator
   public Predicate<ChangeData> has(String value) throws QueryParseException {
     if ("star".equalsIgnoreCase(value)) {
-      return new IsStarredByPredicate(args);
+      return starredby(self());
+    }
+
+    if ("stars".equalsIgnoreCase(value)) {
+      return new HasStarsPredicate(self());
     }
 
     if ("draft".equalsIgnoreCase(value)) {
@@ -435,7 +441,7 @@
   @Operator
   public Predicate<ChangeData> is(String value) throws QueryParseException {
     if ("starred".equalsIgnoreCase(value)) {
-      return new IsStarredByPredicate(args);
+      return starredby(self());
     }
 
     if ("watched".equalsIgnoreCase(value)) {
@@ -646,19 +652,37 @@
   }
 
   @Operator
+  public Predicate<ChangeData> star(String label) throws QueryParseException {
+    return new StarPredicate(self(), label);
+  }
+
+  @Operator
   public Predicate<ChangeData> starredby(String who)
       throws QueryParseException, OrmException {
-    if ("self".equals(who)) {
-      return new IsStarredByPredicate(args);
-    }
-    Set<Account.Id> m = parseAccount(who);
-    List<IsStarredByPredicate> p = Lists.newArrayListWithCapacity(m.size());
-    for (Account.Id id : m) {
-      p.add(new IsStarredByPredicate(args.asUser(id)));
+    return starredby(parseAccount(who));
+  }
+
+  private Predicate<ChangeData> starredby(Set<Account.Id> who)
+      throws QueryParseException {
+    List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(who.size());
+    for (Account.Id id : who) {
+      p.add(starredby(id));
     }
     return Predicate.or(p);
   }
 
+  @SuppressWarnings("deprecation")
+  private Predicate<ChangeData> starredby(Account.Id who)
+      throws QueryParseException {
+    if (args.getSchema().hasField(ChangeField.STAR)) {
+      return new StarPredicate(who, StarredChangesUtil.DEFAULT_LABEL);
+    }
+
+    return args.getSchema().hasField(ChangeField.STARREDBY)
+        ? new IsStarredByPredicate(who)
+        : new IsStarredByLegacyPredicate(args.asUser(who));
+  }
+
   @Operator
   public Predicate<ChangeData> watchedby(String who)
       throws QueryParseException, OrmException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasStarsPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasStarsPredicate.java
new file mode 100644
index 0000000..83990bc
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasStarsPredicate.java
@@ -0,0 +1,44 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.Account;
+import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gerrit.server.index.change.ChangeField;
+import com.google.gwtorm.server.OrmException;
+
+public class HasStarsPredicate extends IndexPredicate<ChangeData> {
+  private final Account.Id accountId;
+
+  HasStarsPredicate(Account.Id accountId) {
+    super(ChangeField.STARBY, accountId.toString());
+    this.accountId = accountId;
+  }
+
+  @Override
+  public boolean match(ChangeData cd) throws OrmException {
+    return cd.stars().containsKey(accountId);
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+
+  @Override
+  public String toString() {
+    return ChangeQueryBuilder.FIELD_STARBY + ":" + accountId;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
index bb72a1b..81d8754 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
@@ -28,6 +28,7 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.Nullable;
+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.Project;
@@ -297,6 +298,11 @@
     return query(and(project(project), or(groupPredicates)));
   }
 
+  @SuppressWarnings("deprecation")
+  public List<ChangeData> byIsStarred(Account.Id id) throws OrmException {
+    return query(new IsStarredByPredicate(id));
+  }
+
   public List<ChangeData> query(Predicate<ChangeData> p) throws OrmException {
     try {
       return qp.queryChanges(p).changes();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByLegacyPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByLegacyPredicate.java
new file mode 100644
index 0000000..718c3f6
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByLegacyPredicate.java
@@ -0,0 +1,71 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.query.OrPredicate;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder.Arguments;
+
+import java.util.List;
+import java.util.Set;
+
+@Deprecated
+class IsStarredByLegacyPredicate extends OrPredicate<ChangeData> {
+  private static String describe(CurrentUser user) {
+    if (user.isIdentifiedUser()) {
+      return user.getAccountId().toString();
+    }
+    return user.toString();
+  }
+
+  private static List<Predicate<ChangeData>> predicates(Set<Change.Id> ids) {
+    List<Predicate<ChangeData>> r = Lists.newArrayListWithCapacity(ids.size());
+    for (Change.Id id : ids) {
+      r.add(new LegacyChangeIdPredicate(id));
+    }
+    return r;
+  }
+
+  private final CurrentUser user;
+
+  IsStarredByLegacyPredicate(Arguments args) throws QueryParseException {
+    super(predicates(args.getIdentifiedUser().getStarredChanges()));
+    this.user = args.getIdentifiedUser();
+  }
+
+  @Override
+  public boolean match(final ChangeData object) {
+    return user.getStarredChanges().contains(object.getId());
+  }
+
+  @Override
+  public int getCost() {
+    return 0;
+  }
+
+  @Override
+  public String toString() {
+    String val = describe(user);
+    if (val.indexOf(' ') < 0) {
+      return ChangeQueryBuilder.FIELD_STARREDBY + ":" + val;
+    } else {
+      return ChangeQueryBuilder.FIELD_STARREDBY + ":\"" + val + "\"";
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByPredicate.java
index 5ff859a..634bc4a6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByPredicate.java
@@ -14,57 +14,32 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.common.collect.Lists;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.query.OrPredicate;
-import com.google.gerrit.server.query.Predicate;
-import com.google.gerrit.server.query.QueryParseException;
-import com.google.gerrit.server.query.change.ChangeQueryBuilder.Arguments;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gerrit.server.index.change.ChangeField;
+import com.google.gwtorm.server.OrmException;
 
-import java.util.List;
-import java.util.Set;
+@Deprecated
+class IsStarredByPredicate extends IndexPredicate<ChangeData> {
+  private final Account.Id accountId;
 
-class IsStarredByPredicate extends OrPredicate<ChangeData> {
-  private static String describe(CurrentUser user) {
-    if (user.isIdentifiedUser()) {
-      return user.getAccountId().toString();
-    }
-    return user.toString();
-  }
-
-  private static List<Predicate<ChangeData>> predicates(Set<Change.Id> ids) {
-    List<Predicate<ChangeData>> r = Lists.newArrayListWithCapacity(ids.size());
-    for (Change.Id id : ids) {
-      r.add(new LegacyChangeIdPredicate(id));
-    }
-    return r;
-  }
-
-  private final CurrentUser user;
-
-  IsStarredByPredicate(Arguments args) throws QueryParseException {
-    super(predicates(args.getIdentifiedUser().getStarredChanges()));
-    this.user = args.getIdentifiedUser();
+  IsStarredByPredicate(Account.Id accountId) {
+    super(ChangeField.STARREDBY, accountId.toString());
+    this.accountId = accountId;
   }
 
   @Override
-  public boolean match(final ChangeData object) {
-    return user.getStarredChanges().contains(object.getId());
+  public boolean match(ChangeData cd) throws OrmException {
+    return cd.starredBy().contains(accountId);
   }
 
   @Override
   public int getCost() {
-    return 0;
+    return 1;
   }
 
   @Override
   public String toString() {
-    String val = describe(user);
-    if (val.indexOf(' ') < 0) {
-      return ChangeQueryBuilder.FIELD_STARREDBY + ":" + val;
-    } else {
-      return ChangeQueryBuilder.FIELD_STARREDBY + ":\"" + val + "\"";
-    }
+    return ChangeQueryBuilder.FIELD_STARREDBY + ":" + accountId;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
index 44e0654..4a8e71b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
@@ -14,18 +14,30 @@
 
 package com.google.gerrit.server.query.change;
 
+import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Lists;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.query.AndPredicate;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryBuilder;
 import com.google.gerrit.server.query.QueryParseException;
+import com.google.gwtorm.server.OrmException;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 class IsWatchedByPredicate extends AndPredicate<ChangeData> {
+  private static final Logger log =
+      LoggerFactory.getLogger(IsWatchedByPredicate.class);
+
+  private static final CurrentUser.PropertyKey<List<AccountProjectWatch>> PROJECT_WATCHES =
+      CurrentUser.PropertyKey.create();
+
   private static String describe(CurrentUser user) {
     if (user.isIdentifiedUser()) {
       return user.getAccountId().toString();
@@ -44,10 +56,9 @@
   private static List<Predicate<ChangeData>> filters(
       ChangeQueryBuilder.Arguments args,
       boolean checkIsVisible) throws QueryParseException {
-    CurrentUser user = args.getUser();
-    List<Predicate<ChangeData>> r = Lists.newArrayList();
+    List<Predicate<ChangeData>> r = new ArrayList<>();
     ChangeQueryBuilder builder = new ChangeQueryBuilder(args);
-    for (AccountProjectWatch w : user.getNotificationFilters()) {
+    for (AccountProjectWatch w : getWatches(args)) {
       Predicate<ChangeData> f = null;
       if (w.getFilter() != null) {
         try {
@@ -90,6 +101,24 @@
     }
   }
 
+  private static List<AccountProjectWatch> getWatches(
+      ChangeQueryBuilder.Arguments args) throws QueryParseException {
+    CurrentUser user = args.getUser();
+    List<AccountProjectWatch> watches = user.get(PROJECT_WATCHES);
+    if (watches == null && user.isIdentifiedUser()) {
+      try {
+        watches = args.db.get().accountProjectWatches()
+            .byAccount(user.asIdentifiedUser().getAccountId()).toList();
+        user.put(PROJECT_WATCHES, watches);
+      } catch (OrmException e) {
+        log.warn("Cannot load accountProjectWatches", e);
+      }
+    }
+    return MoreObjects.firstNonNull(
+        watches,
+        Collections.<AccountProjectWatch> emptyList());
+  }
+
   private static List<Predicate<ChangeData>> none() {
     Predicate<ChangeData> any = any();
     return ImmutableList.of(not(any));
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 83364c3..8bf569a 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
@@ -28,6 +28,7 @@
 import com.google.gerrit.server.util.RangeUtil.Range;
 import com.google.inject.Provider;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
 
@@ -141,10 +142,10 @@
     List<Predicate<ChangeData>> r =
         Lists.newArrayListWithCapacity(2 * MAX_LABEL_VALUE);
     for (int i = 1; i <= MAX_LABEL_VALUE; i++) {
-      r.add(not(equalsLabelPredicate(args, label, i)));
-      r.add(not(equalsLabelPredicate(args, label, -i)));
+      r.add(equalsLabelPredicate(args, label, i));
+      r.add(equalsLabelPredicate(args, label, -i));
     }
-    return and(r);
+    return not(or(r));
   }
 
   private static Predicate<ChangeData> equalsLabelPredicate(Args args,
@@ -152,7 +153,7 @@
     if (args.accounts == null || args.accounts.isEmpty()) {
       return new EqualsLabelPredicate(args, label, expVal, null);
     } else {
-      List<Predicate<ChangeData>> r = Lists.newArrayList();
+      List<Predicate<ChangeData>> r = new ArrayList<>();
       for (Account.Id a : args.accounts) {
         r.add(new EqualsLabelPredicate(args, label, expVal, a));
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
index b07b7f2..00ecdb2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
@@ -35,7 +35,6 @@
 import com.google.gson.Gson;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -69,11 +68,11 @@
   private static final DateTimeFormatter dtf =
       DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss zzz");
 
-  public static enum OutputFormat {
+  public enum OutputFormat {
     TEXT, JSON
   }
 
-  private final Provider<ReviewDb> db;
+  private final ReviewDb db;
   private final GitRepositoryManager repoManager;
   private final ChangeQueryBuilder queryBuilder;
   private final QueryProcessor queryProcessor;
@@ -97,7 +96,7 @@
 
   @Inject
   OutputStreamQuery(
-      Provider<ReviewDb> db,
+      ReviewDb db,
       GitRepositoryManager repoManager,
       ChangeQueryBuilder queryBuilder,
       QueryProcessor queryProcessor,
@@ -239,7 +238,7 @@
     ChangeControl cc = d.changeControl().forUser(user);
 
     LabelTypes labelTypes = cc.getLabelTypes();
-    ChangeAttribute c = eventFactory.asChangeAttribute(db.get(), d.change());
+    ChangeAttribute c = eventFactory.asChangeAttribute(db, d.change());
     eventFactory.extend(c, d.change());
 
     if (!trackingFooters.isEmpty()) {
@@ -248,7 +247,7 @@
     }
 
     if (includeAllReviewers) {
-      eventFactory.addAllReviewers(db.get(), c, d.notes());
+      eventFactory.addAllReviewers(db, c, d.notes());
     }
 
     if (includeSubmitRecords) {
@@ -276,16 +275,16 @@
     }
 
     if (includePatchSets) {
-      eventFactory.addPatchSets(db.get(), rw, c, d.patchSets(),
+      eventFactory.addPatchSets(db, rw, c, d.visiblePatchSets(),
           includeApprovals ? d.approvals().asMap() : null,
           includeFiles, d.change(), labelTypes);
     }
 
     if (includeCurrentPatchSet) {
       PatchSet current = d.currentPatchSet();
-      if (current != null) {
+      if (current != null && cc.isPatchVisible(current, d.db())) {
         c.currentPatchSet =
-            eventFactory.asPatchSetAttribute(db.get(), rw, d.change(), current);
+            eventFactory.asPatchSetAttribute(db, rw, d.change(), current);
         eventFactory.addApprovals(c.currentPatchSet,
             d.currentApprovals(), labelTypes);
 
@@ -303,7 +302,7 @@
     if (includeComments) {
       eventFactory.addComments(c, d.messages());
       if (includePatchSets) {
-        eventFactory.addPatchSets(db.get(), rw, c, d.patchSets(),
+        eventFactory.addPatchSets(db, rw, c, d.visiblePatchSets(),
             includeApprovals ? d.approvals().asMap() : null,
             includeFiles, d.change(), labelTypes);
         for (PatchSetAttribute attribute : c.patchSets) {
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 2cabfc5..0cd6978 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
@@ -14,7 +14,6 @@
 
 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.server.CurrentUser;
@@ -26,6 +25,7 @@
 import com.google.gerrit.server.query.Predicate;
 import com.google.inject.Provider;
 
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
@@ -48,7 +48,7 @@
       return Collections.emptyList();
     }
 
-    List<Predicate<ChangeData>> r = Lists.newArrayList();
+    List<Predicate<ChangeData>> r = new ArrayList<>();
     r.add(new ProjectPredicate(projectState.getProject().getName()));
     ListChildProjects children = listChildProjects.get();
     children.setRecursive(true);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/PredicateArgs.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/PredicateArgs.java
index 23350d2..2fd0177 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/PredicateArgs.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/PredicateArgs.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
 import com.google.gerrit.server.query.QueryParseException;
 
+import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -44,8 +44,8 @@
    * @throws QueryParseException
    */
   PredicateArgs(String args) throws QueryParseException {
-    positional = Lists.newArrayList();
-    keyValue = Maps.newHashMap();
+    positional = new ArrayList<>();
+    keyValue = new HashMap<>();
 
     String[] splitArgs = args.split(",");
 
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 dfc0f75e9..43fb0c5 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
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -31,6 +30,7 @@
 
 import org.kohsuke.args4j.Option;
 
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.EnumSet;
 import java.util.List;
@@ -82,7 +82,7 @@
 
   public void addQuery(String query) {
     if (queries == null) {
-      queries = Lists.newArrayList();
+      queries = new ArrayList<>();
     }
     queries.add(query);
   }
@@ -111,6 +111,7 @@
     return out.size() == 1 ? out.get(0) : out;
   }
 
+  @SuppressWarnings("deprecation")
   private List<List<ChangeInfo>> query()
       throws OrmException, QueryParseException {
     if (imp.isDisabled()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java
index 4b71719..ca7c990 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java
@@ -15,14 +15,12 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.GroupMembership;
 import com.google.gerrit.server.account.ListGroupMembership;
 
-import java.util.Collection;
 import java.util.Collections;
 import java.util.Set;
 
@@ -49,9 +47,4 @@
   public Set<Change.Id> getStarredChanges() {
     return Collections.emptySet();
   }
-
-  @Override
-  public Collection<AccountProjectWatch> getNotificationFilters() {
-    return Collections.emptySet();
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/StarPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/StarPredicate.java
new file mode 100644
index 0000000..2facdb7
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/StarPredicate.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.Account;
+import com.google.gerrit.server.StarredChangesUtil;
+import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gerrit.server.index.change.ChangeField;
+import com.google.gwtorm.server.OrmException;
+
+public class StarPredicate extends IndexPredicate<ChangeData> {
+  private final Account.Id accountId;
+  private final String label;
+
+  StarPredicate(Account.Id accountId, String label) {
+    super(ChangeField.STAR,
+        StarredChangesUtil.StarField.create(accountId, label).toString());
+    this.accountId = accountId;
+    this.label = label;
+  }
+
+  @Override
+  public boolean match(ChangeData cd) throws OrmException {
+    return cd.stars().get(accountId).contains(label);
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+
+  @Override
+  public String toString() {
+    return ChangeQueryBuilder.FIELD_STAR + ":" + label;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java
index 0c3bf67..b362a2e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java
@@ -91,7 +91,7 @@
     }
   }
 
-  public static enum Context {
+  public enum Context {
     SINGLE_USER, MULTI_USER
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/HANA.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/HANA.java
index ed4b9ba..44f1f0c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/HANA.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/HANA.java
@@ -30,7 +30,7 @@
   private Config cfg;
 
   @Inject
-  public HANA(@GerritServerConfig final Config cfg) {
+  HANA(@GerritServerConfig final Config cfg) {
     super("com.sap.db.jdbc.Driver");
     this.cfg = cfg;
   }
@@ -52,4 +52,4 @@
     // HANA uses column tables and should not require additional indices
     return ScriptRunner.NOOP;
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/JDBC.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/JDBC.java
index 2c2051d..7cdf93e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/JDBC.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/JDBC.java
@@ -34,4 +34,4 @@
   public String getUrl() {
     return ConfigUtil.getRequired(cfg, "database", "url");
   }
-}
\ No newline at end of file
+}
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
index 7ef88f0..9a09746 100644
--- 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
@@ -29,7 +29,7 @@
   private Config cfg;
 
   @Inject
-  public MaxDb(@GerritServerConfig final Config cfg) {
+  MaxDb(@GerritServerConfig final Config cfg) {
     super("com.sap.dbtech.jdbc.DriverSapDB");
     this.cfg = cfg;
   }
@@ -49,4 +49,4 @@
   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/MySql.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/MySql.java
index 308cec8..0b345e8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/MySql.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/MySql.java
@@ -28,7 +28,7 @@
   private Config cfg;
 
   @Inject
-  public MySql(@GerritServerConfig final Config cfg) {
+  MySql(@GerritServerConfig final Config cfg) {
     super("com.mysql.jdbc.Driver");
     this.cfg = cfg;
   }
@@ -53,4 +53,4 @@
     // a new MySQL connection is usually very fast.
     return false;
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/PostgreSQL.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/PostgreSQL.java
index c58d0c2..3e3509e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/PostgreSQL.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/PostgreSQL.java
@@ -30,7 +30,7 @@
   private Config cfg;
 
   @Inject
-  public PostgreSQL(@GerritServerConfig final Config cfg) {
+  PostgreSQL(@GerritServerConfig final Config cfg) {
     super("org.postgresql.Driver");
     this.cfg = cfg;
   }
@@ -51,4 +51,4 @@
   public ScriptRunner getIndexScript() throws IOException {
     return getScriptRunner("index_postgres.sql");
   }
-}
\ No newline at end of file
+}
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 2d4b65f..f23dabf 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
@@ -19,7 +19,6 @@
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.GerritPersonIdentProvider;
-import com.google.gerrit.server.api.config.GerritServerIdProvider;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AllProjectsNameProvider;
 import com.google.gerrit.server.config.AllUsersName;
@@ -27,6 +26,7 @@
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.AnonymousCowardNameProvider;
 import com.google.gerrit.server.config.GerritServerId;
+import com.google.gerrit.server.config.GerritServerIdProvider;
 
 import org.eclipse.jgit.lib.PersonIdent;
 
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 8ddc86d..ddffd36 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
@@ -26,13 +26,14 @@
 import java.sql.PreparedStatement;
 import java.sql.SQLException;
 import java.sql.Statement;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
 /** A version of the database schema. */
 public abstract class SchemaVersion {
   /** The current schema version. */
-  public static final Class<Schema_121> C = Schema_121.class;
+  public static final Class<Schema_124> C = Schema_124.class;
 
   public static int getBinaryVersion() {
     return guessVersion(C);
@@ -80,7 +81,7 @@
     migrateData(pending, ui, curr, db);
 
     JdbcSchema s = (JdbcSchema) db;
-    final List<String> pruneList = Lists.newArrayList();
+    final List<String> pruneList = new ArrayList<>();
     s.pruneSchema(new StatementExecutor() {
       @Override
       public void execute(String sql) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_115.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_115.java
index 122b369..26cd3e1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_115.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_115.java
@@ -154,7 +154,7 @@
         RevWalk rw = new RevWalk(git)) {
       BatchRefUpdate bru = git.getRefDatabase().newBatchUpdate();
       for (Map.Entry<Account.Id, DiffPreferencesInfo> e : imports.entrySet()) {
-        try(MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED,
+        try (MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED,
             allUsersName, git, bru)) {
           md.getCommitBuilder().setAuthor(serverUser);
           md.getCommitBuilder().setCommitter(serverUser);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_119.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_119.java
index 4f6620e..ffa80a2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_119.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_119.java
@@ -149,7 +149,7 @@
       BatchRefUpdate bru = git.getRefDatabase().newBatchUpdate();
       for (Map.Entry<Account.Id, GeneralPreferencesInfo> e
           : imports.entrySet()) {
-        try(MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED,
+        try (MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED,
             allUsersName, git, bru)) {
           md.getCommitBuilder().setAuthor(serverUser);
           md.getCommitBuilder().setCommitter(serverUser);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_120.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_120.java
index fd94108..645e2d1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_120.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_120.java
@@ -55,7 +55,7 @@
     try (Repository git = mgr.openRepository(subbranch.getParentKey());
         RevWalk rw = new RevWalk(git)) {
       BatchRefUpdate bru = git.getRefDatabase().newBatchUpdate();
-      try(MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED,
+      try (MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED,
           subbranch.getParentKey(), git, bru)) {
         md.setMessage("Added superproject subscription during upgrade");
         ProjectConfig pc = ProjectConfig.read(md);
@@ -97,7 +97,7 @@
             + "super_project_branch_name, "
             + "submodule_project_name, "
             + "submodule_branch_name "
-            + "FROM submodule_subscriptions");) {
+            + "FROM submodule_subscriptions")) {
       while (rs.next()) {
         Project.NameKey superproject = new Project.NameKey(rs.getString(1));
         Branch.NameKey superbranch = new Branch.NameKey(superproject,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeModifiedException.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_122.java
similarity index 64%
rename from gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeModifiedException.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_122.java
index f3ca8b6..b5b799d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeModifiedException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_122.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2016 The Android Open 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,12 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.project;
+package com.google.gerrit.server.schema;
 
-public class ChangeModifiedException extends InvalidChangeOperationException {
-  private static final long serialVersionUID = 1L;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
 
-  public ChangeModifiedException(String msg) {
-    super(msg);
+public class Schema_122 extends SchemaVersion {
+  @Inject
+  Schema_122(Provider<Schema_121> prior) {
+    super(prior);
   }
+
+  // Adds tag column
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_123.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_123.java
new file mode 100644
index 0000000..d698974
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_123.java
@@ -0,0 +1,92 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.ArrayListMultimap;
+import com.google.common.collect.Multimap;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.StarredChangesUtil;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+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.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.TextProgressMonitor;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+import java.io.IOException;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Map;
+
+public class Schema_123 extends SchemaVersion {
+  private final GitRepositoryManager repoManager;
+  private final AllUsersName allUsersName;
+
+  @Inject
+  Schema_123(Provider<Schema_122> prior,
+      GitRepositoryManager repoManager,
+      AllUsersName allUsersName) {
+    super(prior);
+    this.repoManager = repoManager;
+    this.allUsersName = allUsersName;
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui)
+      throws OrmException, SQLException {
+    Multimap<Account.Id, Change.Id> imports = ArrayListMultimap.create();
+    try (Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+      ResultSet rs = stmt.executeQuery(
+          "SELECT "
+          + "account_id, "
+          + "change_id "
+          + "FROM starred_changes")) {
+      while (rs.next()) {
+        Account.Id accountId = new Account.Id(rs.getInt(1));
+        Change.Id changeId = new Change.Id(rs.getInt(2));
+        imports.put(accountId, changeId);
+      }
+    }
+
+    if (imports.isEmpty()) {
+      return;
+    }
+
+    try (Repository git = repoManager.openRepository(allUsersName);
+        RevWalk rw = new RevWalk(git)) {
+      BatchRefUpdate bru = git.getRefDatabase().newBatchUpdate();
+      ObjectId id = StarredChangesUtil.writeLabels(git,
+          StarredChangesUtil.DEFAULT_LABELS);
+      for (Map.Entry<Account.Id, Change.Id> e : imports.entries()) {
+        bru.addCommand(new ReceiveCommand(ObjectId.zeroId(), id,
+            RefNames.refsStarredChanges(e.getValue(), e.getKey())));
+      }
+      bru.execute(rw, new TextProgressMonitor());
+    } catch (IOException ex) {
+      throw new OrmException(ex);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_124.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_124.java
new file mode 100644
index 0000000..6354b68
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_124.java
@@ -0,0 +1,146 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.Function;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Ordering;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountSshKey;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.account.VersionedAuthorizedKeys;
+import com.google.gerrit.server.account.VersionedAuthorizedKeys.SimpleSshKeyCreator;
+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.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.BatchRefUpdate;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+import java.io.IOException;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+public class Schema_124 extends SchemaVersion {
+  private final GitRepositoryManager repoManager;
+  private final AllUsersName allUsersName;
+  private final PersonIdent serverUser;
+
+  @Inject
+  Schema_124(Provider<Schema_123> prior,
+      GitRepositoryManager repoManager,
+      AllUsersName allUsersName,
+      @GerritPersonIdent PersonIdent serverUser) {
+    super(prior);
+    this.repoManager = repoManager;
+    this.allUsersName = allUsersName;
+    this.serverUser = serverUser;
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui)
+      throws OrmException, SQLException {
+    Multimap<Account.Id, AccountSshKey> imports = ArrayListMultimap.create();
+    try (Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+      ResultSet rs = stmt.executeQuery(
+          "SELECT "
+          + "account_id, "
+          + "seq, "
+          + "ssh_public_key, "
+          + "valid "
+          + "FROM account_ssh_keys")) {
+      while (rs.next()) {
+        Account.Id accountId = new Account.Id(rs.getInt(1));
+        int seq = rs.getInt(2);
+        String sshPublicKey = rs.getString(3);
+        AccountSshKey key = new AccountSshKey(
+            new AccountSshKey.Id(accountId, seq), sshPublicKey);
+        boolean valid = rs.getBoolean(4);
+        if (!valid) {
+          key.setInvalid();
+        }
+        imports.put(accountId, key);
+      }
+    }
+
+    if (imports.isEmpty()) {
+      return;
+    }
+
+    try (Repository git = repoManager.openRepository(allUsersName);
+        RevWalk rw = new RevWalk(git)) {
+      BatchRefUpdate bru = git.getRefDatabase().newBatchUpdate();
+
+      for (Map.Entry<Account.Id, Collection<AccountSshKey>> e : imports.asMap()
+          .entrySet()) {
+        try (MetaDataUpdate md = new MetaDataUpdate(
+                 GitReferenceUpdated.DISABLED, allUsersName, git, bru);
+             VersionedAuthorizedKeys authorizedKeys =
+                 new VersionedAuthorizedKeys(
+                     new SimpleSshKeyCreator(), e.getKey())) {
+          md.getCommitBuilder().setAuthor(serverUser);
+          md.getCommitBuilder().setCommitter(serverUser);
+
+          authorizedKeys.load(md);
+          authorizedKeys.setKeys(fixInvalidSequenceNumbers(e.getValue()));
+          authorizedKeys.commit(md);
+        }
+      }
+
+      bru.execute(rw, NullProgressMonitor.INSTANCE);
+    } catch (ConfigInvalidException | IOException ex) {
+      throw new OrmException(ex);
+    }
+  }
+
+  private Collection<AccountSshKey> fixInvalidSequenceNumbers(
+      Collection<AccountSshKey> keys) {
+    Ordering<AccountSshKey> o =
+        Ordering.natural().onResultOf(new Function<AccountSshKey, Integer>() {
+          @Override
+          public Integer apply(AccountSshKey sshKey) {
+            return sshKey.getKey().get();
+          }
+        });
+    List<AccountSshKey> fixedKeys = new ArrayList<>(keys);
+    AccountSshKey minKey = o.min(keys);
+    while (minKey.getKey().get() <= 0) {
+      AccountSshKey fixedKey = new AccountSshKey(
+          new AccountSshKey.Id(minKey.getKey().getParentKey(),
+              Math.max(o.max(keys).getKey().get() + 1, 1)),
+          minKey.getSshPublicKey());
+      Collections.replaceAll(fixedKeys, minKey, fixedKey);
+      minKey = o.min(fixedKeys);
+    }
+    return fixedKeys;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshKeyCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshKeyCache.java
index 0957594..f3250d9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshKeyCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshKeyCache.java
@@ -18,15 +18,17 @@
 import com.google.gerrit.reviewdb.client.AccountSshKey;
 import com.google.inject.AbstractModule;
 import com.google.inject.Module;
+import com.google.inject.Singleton;
 
-
-public class NoSshKeyCache implements SshKeyCache {
+@Singleton
+public class NoSshKeyCache implements SshKeyCache, SshKeyCreator {
 
   public static Module module() {
     return new AbstractModule() {
       @Override
       protected void configure() {
         bind(SshKeyCache.class).to(NoSshKeyCache.class);
+        bind(SshKeyCreator.class).to(NoSshKeyCache.class);
       }
     };
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshAddressesModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshAddressesModule.java
index 36e7e8c..f768c5e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshAddressesModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshAddressesModule.java
@@ -27,6 +27,7 @@
 
 import java.net.InetSocketAddress;
 import java.net.SocketAddress;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 
@@ -81,8 +82,8 @@
     if (want.length > 0) {
       return Arrays.asList(want);
     }
-    List<InetSocketAddress> pub = Lists.newArrayList();
-    List<InetSocketAddress> local = Lists.newArrayList();
+    List<InetSocketAddress> pub = new ArrayList<>();
+    List<InetSocketAddress> local = new ArrayList<>();
 
     for (SocketAddress addr : listen) {
       if (addr instanceof InetSocketAddress) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshKeyCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshKeyCache.java
index a3c0a5a..fd8e1b9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshKeyCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshKeyCache.java
@@ -14,13 +14,7 @@
 
 package com.google.gerrit.server.ssh;
 
-import com.google.gerrit.common.errors.InvalidSshKeyException;
-import com.google.gerrit.reviewdb.client.AccountSshKey;
-
 /** Permits controlling the contents of the SSH key cache area. */
 public interface SshKeyCache {
   void evict(String username);
-
-  AccountSshKey create(AccountSshKey.Id id, String encoded)
-      throws InvalidSshKeyException;
 }
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshKeyCreator.java
similarity index 61%
copy from gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFactory.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshKeyCreator.java
index f68b629..fd0c69c 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshKeyCreator.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 The Android Open Source Project
+// Copyright (C) 2016 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,9 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.prettify.client;
+package com.google.gerrit.server.ssh;
 
-/** Creates a new PrettyFormatter instance for one formatting run. */
-public interface PrettyFactory {
-  PrettyFormatter get();
+import com.google.gerrit.common.errors.InvalidSshKeyException;
+import com.google.gerrit.reviewdb.client.AccountSshKey;
+
+public interface SshKeyCreator {
+  AccountSshKey create(AccountSshKey.Id id, String encoded)
+      throws InvalidSshKeyException;
 }
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 f59bba9..37383b7 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
@@ -135,7 +135,7 @@
 
   /** A file served out of the tools root directory. */
   public static class Entry {
-    public static enum Type {
+    public enum Type {
       DIR, FILE
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/GuiceRequestScopePropagator.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/GuiceRequestScopePropagator.java
index 0b2efd8..2eb8596 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/GuiceRequestScopePropagator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/GuiceRequestScopePropagator.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.util;
 
-import com.google.common.collect.Maps;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.server.RemotePeer;
 import com.google.gerrit.server.config.CanonicalWebUrl;
@@ -29,6 +28,7 @@
 import java.lang.reflect.ParameterizedType;
 import java.lang.reflect.Type;
 import java.net.SocketAddress;
+import java.util.HashMap;
 import java.util.Map;
 import java.util.concurrent.Callable;
 
@@ -54,7 +54,7 @@
    */
   @Override
   protected <T> Callable<T> wrapImpl(Callable<T> callable) {
-    Map<Key<?>, Object> seedMap = Maps.newHashMap();
+    Map<Key<?>, Object> seedMap = new HashMap<>();
 
     // Request scopes appear to use specific keys in their map, instead of only
     // providers. Add bindings for both the key to the instance directly and the
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/HostPlatform.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/HostPlatform.java
index c585270..86b3b7364 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/HostPlatform.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/HostPlatform.java
@@ -21,11 +21,11 @@
   private static final boolean win32 = computeWin32();
 
   /** @return true if this JVM is running on a Windows platform. */
-  public static final boolean isWin32() {
+  public static boolean isWin32() {
     return win32;
   }
 
-  private static final boolean computeWin32() {
+  private static boolean computeWin32() {
     final String osDotName =
         AccessController.doPrivileged(new PrivilegedAction<String>() {
           @Override
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 1cb180b..ce7f24d 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
@@ -67,7 +67,7 @@
         cmp = -1;
       } else if (!p1_finite && p2_finite) {
         cmp = 1;
-      } else /* if (f1 == f2) */{
+      } else /* if (f1 == f2) */ {
         cmp = 0;
       }
     }
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 ad2ab90..907ef70 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
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.util;
 
-import com.google.common.collect.Sets;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
@@ -27,6 +26,7 @@
 
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.util.HashSet;
 import java.util.Set;
 
 /**
@@ -70,7 +70,7 @@
   }
 
   public Set<SubmoduleSubscription> parseAllSections() {
-    Set<SubmoduleSubscription> parsedSubscriptions = Sets.newHashSet();
+    Set<SubmoduleSubscription> parsedSubscriptions = new HashSet<>();
     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/TreeFormatter.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/TreeFormatter.java
index cd559ee..883f972 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/TreeFormatter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/TreeFormatter.java
@@ -19,7 +19,7 @@
 
 public class TreeFormatter {
 
-  public static interface TreeNode {
+  public interface TreeNode {
     String getDisplayName();
     boolean isVisible();
     SortedSet<? extends TreeNode> getChildren();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/validators/OutgoingEmailValidationListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/validators/OutgoingEmailValidationListener.java
index 398a303..b2899c1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/validators/OutgoingEmailValidationListener.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/validators/OutgoingEmailValidationListener.java
@@ -29,7 +29,7 @@
   /**
    * Arguments supplied to validateOutgoingEmail.
    */
-  public static class Args {
+  class Args {
     // in arguments
     public String messageClass;
 
diff --git a/gerrit-server/src/main/java/gerrit/AbstractCommitUserIdentityPredicate.java b/gerrit-server/src/main/java/gerrit/AbstractCommitUserIdentityPredicate.java
index 32713d1..a91bead 100644
--- a/gerrit-server/src/main/java/gerrit/AbstractCommitUserIdentityPredicate.java
+++ b/gerrit-server/src/main/java/gerrit/AbstractCommitUserIdentityPredicate.java
@@ -75,4 +75,4 @@
     }
     return cont;
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-server/src/main/java/gerrit/PRED_commit_author_3.java b/gerrit-server/src/main/java/gerrit/PRED_commit_author_3.java
index 59c4c18..9ef68f5 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_commit_author_3.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_commit_author_3.java
@@ -34,4 +34,4 @@
     UserIdentity author = psInfo.getAuthor();
     return exec(engine, author);
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-server/src/main/java/gerrit/PRED_commit_committer_3.java b/gerrit-server/src/main/java/gerrit/PRED_commit_committer_3.java
index 77a668b..d73ed9b 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_commit_committer_3.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_commit_committer_3.java
@@ -34,4 +34,4 @@
     UserIdentity committer = psInfo.getCommitter();
     return exec(engine, committer);
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-server/src/main/java/gerrit/PRED_commit_delta_4.java b/gerrit-server/src/main/java/gerrit/PRED_commit_delta_4.java
index 893c5bc..8fcb98c 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_commit_delta_4.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_commit_delta_4.java
@@ -175,4 +175,4 @@
     }
     throw new IllegalArgumentException("ChangeType not recognized");
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-server/src/main/java/gerrit/PRED_commit_message_1.java b/gerrit-server/src/main/java/gerrit/PRED_commit_message_1.java
index 6e1dc91..6fc1c2f 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_commit_message_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_commit_message_1.java
@@ -50,4 +50,4 @@
     }
     return cont;
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-server/src/main/java/gerrit/PRED_commit_stats_3.java b/gerrit-server/src/main/java/gerrit/PRED_commit_stats_3.java
index 4f665ee..1dbdb68 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_commit_stats_3.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_commit_stats_3.java
@@ -48,13 +48,13 @@
     Term a3 = arg3.dereference();
 
     PatchList pl = StoredValues.PATCH_LIST.get(engine);
-    if(!a1.unify(new IntegerTerm(pl.getPatches().size() -1),engine.trail)) { //Account for /COMMIT_MSG.
+    if (!a1.unify(new IntegerTerm(pl.getPatches().size() - 1),engine.trail)) { //Account for /COMMIT_MSG.
       return engine.fail();
     }
-    if(!a2.unify(new IntegerTerm(pl.getInsertions()),engine.trail)) {
+    if (!a2.unify(new IntegerTerm(pl.getInsertions()),engine.trail)) {
       return engine.fail();
     }
-    if(!a3.unify(new IntegerTerm(pl.getDeletions()),engine.trail)) {
+    if (!a3.unify(new IntegerTerm(pl.getDeletions()),engine.trail)) {
       return engine.fail();
     }
     return cont;
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteReviewer.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteReviewer.vm
new file mode 100644
index 0000000..619ba2a
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteReviewer.vm
@@ -0,0 +1,44 @@
+## Copyright (C) 2016 The Android Open Source Project
+##
+## Licensed under the Apache License, Version 2.0 (the "License");
+## you may not use this file except in compliance with the License.
+## You may obtain a copy of the License at
+##
+## http://www.apache.org/licenses/LICENSE-2.0
+##
+## Unless required by applicable law or agreed to in writing, software
+## distributed under the License is distributed on an "AS IS" BASIS,
+## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+## See the License for the specific language governing permissions and
+## limitations under the License.
+##
+##
+## Template Type:
+## -------------
+## This is a velocity mail template, see: http://velocity.apache.org and the
+## gerrit-docs:config-mail.txt for more info on modifying gerrit mail templates.
+##
+## Template File Names and extensions:
+## ----------------------------------
+## Gerrit will use templates ending in ".vm" but will ignore templates ending
+## in ".vm.example".  If a .vm template does not exist, the default internal
+## gerrit template which is the same as the .vm.example will be used.  If you
+## want to override the default template, copy the .vm.example file to a .vm
+## file and edit it appropriately.
+##
+## This Template:
+## --------------
+## The DeleteReviewer.vm template will determine the contents of the email
+## related to removal of a reviewer (and the reviewer's votes) from reviews.
+## It is a ChangeEmail: see ChangeSubject.vm and ChangeFooter.vm.
+##
+$fromName has removed $email.joinStrings($email.reviewerNames, ', ') from this change.
+
+Change subject: $change.subject
+......................................................................
+
+
+#if ($email.coverLetter)
+$email.coverLetter
+
+#end
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mime/mime-types.properties b/gerrit-server/src/main/resources/com/google/gerrit/server/mime/mime-types.properties
index 02978da..d51547c 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mime/mime-types.properties
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mime/mime-types.properties
@@ -119,6 +119,7 @@
 macruby = text/x-ruby
 map = application/json
 markdown = text/x-markdown
+mbox = application/mbox
 md = text/x-markdown
 mirc = text/mirc
 mkd = text/x-markdown
@@ -159,6 +160,9 @@
 properties = text/x-ini
 proto = text/x-protobuf
 protobuf = text/x-protobuf
+ps1 = application/x-powershell
+psd1 = application/x-powershell
+psm1 = application/x-powershell
 py = text/x-python
 pyw = text/x-python
 pyx = text/x-cython
@@ -179,6 +183,7 @@
 rst = text/x-rst
 README.md = text/x-gfm
 s = text/x-gas
+sas = text/x-sas
 sass = text/x-sass
 scala = text/x-scala
 scm = text/x-scheme
@@ -223,6 +228,7 @@
 vhdl = text/x-vhdl
 vm = text/velocity
 vtl = text/velocity
+webidl = text/x-webidl
 wsdl = application/xml
 xhtml = text/html
 xml = application/xml
@@ -233,6 +239,7 @@
 xy = application/xquery
 yaml = text/x-yaml
 yml = text/x-yaml
+ys = text/x-yacas
 zsh = text/x-sh
 z80 = text/x-z80
 1 = text/troff
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg b/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
index 937a623..8b85f9b 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
@@ -19,7 +19,7 @@
 
 unset GREP_OPTIONS
 
-CHANGE_ID_AFTER="Bug|Issue|Test|Feature"
+CHANGE_ID_AFTER="Bug|Issue|Test|Feature|Fixes|Fixed"
 MSG="$1"
 
 # Check for, and add if missing, a unique Change-Id
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/IdentifiedUserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/IdentifiedUserTest.java
index 6349be2..390a4c1 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/IdentifiedUserTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/IdentifiedUserTest.java
@@ -60,7 +60,7 @@
   private static final String[] TEST_CASES = {
     "",
     "FirstName.LastName@Corporation.com",
-    "!#$%&'+-/=.?^`{|}~@[IPv6:0123:4567:89AB:CDEF:0123:4567:89AB:CDEF]"
+    "!#$%&'+-/=.?^`{|}~@[IPv6:0123:4567:89AB:CDEF:0123:4567:89AB:CDEF]",
   };
 
   @Before
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/StringUtilTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/StringUtilTest.java
index b4d6e96..0646eef0 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/StringUtilTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/StringUtilTest.java
@@ -51,7 +51,7 @@
         "string with 'quotes'", "string with 'quotes'",
         "C:\\Program Files\\MyProgram", "C:\\\\Program Files\\\\MyProgram",
         "string\nwith\nnewlines", "string\\nwith\\nnewlines",
-        "string\twith\ttabs", "string\\twith\\ttabs" };
+        "string\twith\ttabs", "string\\twith\\ttabs", };
     for (int i = 0; i < testPairs.length; i += 2) {
       assertEquals(StringUtil.escapeString(testPairs[i]), testPairs[i + 1]);
     }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/account/AuthorizedKeysTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/account/AuthorizedKeysTest.java
new file mode 100644
index 0000000..f5849c1
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/account/AuthorizedKeysTest.java
@@ -0,0 +1,174 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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 static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.base.Optional;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountSshKey;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class AuthorizedKeysTest {
+  private static final String KEY1 =
+      "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCgug5VyMXQGnem2H1KVC4/HcRcD4zzBqS"
+      + "uJBRWVonSSoz3RoAZ7bWXCVVGwchtXwUURD689wFYdiPecOrWOUgeeyRq754YWRhU+W28"
+      + "vf8IZixgjCmiBhaL2gt3wff6pP+NXJpTSA4aeWE5DfNK5tZlxlSxqkKOS8JRSUeNQov5T"
+      + "w== john.doe@example.com";
+  private static final String KEY2 =
+      "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDm5yP7FmEoqzQRDyskX+9+N0q9GrvZeh5"
+      + "RG52EUpE4ms/Ujm3ewV1LoGzc/lYKJAIbdcZQNJ9+06EfWZaIRA3oOwAPe1eCnX+aLr8E"
+      + "6Tw2gDMQOGc5e9HfyXpC2pDvzauoZNYqLALOG3y/1xjo7IH8GYRS2B7zO/Mf9DdCcCKSf"
+      + "w== john.doe@example.com";
+  private static final String KEY3 =
+      "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCaS7RHEcZ/zjl9hkWkqnm29RNr2OQ/TZ5"
+      + "jk2qBVMH3BgzPsTsEs+7ag9tfD8OCj+vOcwm626mQBZoR2e3niHa/9gnHBHFtOrGfzKbp"
+      + "RjTWtiOZbB9HF+rqMVD+Dawo/oicX/dDg7VAgOFSPothe6RMhbgWf84UcK5aQd5eP5y+t"
+      + "Q== john.doe@example.com";
+  private static final String KEY4 =
+      "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDIJzW9BaAeO+upFletwwEBnGS15lJmS5i"
+      + "08/NiFef0jXtNNKcLtnd13bq8jOi5VA2is0bwof1c8YbwcvUkdFa8RL5aXoyZBpfYZsWs"
+      + "/YBLZGiHy5rjooMZQMnH37A50cBPnXr0AQz0WRBxLDBDyOZho+O/DfYAKv4rzPSQ3yw4+"
+      + "w== john.doe@example.com";
+  private static final String KEY5 =
+      "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCgBRKGhiXvY6D9sM+Vbth5Kate57YF7kD"
+      + "rqIyUiYIMJK93/AXc8qR/J/p3OIFQAxvLz1qozAur3j5HaiwvxVU19IiSA0vafdhaDLRi"
+      + "zRuEL5e/QOu9yGq9xkWApCmg6edpWAHG+Bx4AldU78MiZvzoB7gMMdxc9RmZ1gYj/DjxV"
+      + "w== john.doe@example.com";
+
+  @Test
+  public void test() throws Exception {
+    List<Optional<AccountSshKey>> keys = new ArrayList<>();
+    StringBuilder expected = new StringBuilder();
+    assertSerialization(keys, expected);
+    assertParse(expected, keys);
+
+    expected.append(addKey(keys, KEY1));
+    assertSerialization(keys, expected);
+    assertParse(expected, keys);
+
+    expected.append(addKey(keys, KEY2));
+    assertSerialization(keys, expected);
+    assertParse(expected, keys);
+
+    expected.append(addInvalidKey(keys, KEY3));
+    assertSerialization(keys, expected);
+    assertParse(expected, keys);
+
+    expected.append(addKey(keys, KEY4));
+    assertSerialization(keys, expected);
+    assertParse(expected, keys);
+
+    expected.append(addDeletedKey(keys));
+    assertSerialization(keys, expected);
+    assertParse(expected, keys);
+
+    expected.append(addKey(keys, KEY5));
+    assertSerialization(keys, expected);
+    assertParse(expected, keys);
+  }
+
+  @Test
+  public void testParseWindowsLineEndings() throws Exception {
+    List<Optional<AccountSshKey>> keys = new ArrayList<>();
+    StringBuilder authorizedKeys = new StringBuilder();
+    authorizedKeys.append(toWindowsLineEndings(addKey(keys, KEY1)));
+    assertParse(authorizedKeys, keys);
+
+    authorizedKeys.append(toWindowsLineEndings(addKey(keys, KEY2)));
+    assertParse(authorizedKeys, keys);
+
+    authorizedKeys.append(toWindowsLineEndings(addInvalidKey(keys, KEY3)));
+    assertParse(authorizedKeys, keys);
+
+    authorizedKeys.append(toWindowsLineEndings(addKey(keys, KEY4)));
+    assertParse(authorizedKeys, keys);
+
+    authorizedKeys.append(toWindowsLineEndings(addDeletedKey(keys)));
+    assertParse(authorizedKeys, keys);
+
+    authorizedKeys.append(toWindowsLineEndings(addKey(keys, KEY5)));
+    assertParse(authorizedKeys, keys);
+
+  }
+
+  private static String toWindowsLineEndings(String s) {
+    return s.replaceAll("\n", "\r\n");
+  }
+
+  private static void assertSerialization(List<Optional<AccountSshKey>> keys,
+      StringBuilder expected) {
+    assertThat(AuthorizedKeys.serialize(keys)).isEqualTo(expected.toString());
+  }
+
+  private static void assertParse(StringBuilder authorizedKeys,
+      List<Optional<AccountSshKey>> expectedKeys) {
+    Account.Id accountId = new Account.Id(1);
+    List<Optional<AccountSshKey>> parsedKeys =
+        AuthorizedKeys.parse(accountId, authorizedKeys.toString());
+    assertThat(parsedKeys).containsExactlyElementsIn(expectedKeys);
+    int seq = 1;
+    for(Optional<AccountSshKey> sshKey : parsedKeys) {
+      if (sshKey.isPresent()) {
+        assertThat(sshKey.get().getAccount()).isEqualTo(accountId);
+        assertThat(sshKey.get().getKey().get()).isEqualTo(seq);
+      }
+      seq++;
+    }
+  }
+
+  /**
+   * Adds the given public key as new SSH key to the given list.
+   *
+   * @return the expected line for this key in the authorized_keys file
+   */
+  private static String addKey(List<Optional<AccountSshKey>> keys, String pub) {
+    AccountSshKey.Id keyId =
+        new AccountSshKey.Id(new Account.Id(1), keys.size() + 1);
+    AccountSshKey key = new AccountSshKey(keyId, pub);
+    keys.add(Optional.of(key));
+    return key.getSshPublicKey() + "\n";
+  }
+
+  /**
+   * Adds the given public key as invalid SSH key to the given list.
+   *
+   * @return the expected line for this key in the authorized_keys file
+   */
+  private static String addInvalidKey(List<Optional<AccountSshKey>> keys,
+      String pub) {
+    AccountSshKey.Id keyId =
+        new AccountSshKey.Id(new Account.Id(1), keys.size() + 1);
+    AccountSshKey key = new AccountSshKey(keyId, pub);
+    key.setInvalid();
+    keys.add(Optional.of(key));
+    return AuthorizedKeys.INVALID_KEY_COMMENT_PREFIX
+        + key.getSshPublicKey() + "\n";
+  }
+
+  /**
+   * Adds a deleted SSH key to the given list.
+   *
+   * @return the expected line for this key in the authorized_keys file
+   */
+  private static String addDeletedKey(List<Optional<AccountSshKey>> keys) {
+    keys.add(Optional.<AccountSshKey> absent());
+    return AuthorizedKeys.DELETED_KEY_COMMENT + "\n";
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/config/GitwebConfigTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/config/GitwebConfigTest.java
index b03a381..12e563f 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/config/GitwebConfigTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/config/GitwebConfigTest.java
@@ -26,14 +26,14 @@
 
   @Test
   public void testValidPathSeparator() {
-    for(char c : VALID_CHARACTERS.toCharArray()) {
+    for (char c : VALID_CHARACTERS.toCharArray()) {
       assertTrue("valid character rejected: " + c, GitwebConfig.isValidPathSeparator(c));
     }
   }
 
   @Test
   public void testInalidPathSeparator() {
-    for(char c : SOME_INVALID_CHARACTERS.toCharArray()) {
+    for (char c : SOME_INVALID_CHARACTERS.toCharArray()) {
       assertFalse("invalid character accepted: " + c, GitwebConfig.isValidPathSeparator(c));
     }
   }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/config/RepositoryConfigTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/config/RepositoryConfigTest.java
index b64bd1a..bf36738 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/config/RepositoryConfigTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/config/RepositoryConfigTest.java
@@ -16,7 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.common.collect.Lists;
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.reviewdb.client.Project.NameKey;
 
@@ -26,7 +26,6 @@
 
 import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.util.Arrays;
 import java.util.List;
 
 public class RepositoryConfigTest {
@@ -98,48 +97,46 @@
 
   @Test
   public void testOwnerGroupsWhenNotConfigured() {
-    assertThat(repoCfg.getOwnerGroups(new NameKey("someProject"))).isEqualTo(
-        new String[] {});
+    assertThat(repoCfg.getOwnerGroups(new NameKey("someProject"))).isEmpty();
   }
 
   @Test
   public void testOwnerGroupsForStarFilter() {
-    String[] ownerGroups = new String[] {"group1", "group2"};
-    configureOwnerGroups("*", Lists.newArrayList(ownerGroups));
-    assertThat(repoCfg.getOwnerGroups(new NameKey("someProject"))).isEqualTo(
-        ownerGroups);
+    ImmutableList<String> ownerGroups = ImmutableList.of("group1", "group2");
+    configureOwnerGroups("*", ownerGroups);
+    assertThat(repoCfg.getOwnerGroups(new NameKey("someProject")))
+        .containsExactlyElementsIn(ownerGroups);
   }
 
   @Test
   public void testOwnerGroupsForSpecificFilter() {
-    String[] ownerGroups = new String[] {"group1", "group2"};
-    configureOwnerGroups("someProject", Lists.newArrayList(ownerGroups));
+    ImmutableList<String> ownerGroups = ImmutableList.of("group1", "group2");
+    configureOwnerGroups("someProject", ownerGroups);
     assertThat(repoCfg.getOwnerGroups(new NameKey("someOtherProject")))
-        .isEqualTo(new String[] {});
-    assertThat(repoCfg.getOwnerGroups(new NameKey("someProject"))).isEqualTo(
-        ownerGroups);
+        .isEmpty();
+    assertThat(repoCfg.getOwnerGroups(new NameKey("someProject")))
+        .containsExactlyElementsIn(ownerGroups);
   }
 
   @Test
   public void testOwnerGroupsForStartWithFilter() {
-    String[] ownerGroups1 = new String[] {"group1"};
-    String[] ownerGroups2 = new String[] {"group2"};
-    String[] ownerGroups3 = new String[] {"group3"};
+    ImmutableList<String> ownerGroups1 = ImmutableList.of("group1");
+    ImmutableList<String> ownerGroups2 = ImmutableList.of("group2");
+    ImmutableList<String> ownerGroups3 = ImmutableList.of("group3");
 
-    configureOwnerGroups("*", Lists.newArrayList(ownerGroups1));
-    configureOwnerGroups("somePath/*", Lists.newArrayList(ownerGroups2));
-    configureOwnerGroups("somePath/somePath/*",
-        Lists.newArrayList(ownerGroups3));
+    configureOwnerGroups("*", ownerGroups1);
+    configureOwnerGroups("somePath/*", ownerGroups2);
+    configureOwnerGroups("somePath/somePath/*", ownerGroups3);
 
-    assertThat(repoCfg.getOwnerGroups(new NameKey("someProject"))).isEqualTo(
-        ownerGroups1);
+    assertThat(repoCfg.getOwnerGroups(new NameKey("someProject")))
+        .containsExactlyElementsIn(ownerGroups1);
 
     assertThat(repoCfg.getOwnerGroups(new NameKey("somePath/someProject")))
-        .isEqualTo(ownerGroups2);
+        .containsExactlyElementsIn(ownerGroups2);
 
     assertThat(
         repoCfg.getOwnerGroups(new NameKey("somePath/somePath/someProject")))
-        .isEqualTo(ownerGroups3);
+        .containsExactlyElementsIn(ownerGroups3);
   }
 
   private void configureOwnerGroups(String projectFilter,
@@ -196,8 +193,10 @@
 
   @Test
   public void testAllBasePath() {
-    List<Path> allBasePaths = Arrays.asList(Paths.get("/someBasePath1"),
-        Paths.get("/someBasePath2"), Paths.get("/someBasePath2"));
+    ImmutableList<Path> allBasePaths = ImmutableList.of(
+        Paths.get("/someBasePath1"),
+        Paths.get("/someBasePath2"),
+        Paths.get("/someBasePath2"));
 
     configureBasePath("*", allBasePaths.get(0).toString());
     configureBasePath("project/*", allBasePaths.get(1).toString());
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/DestinationListTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/DestinationListTest.java
index 2304ece..fb046fd 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/git/DestinationListTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/DestinationListTest.java
@@ -18,7 +18,6 @@
 import static org.easymock.EasyMock.createNiceMock;
 import static org.easymock.EasyMock.replay;
 
-import com.google.common.collect.Sets;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
 
@@ -27,6 +26,7 @@
 import org.junit.Test;
 
 import java.io.IOException;
+import java.util.HashSet;
 import java.util.Set;
 
 public class DestinationListTest extends TestCase {
@@ -61,7 +61,7 @@
   public static final Branch.NameKey B_BAR = dest(P_SLASH, R_BAR);
   public static final Branch.NameKey B_COMPLEX = dest(P_COMPLEX, R_FOO);
 
-  public static final Set<Branch.NameKey> D_SIMPLE = Sets.newHashSet();
+  public static final Set<Branch.NameKey> D_SIMPLE = new HashSet<>();
   static {
     D_SIMPLE.clear();
     D_SIMPLE.add(B_FOO);
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 4f22a1c..6b6528e 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
@@ -214,7 +214,7 @@
   }
 
   private void save(ProjectConfig pc) throws Exception {
-    try(MetaDataUpdate md =
+    try (MetaDataUpdate md =
         metaDataUpdateFactory.create(pc.getProject().getNameKey(), user)) {
       pc.commit(md);
       projectCache.evict(pc.getProject().getNameKey());
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/LocalDiskRepositoryManagerTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/LocalDiskRepositoryManagerTest.java
index 3f122e2..86fa0db 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/git/LocalDiskRepositoryManagerTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/LocalDiskRepositoryManagerTest.java
@@ -18,7 +18,6 @@
 
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.testutil.TempFileUtil;
 import com.google.gwtorm.client.KeyUtil;
 import com.google.gwtorm.server.StandardKeyEncoder;
@@ -53,16 +52,13 @@
     site.resolve("git").toFile().mkdir();
     cfg = new Config();
     cfg.setString("gerrit", null, "basePath", "git");
-    repoManager =
-        new LocalDiskRepositoryManager(site, cfg,
-            createNiceMock(NotesMigration.class));
+    repoManager = new LocalDiskRepositoryManager(site, cfg);
     repoManager.start();
   }
 
   @Test(expected = IllegalStateException.class)
   public void testThatNullBasePathThrowsAnException() {
-    new LocalDiskRepositoryManager(site, new Config(),
-        createNiceMock(NotesMigration.class));
+    new LocalDiskRepositoryManager(site, new Config());
   }
 
   @Test
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManagerTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManagerTest.java
index 4769b34..b26a228 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManagerTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManagerTest.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.RepositoryConfig;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.testutil.TempFileUtil;
 import com.google.gwtorm.client.KeyUtil;
 import com.google.gwtorm.server.StandardKeyEncoder;
@@ -66,11 +65,8 @@
     configMock = createNiceMock(RepositoryConfig.class);
     expect(configMock.getAllBasePaths()).andReturn(new ArrayList<Path>()).anyTimes();
     replay(configMock);
-    NotesMigration notesMigrationMock = createNiceMock(NotesMigration.class);
-    replay(notesMigrationMock);
     repoManager =
-        new MultiBaseLocalDiskRepositoryManager(site, cfg,
-            notesMigrationMock, configMock);
+        new MultiBaseLocalDiskRepositoryManager(site, cfg, configMock);
   }
 
   @After
@@ -188,7 +184,6 @@
         .andReturn(Arrays.asList(Paths.get("repos"))).anyTimes();
     replay(configMock);
     repoManager =
-        new MultiBaseLocalDiskRepositoryManager(site, cfg,
-            createNiceMock(NotesMigration.class), configMock);
+        new MultiBaseLocalDiskRepositoryManager(site, cfg, configMock);
   }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/BasicSerializationTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/BasicSerializationTest.java
index dbc8f02..f6bdeac 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/BasicSerializationTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/BasicSerializationTest.java
@@ -156,6 +156,6 @@
 
   private static byte[] b(int a, int b, int c, int d, int e, int f, int g, int h) {
     return new byte[] {(byte) a, (byte) b, (byte) c, (byte) d, //
-        (byte) e, (byte) f, (byte) g, (byte) h};
+        (byte) e, (byte) f, (byte) g, (byte) h,};
   }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/ColumnFormatterTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/ColumnFormatterTest.java
index fa4ac0e..049e17d 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/ColumnFormatterTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/ColumnFormatterTest.java
@@ -29,7 +29,7 @@
     private PrintWriter printWriter;
     private StringWriter stringWriter;
 
-    public PrintWriterComparator() {
+    PrintWriterComparator() {
       stringWriter = new StringWriter();
       printWriter = new PrintWriter(stringWriter);
     }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeBundleTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeBundleTest.java
index fd20852..ea1120f 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeBundleTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeBundleTest.java
@@ -21,6 +21,7 @@
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.SECONDS;
 
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
@@ -39,10 +40,13 @@
 import com.google.gwtorm.protobuf.ProtobufCodec;
 import com.google.gwtorm.server.StandardKeyEncoder;
 
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
+import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -117,7 +121,7 @@
         "changeId differs for Changes: {" + id1 + "} != {" + id2 + "}",
         "createdOn differs for Changes:"
             + " {2009-09-30 17:00:00.0} != {2009-09-30 17:00:06.0}",
-        "lastUpdatedOn differs for Changes:"
+        "effective last updated time differs for Changes:"
             + " {2009-09-30 17:00:00.0} != {2009-09-30 17:00:06.0}");
   }
 
@@ -135,7 +139,7 @@
 
     c2.setTopic("topic");
     assertDiffs(b1, b2,
-        "topic differs for Change.Id "+ c1.getId() + ": {null} != {topic}");
+        "topic differs for Change.Id " + c1.getId() + ": {null} != {topic}");
   }
 
   @Test
@@ -155,7 +159,7 @@
     assertDiffs(b1, b2,
         "createdOn differs for Change.Id " + c1.getId() + ":"
             + " {2009-09-30 17:00:01.0} != {2009-09-30 17:00:02.0}",
-        "lastUpdatedOn differs for Change.Id " + c1.getId() + ":"
+        "effective last updated time differs for Change.Id " + c1.getId() + ":"
             + " {2009-09-30 17:00:01.0} != {2009-09-30 17:00:03.0}");
 
     // One NoteDb, slop is allowed.
@@ -174,14 +178,255 @@
         comments(), NOTE_DB);
     ChangeBundle b3 = new ChangeBundle(c3, messages(), patchSets(), approvals(),
         comments(), REVIEW_DB);
-    String msg = "lastUpdatedOn differs for Change.Id " + c1.getId()
-        + " in NoteDb vs. ReviewDb:"
+    String msg = "effective last updated time differs for Change.Id "
+        + c1.getId() + " in NoteDb vs. ReviewDb:"
         + " {2009-09-30 17:00:01.0} != {2009-09-30 17:00:10.0}";
     assertDiffs(b1, b3, msg);
     assertDiffs(b3, b1, msg);
   }
 
   @Test
+  public void diffChangesIgnoresOriginalSubjectInReviewDb()
+      throws Exception {
+    Change c1 = TestChanges.newChange(
+        new Project.NameKey("project"), new Account.Id(100));
+    c1.setCurrentPatchSet(c1.currentPatchSetId(), "Subject", "Original A");
+    Change c2 = clone(c1);
+    c2.setCurrentPatchSet(
+        c2.currentPatchSetId(), c1.getSubject(), "Original B");
+
+    // Both ReviewDb, exact match required.
+    ChangeBundle b1 = new ChangeBundle(c1, messages(), patchSets(), approvals(),
+        comments(), REVIEW_DB);
+    ChangeBundle b2 = new ChangeBundle(c2, messages(), patchSets(), approvals(),
+        comments(), REVIEW_DB);
+    assertDiffs(b1, b2,
+        "originalSubject differs for Change.Id " + c1.getId() + ":"
+            + " {Original A} != {Original B}");
+
+    // Both NoteDb, exact match required.
+    b1 = new ChangeBundle(c1, messages(), patchSets(), approvals(), comments(),
+        NOTE_DB);
+    b2 = new ChangeBundle(c2, messages(), patchSets(), approvals(), comments(),
+        NOTE_DB);
+    assertDiffs(b1, b2,
+        "originalSubject differs for Change.Id " + c1.getId() + ":"
+            + " {Original A} != {Original B}");
+
+    // One ReviewDb, one NoteDb, original subject is ignored.
+    b1 = new ChangeBundle(c1, messages(), patchSets(), approvals(), comments(),
+        REVIEW_DB);
+    b2 = new ChangeBundle(c2, messages(), patchSets(), approvals(), comments(),
+        NOTE_DB);
+    assertNoDiffs(b1, b2);
+    assertNoDiffs(b2, b1);
+  }
+
+  @Test
+  public void diffChangesConsidersEmptyReviewDbTopicEquivalentToNullInNoteDb()
+      throws Exception {
+    Change c1 = TestChanges.newChange(
+        new Project.NameKey("project"), new Account.Id(100));
+    c1.setTopic("");
+    Change c2 = clone(c1);
+    c2.setTopic(null);
+
+    // Both ReviewDb, exact match required.
+    ChangeBundle b1 = new ChangeBundle(c1, messages(), patchSets(), approvals(),
+        comments(), REVIEW_DB);
+    ChangeBundle b2 = new ChangeBundle(c2, messages(), patchSets(), approvals(),
+        comments(), REVIEW_DB);
+    assertDiffs(b1, b2,
+        "topic differs for Change.Id " + c1.getId() + ":"
+            + " {} != {null}");
+
+    // Topic ignored if ReviewDb is empty and NoteDb is null.
+    b1 = new ChangeBundle(c1, messages(), patchSets(), approvals(), comments(),
+        REVIEW_DB);
+    b2 = new ChangeBundle(c2, messages(), patchSets(), approvals(), comments(),
+        NOTE_DB);
+    assertNoDiffs(b1, b2);
+
+    // Exact match still required if NoteDb has empty value (not realistic).
+    b1 = new ChangeBundle(c1, messages(), patchSets(), approvals(), comments(),
+        NOTE_DB);
+    b2 = new ChangeBundle(c2, messages(), patchSets(), approvals(), comments(),
+        REVIEW_DB);
+    assertDiffs(b1, b2,
+        "topic differs for Change.Id " + c1.getId() + ":"
+            + " {} != {null}");
+
+    // Null is not equal to a non-empty string.
+    Change c3 = clone(c1);
+    c3.setTopic("topic");
+    b1 = new ChangeBundle(c3, messages(), patchSets(), approvals(), comments(),
+        REVIEW_DB);
+    b2 = new ChangeBundle(c2, messages(), patchSets(), approvals(), comments(),
+        NOTE_DB);
+    assertDiffs(b1, b2,
+        "topic differs for Change.Id " + c1.getId() + ":"
+            + " {topic} != {null}");
+  }
+
+  @Test
+  public void diffChangesTakesMaxEntityTimestampFromReviewDb()
+      throws Exception {
+    Change c1 = TestChanges.newChange(
+        new Project.NameKey("project"), new Account.Id(100));
+    PatchSetApproval a = new PatchSetApproval(
+        new PatchSetApproval.Key(
+            c1.currentPatchSetId(), accountId, new LabelId("Code-Review")),
+        (short) 1,
+        TimeUtil.nowTs());
+
+    Change c2 = clone(c1);
+    c2.setLastUpdatedOn(a.getGranted());
+
+    // Both ReviewDb, exact match required.
+    ChangeBundle b1 = new ChangeBundle(c1, messages(), patchSets(),
+        approvals(a), comments(), REVIEW_DB);
+    ChangeBundle b2 = new ChangeBundle(c2, messages(), patchSets(),
+        approvals(a), comments(), REVIEW_DB);
+    assertDiffs(b1, b2,
+        "effective last updated time differs for Change.Id " + c1.getId() + ":"
+            + " {2009-09-30 17:00:00.0} != {2009-09-30 17:00:06.0}");
+
+    // NoteDb allows latest timestamp from all entities in bundle.
+    b2 = new ChangeBundle(c2, messages(), patchSets(),
+        approvals(a), comments(), NOTE_DB);
+    assertNoDiffs(b1, b2);
+  }
+
+  @Test
+  public void diffChangesIgnoresChangeTimestampIfAnyOtherEntitiesExist() {
+    Change c1 = TestChanges.newChange(
+        new Project.NameKey("project"), new Account.Id(100));
+    PatchSetApproval a = new PatchSetApproval(
+        new PatchSetApproval.Key(
+            c1.currentPatchSetId(), accountId, new LabelId("Code-Review")),
+        (short) 1,
+        TimeUtil.nowTs());
+    c1.setLastUpdatedOn(a.getGranted());
+
+    Change c2 = clone(c1);
+    c2.setLastUpdatedOn(TimeUtil.nowTs());
+
+    // ReviewDb has later lastUpdatedOn timestamp than NoteDb, allowed since
+    // NoteDb matches the latest timestamp of a non-Change entity.
+    ChangeBundle b1 = new ChangeBundle(c2, messages(), patchSets(),
+        approvals(a), comments(), REVIEW_DB);
+    ChangeBundle b2 = new ChangeBundle(c1, messages(), patchSets(),
+        approvals(a), comments(), NOTE_DB);
+    assertThat(b1.getChange().getLastUpdatedOn())
+        .isGreaterThan(b2.getChange().getLastUpdatedOn());
+    assertNoDiffs(b1, b2);
+
+    // Timestamps must actually match if Change is the only entity.
+    b1 = new ChangeBundle(c2, messages(), patchSets(), approvals(), comments(),
+        REVIEW_DB);
+    b2 = new ChangeBundle(c1, messages(), patchSets(), approvals(), comments(),
+        NOTE_DB);
+    assertDiffs(b1, b2,
+        "effective last updated time differs for Change.Id " + c1.getId()
+            + " in NoteDb vs. ReviewDb:"
+            + " {2009-09-30 17:00:06.0} != {2009-09-30 17:00:12.0}");
+  }
+
+  @Test
+  public void diffChangesAllowsReviewDbSubjectToBePrefixOfNoteDbSubject()
+      throws Exception {
+    Change c1 = TestChanges.newChange(
+        new Project.NameKey("project"), new Account.Id(100));
+    Change c2 = clone(c1);
+    c2.setCurrentPatchSet(c1.currentPatchSetId(),
+        c1.getSubject().substring(0, 10), c1.getOriginalSubject());
+    assertThat(c2.getSubject()).isNotEqualTo(c1.getSubject());
+
+    // Both ReviewDb, exact match required.
+    ChangeBundle b1 = new ChangeBundle(c1, messages(), patchSets(), approvals(),
+        comments(), REVIEW_DB);
+    ChangeBundle b2 = new ChangeBundle(c2, messages(), patchSets(), approvals(),
+        comments(), REVIEW_DB);
+    assertDiffs(b1, b2,
+        "subject differs for Change.Id " + c1.getId() + ":"
+            + " {Change subject} != {Change sub}");
+
+    // ReviewDb has shorter subject, allowed.
+    b1 = new ChangeBundle(c1, messages(), patchSets(), approvals(),
+        comments(), NOTE_DB);
+    b2 = new ChangeBundle(c2, messages(), patchSets(), approvals(),
+        comments(), REVIEW_DB);
+    assertNoDiffs(b1, b2);
+
+    // NoteDb has shorter subject, not allowed.
+    b1 = new ChangeBundle(c1, messages(), patchSets(), approvals(),
+        comments(), REVIEW_DB);
+    b2 = new ChangeBundle(c2, messages(), patchSets(), approvals(),
+        comments(), NOTE_DB);
+    assertDiffs(b1, b2,
+        "subject differs for Change.Id " + c1.getId() + ":"
+            + " {Change subject} != {Change sub}");
+  }
+
+  @Test
+  public void diffChangesTrimsLeadingSpacesFromReviewDbComparingToNoteDb()
+      throws Exception {
+    Change c1 = TestChanges.newChange(
+        new Project.NameKey("project"), new Account.Id(100));
+    Change c2 = clone(c1);
+    c2.setCurrentPatchSet(c1.currentPatchSetId(),
+        "   " + c1.getSubject(), c1.getOriginalSubject());
+
+    // Both ReviewDb, exact match required.
+    ChangeBundle b1 = new ChangeBundle(c1, messages(), patchSets(), approvals(),
+        comments(), REVIEW_DB);
+    ChangeBundle b2 = new ChangeBundle(c2, messages(), patchSets(), approvals(),
+        comments(), REVIEW_DB);
+    assertDiffs(b1, b2,
+        "subject differs for Change.Id " + c1.getId() + ":"
+            + " {Change subject} != {   Change subject}");
+
+    // ReviewDb is missing leading spaces, allowed.
+    b1 = new ChangeBundle(c1, messages(), patchSets(), approvals(),
+        comments(), NOTE_DB);
+    b2 = new ChangeBundle(c2, messages(), patchSets(), approvals(),
+        comments(), REVIEW_DB);
+    assertNoDiffs(b1, b2);
+    assertNoDiffs(b2, b1);
+  }
+
+  @Test
+  public void diffChangesDoesntTrimLeadingNonSpaceWhitespaceFromSubject()
+      throws Exception {
+    Change c1 = TestChanges.newChange(
+        new Project.NameKey("project"), new Account.Id(100));
+    Change c2 = clone(c1);
+    c2.setCurrentPatchSet(c1.currentPatchSetId(),
+        "\t" + c1.getSubject(), c1.getOriginalSubject());
+
+    // Both ReviewDb.
+    ChangeBundle b1 = new ChangeBundle(c1, messages(), patchSets(), approvals(),
+        comments(), REVIEW_DB);
+    ChangeBundle b2 = new ChangeBundle(c2, messages(), patchSets(), approvals(),
+        comments(), REVIEW_DB);
+    assertDiffs(b1, b2,
+        "subject differs for Change.Id " + c1.getId() + ":"
+            + " {Change subject} != {\tChange subject}");
+
+    // One NoteDb.
+    b1 = new ChangeBundle(c1, messages(), patchSets(), approvals(),
+        comments(), NOTE_DB);
+    b2 = new ChangeBundle(c2, messages(), patchSets(), approvals(),
+        comments(), REVIEW_DB);
+    assertDiffs(b1, b2,
+        "subject differs for Change.Id " + c1.getId() + ":"
+            + " {Change subject} != {\tChange subject}");
+    assertDiffs(b2, b1,
+        "subject differs for Change.Id " + c1.getId() + ":"
+            + " {\tChange subject} != {Change subject}");
+  }
+
+  @Test
   public void diffChangeMessageKeySets() throws Exception {
     Change c = TestChanges.newChange(project, accountId);
     int id = c.getId().get();
@@ -191,10 +436,10 @@
     ChangeMessage cm2 = new ChangeMessage(
         new ChangeMessage.Key(c.getId(), "uuid2"),
         accountId, TimeUtil.nowTs(), c.currentPatchSetId());
-    ChangeBundle b1 = new ChangeBundle(c, messages(cm1), patchSets(),
-        approvals(), comments(), REVIEW_DB);
-    ChangeBundle b2 = new ChangeBundle(c, messages(cm2), patchSets(),
-        approvals(), comments(), REVIEW_DB);
+    ChangeBundle b1 = new ChangeBundle(c, messages(cm1), latest(c), approvals(),
+        comments(), REVIEW_DB);
+    ChangeBundle b2 = new ChangeBundle(c, messages(cm2), latest(c), approvals(),
+        comments(), REVIEW_DB);
 
     assertDiffs(b1, b2,
         "ChangeMessage.Key sets differ:"
@@ -209,10 +454,10 @@
         accountId, TimeUtil.nowTs(), c.currentPatchSetId());
     cm1.setMessage("message 1");
     ChangeMessage cm2 = clone(cm1);
-    ChangeBundle b1 = new ChangeBundle(c, messages(cm1), patchSets(),
-        approvals(), comments(), REVIEW_DB);
-    ChangeBundle b2 = new ChangeBundle(c, messages(cm2), patchSets(),
-        approvals(), comments(), REVIEW_DB);
+    ChangeBundle b1 = new ChangeBundle(c, messages(cm1), latest(c), approvals(),
+        comments(), REVIEW_DB);
+    ChangeBundle b2 = new ChangeBundle(c, messages(cm2), latest(c), approvals(),
+        comments(), REVIEW_DB);
 
     assertNoDiffs(b1, b2);
 
@@ -233,20 +478,20 @@
     ChangeMessage cm2 = clone(cm1);
     cm2.getKey().set("uuid2");
 
-    ChangeBundle b1 = new ChangeBundle(c, messages(cm1), patchSets(),
-        approvals(), comments(), REVIEW_DB);
-    ChangeBundle b2 = new ChangeBundle(c, messages(cm2), patchSets(),
-        approvals(), comments(), REVIEW_DB);
+    ChangeBundle b1 = new ChangeBundle(c, messages(cm1), latest(c), approvals(),
+        comments(), REVIEW_DB);
+    ChangeBundle b2 = new ChangeBundle(c, messages(cm2), latest(c), approvals(),
+        comments(), REVIEW_DB);
     // Both are ReviewDb, exact UUID match is required.
     assertDiffs(b1, b2,
         "ChangeMessage.Key sets differ:"
             + " [" + id + ",uuid1] only in A; [" + id + ",uuid2] only in B");
 
     // One NoteDb, UUIDs are ignored.
-    b1 = new ChangeBundle(c, messages(cm1), patchSets(), approvals(),
-        comments(), REVIEW_DB);
-    b2 = new ChangeBundle(c, messages(cm2), patchSets(), approvals(),
-        comments(), NOTE_DB);
+    b1 = new ChangeBundle(c, messages(cm1), latest(c), approvals(), comments(),
+        REVIEW_DB);
+    b2 = new ChangeBundle(c, messages(cm2), latest(c), approvals(), comments(),
+        NOTE_DB);
     assertNoDiffs(b1, b2);
   }
 
@@ -264,31 +509,25 @@
     cm1.setMessage("message 2");
 
     // Both ReviewDb: Uses same keySet diff as other types.
-    ChangeBundle b1 = new ChangeBundle(c, messages(cm1, cm2), patchSets(),
+    ChangeBundle b1 = new ChangeBundle(c, messages(cm1, cm2), latest(c),
         approvals(), comments(), REVIEW_DB);
-    ChangeBundle b2 = new ChangeBundle(c, messages(cm1), patchSets(),
-        approvals(), comments(), REVIEW_DB);
+    ChangeBundle b2 = new ChangeBundle(c, messages(cm1), latest(c), approvals(),
+        comments(), REVIEW_DB);
     assertDiffs(b1, b2,
         "ChangeMessage.Key sets differ: [" + id
         + ",uuid2] only in A; [] only in B");
 
     // One NoteDb: UUIDs in keys can't be used for comparison, just diff counts.
-    b1 = new ChangeBundle(c, messages(cm1, cm2), patchSets(), approvals(),
+    b1 = new ChangeBundle(c, messages(cm1, cm2), latest(c), approvals(),
         comments(), REVIEW_DB);
-    b2 = new ChangeBundle(c, messages(cm1), patchSets(), approvals(),
-        comments(), NOTE_DB);
+    b2 = new ChangeBundle(c, messages(cm1), latest(c), approvals(), comments(),
+        NOTE_DB);
     assertDiffs(b1, b2,
-        "Differing numbers of ChangeMessages for Change.Id " + id + ":\n"
-        + "ChangeMessage{key=" + id + ",uuid1, author=100,"
-        + " writtenOn=2009-09-30 17:00:06.0, patchset=" + id + ",1,"
-        + " message=[message 2]}\n"
-        + "ChangeMessage{key=" + id + ",uuid2, author=100,"
-        + " writtenOn=2009-09-30 17:00:12.0, patchset=" + id + ",1,"
-        + " message=[null]}\n"
-        + "--- vs. ---\n"
-        + "ChangeMessage{key=" + id + ",uuid1, author=100,"
-        + " writtenOn=2009-09-30 17:00:06.0, patchset=" + id + ",1,"
-        + " message=[message 2]}");
+        "ChangeMessages differ for Change.Id " + id + "\n"
+            + "Only in A:\n  " + cm2);
+    assertDiffs(b2, b1,
+        "ChangeMessages differ for Change.Id " + id + "\n"
+            + "Only in B:\n  " + cm2);
   }
 
   @Test
@@ -304,13 +543,21 @@
     ChangeMessage cm3 = clone(cm1);
     cm3.getKey().set("uuid2"); // Differs only in UUID.
 
-    ChangeBundle b1 = new ChangeBundle(c, messages(cm1, cm3), patchSets(),
+    ChangeBundle b1 = new ChangeBundle(c, messages(cm1, cm3), latest(c),
         approvals(), comments(), REVIEW_DB);
-    ChangeBundle b2 = new ChangeBundle(c, messages(cm2, cm3), patchSets(),
+    ChangeBundle b2 = new ChangeBundle(c, messages(cm2, cm3), latest(c),
         approvals(), comments(), NOTE_DB);
+    // Implementation happens to pair up cm1 in b1 with cm3 in b2 because it
+    // depends on iteration order and doesn't care about UUIDs. The important
+    // thing is that there's some diff.
     assertDiffs(b1, b2,
-        "message differs for ChangeMessage on " + id + " at index 1:"
-        + " {message 1} != {message 2}");
+        "ChangeMessages differ for Change.Id " + id + "\n"
+            + "Only in A:\n  " + cm3 + "\n"
+            + "Only in B:\n  " + cm2);
+    assertDiffs(b2, b1,
+        "ChangeMessages differ for Change.Id " + id + "\n"
+            + "Only in A:\n  " + cm2 + "\n"
+            + "Only in B:\n  " + cm3);
   }
 
   @Test
@@ -324,19 +571,19 @@
     cm2.setWrittenOn(TimeUtil.nowTs());
 
     // Both are ReviewDb, exact timestamp match is required.
-    ChangeBundle b1 = new ChangeBundle(c, messages(cm1), patchSets(),
-        approvals(), comments(), REVIEW_DB);
-    ChangeBundle b2 = new ChangeBundle(c, messages(cm2), patchSets(),
-        approvals(), comments(), REVIEW_DB);
+    ChangeBundle b1 = new ChangeBundle(c, messages(cm1), latest(c), approvals(),
+        comments(), REVIEW_DB);
+    ChangeBundle b2 = new ChangeBundle(c, messages(cm2), latest(c), approvals(),
+        comments(), REVIEW_DB);
     assertDiffs(b1, b2,
         "writtenOn differs for ChangeMessage.Key " + c.getId() + ",uuid1:"
             + " {2009-09-30 17:00:02.0} != {2009-09-30 17:00:03.0}");
 
     // One NoteDb, slop is allowed.
-    b1 = new ChangeBundle(c, messages(cm1), patchSets(), approvals(),
-        comments(), NOTE_DB);
-    b2 = new ChangeBundle(c, messages(cm2), patchSets(), approvals(),
-        comments(), REVIEW_DB);
+    b1 = new ChangeBundle(c, messages(cm1), latest(c), approvals(), comments(),
+        NOTE_DB);
+    b2 = new ChangeBundle(c, messages(cm2), latest(c), approvals(), comments(),
+        REVIEW_DB);
     assertNoDiffs(b1, b2);
     assertNoDiffs(b2, b1);
 
@@ -344,15 +591,63 @@
     superWindowResolution();
     ChangeMessage cm3 = clone(cm1);
     cm3.setWrittenOn(TimeUtil.nowTs());
-    b1 = new ChangeBundle(c, messages(cm1), patchSets(), approvals(),
-        comments(), NOTE_DB);
-    ChangeBundle b3 = new ChangeBundle(c, messages(cm3), patchSets(),
-        approvals(), comments(), REVIEW_DB);
-    String msg = "writtenOn differs for ChangeMessage on " + c.getId() +
-        " at index 0 in NoteDb vs. ReviewDb:"
-        + " {2009-09-30 17:00:02.0} != {2009-09-30 17:00:10.0}";
-    assertDiffs(b1, b3, msg);
-    assertDiffs(b3, b1, msg);
+    b1 = new ChangeBundle(c, messages(cm1), latest(c), approvals(), comments(),
+        NOTE_DB);
+    ChangeBundle b3 = new ChangeBundle(c, messages(cm3), latest(c), approvals(),
+        comments(), REVIEW_DB);
+    int id = c.getId().get();
+    assertDiffs(b1, b3,
+        "ChangeMessages differ for Change.Id " + id + "\n"
+            + "Only in A:\n  " + cm1 + "\n"
+            + "Only in B:\n  " + cm3);
+    assertDiffs(b3, b1,
+        "ChangeMessages differ for Change.Id " + id + "\n"
+            + "Only in A:\n  " + cm3 + "\n"
+            + "Only in B:\n  " + cm1);
+  }
+
+  @Test
+  public void diffChangeMessagesAllowsNullPatchSetIdFromReviewDb()
+      throws Exception {
+    Change c = TestChanges.newChange(project, accountId);
+    int id = c.getId().get();
+    ChangeMessage cm1 = new ChangeMessage(
+        new ChangeMessage.Key(c.getId(), "uuid"),
+        accountId, TimeUtil.nowTs(), c.currentPatchSetId());
+    cm1.setMessage("message 1");
+    ChangeMessage cm2 = clone(cm1);
+    cm2.setPatchSetId(null);
+
+    ChangeBundle b1 = new ChangeBundle(c, messages(cm1), latest(c), approvals(),
+        comments(), REVIEW_DB);
+    ChangeBundle b2 = new ChangeBundle(c, messages(cm2), latest(c), approvals(),
+        comments(), REVIEW_DB);
+
+    // Both are ReviewDb, exact patch set ID match is required.
+    assertDiffs(b1, b2,
+        "patchset differs for ChangeMessage.Key " + c.getId() + ",uuid:"
+            + " {" + id + ",1} != {null}");
+
+    // Null patch set ID on ReviewDb is ignored.
+    b1 = new ChangeBundle(c, messages(cm1), latest(c), approvals(), comments(),
+        NOTE_DB);
+    b2 = new ChangeBundle(c, messages(cm2), latest(c), approvals(), comments(),
+        REVIEW_DB);
+    assertNoDiffs(b1, b2);
+
+    // Null patch set ID on NoteDb is not ignored (but is not realistic).
+    b1 = new ChangeBundle(c, messages(cm1), latest(c), approvals(), comments(),
+        REVIEW_DB);
+    b2 = new ChangeBundle(c, messages(cm2), latest(c), approvals(), comments(),
+        NOTE_DB);
+    assertDiffs(b1, b2,
+        "ChangeMessages differ for Change.Id " + id + "\n"
+            + "Only in A:\n  " + cm1 + "\n"
+            + "Only in B:\n  " + cm2);
+    assertDiffs(b2, b1,
+        "ChangeMessages differ for Change.Id " + id + "\n"
+            + "Only in A:\n  " + cm2 + "\n"
+            + "Only in B:\n  " + cm1);
   }
 
   @Test
@@ -444,6 +739,82 @@
   }
 
   @Test
+  public void diffPatchSetsIgnoresTrailingNewlinesInPushCertificate()
+      throws Exception {
+    subWindowResolution();
+    Change c = TestChanges.newChange(project, accountId);
+    PatchSet ps1 = new PatchSet(c.currentPatchSetId());
+    ps1.setRevision(new RevId("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
+    ps1.setUploader(accountId);
+    ps1.setCreatedOn(roundToSecond(TimeUtil.nowTs()));
+    ps1.setPushCertificate("some cert");
+    PatchSet ps2 = clone(ps1);
+    ps2.setPushCertificate(ps2.getPushCertificate() + "\n\n");
+
+    ChangeBundle b1 = new ChangeBundle(c, messages(), patchSets(ps1),
+        approvals(), comments(), NOTE_DB);
+    ChangeBundle b2 = new ChangeBundle(c, messages(), patchSets(ps2),
+        approvals(), comments(), REVIEW_DB);
+    assertNoDiffs(b1, b2);
+    assertNoDiffs(b2, b1);
+
+    b1 = new ChangeBundle(c, messages(), patchSets(ps1), approvals(),
+        comments(), REVIEW_DB);
+    b2 = new ChangeBundle(c, messages(), patchSets(ps2), approvals(),
+        comments(), NOTE_DB);
+    assertNoDiffs(b1, b2);
+    assertNoDiffs(b2, b1);
+  }
+
+  @Test
+  public void diffIgnoresPatchSetsGreaterThanCurrent() throws Exception {
+    Change c = TestChanges.newChange(project, accountId);
+
+    PatchSet ps1 = new PatchSet(new PatchSet.Id(c.getId(), 1));
+    ps1.setRevision(new RevId("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
+    ps1.setUploader(accountId);
+    ps1.setCreatedOn(TimeUtil.nowTs());
+    PatchSet ps2 = new PatchSet(new PatchSet.Id(c.getId(), 2));
+    ps2.setRevision(new RevId("badc0feebadc0feebadc0feebadc0feebadc0fee"));
+    ps2.setUploader(accountId);
+    ps2.setCreatedOn(TimeUtil.nowTs());
+    assertThat(ps2.getId().get()).isGreaterThan(c.currentPatchSetId().get());
+
+    PatchSetApproval a1 = new PatchSetApproval(
+        new PatchSetApproval.Key(
+            ps1.getId(), accountId, new LabelId("Code-Review")),
+        (short) 1,
+        TimeUtil.nowTs());
+    PatchSetApproval a2 = new PatchSetApproval(
+        new PatchSetApproval.Key(
+            ps2.getId(), accountId, new LabelId("Code-Review")),
+        (short) 1,
+        TimeUtil.nowTs());
+
+    // Both ReviewDb.
+    ChangeBundle b1 = new ChangeBundle(c, messages(), patchSets(ps1),
+        approvals(a1), comments(), REVIEW_DB);
+    ChangeBundle b2 = new ChangeBundle(c, messages(), patchSets(ps1, ps2),
+        approvals(a1, a2), comments(), REVIEW_DB);
+    assertNoDiffs(b1, b2);
+
+    // One NoteDb.
+    b1 = new ChangeBundle(c, messages(), patchSets(ps1), approvals(a1),
+        comments(), NOTE_DB);
+    b2 = new ChangeBundle(c, messages(), patchSets(ps1, ps2), approvals(a1, a2),
+        comments(), REVIEW_DB);
+    assertNoDiffs(b1, b2);
+    assertNoDiffs(b2, b1);
+
+    // Both NoteDb.
+    b1 = new ChangeBundle(c, messages(), patchSets(ps1), approvals(a1),
+        comments(), NOTE_DB);
+    b2 = new ChangeBundle(c, messages(), patchSets(ps1, ps2), approvals(a1, a2),
+        comments(), NOTE_DB);
+    assertNoDiffs(b1, b2);
+  }
+
+  @Test
   public void diffPatchSetApprovalKeySets() throws Exception {
     Change c = TestChanges.newChange(project, accountId);
     int id = c.getId().get();
@@ -458,10 +829,10 @@
         (short) 1,
         TimeUtil.nowTs());
 
-    ChangeBundle b1 = new ChangeBundle(c, messages(), patchSets(),
-        approvals(a1), comments(), REVIEW_DB);
-    ChangeBundle b2 = new ChangeBundle(c, messages(), patchSets(),
-        approvals(a2), comments(), REVIEW_DB);
+    ChangeBundle b1 = new ChangeBundle(c, messages(), latest(c), approvals(a1),
+        comments(), REVIEW_DB);
+    ChangeBundle b2 = new ChangeBundle(c, messages(), latest(c), approvals(a2),
+        comments(), REVIEW_DB);
 
     assertDiffs(b1, b2,
         "PatchSetApproval.Key sets differ:"
@@ -478,10 +849,10 @@
         (short) 1,
         TimeUtil.nowTs());
     PatchSetApproval a2 = clone(a1);
-    ChangeBundle b1 = new ChangeBundle(c, messages(), patchSets(),
-        approvals(a1), comments(), REVIEW_DB);
-    ChangeBundle b2 = new ChangeBundle(c, messages(), patchSets(),
-        approvals(a2), comments(), REVIEW_DB);
+    ChangeBundle b1 = new ChangeBundle(c, messages(), latest(c), approvals(a1),
+        comments(), REVIEW_DB);
+    ChangeBundle b2 = new ChangeBundle(c, messages(), latest(c), approvals(a2),
+        comments(), REVIEW_DB);
 
     assertNoDiffs(b1, b2);
 
@@ -505,30 +876,30 @@
     a2.setGranted(TimeUtil.nowTs());
 
     // Both are ReviewDb, exact timestamp match is required.
-    ChangeBundle b1 = new ChangeBundle(c, messages(), patchSets(),
-        approvals(a1), comments(), REVIEW_DB);
-    ChangeBundle b2 = new ChangeBundle(c, messages(), patchSets(),
-        approvals(a2), comments(), REVIEW_DB);
+    ChangeBundle b1 = new ChangeBundle(c, messages(), latest(c), approvals(a1),
+        comments(), REVIEW_DB);
+    ChangeBundle b2 = new ChangeBundle(c, messages(), latest(c), approvals(a2),
+        comments(), REVIEW_DB);
     assertDiffs(b1, b2,
         "granted differs for PatchSetApproval.Key "
             + c.getId() + "%2C1,100,Code-Review:"
             + " {2009-09-30 17:00:07.0} != {2009-09-30 17:00:08.0}");
 
     // One NoteDb, slop is allowed.
-    b1 = new ChangeBundle(c, messages(), patchSets(), approvals(a1),
-        comments(), NOTE_DB);
-    b2 = new ChangeBundle(c, messages(), patchSets(), approvals(a2),
-        comments(), REVIEW_DB);
+    b1 = new ChangeBundle(c, messages(), latest(c), approvals(a1), comments(),
+        NOTE_DB);
+    b2 = new ChangeBundle(c, messages(), latest(c), approvals(a2), comments(),
+        REVIEW_DB);
     assertNoDiffs(b1, b2);
 
     // But not too much slop.
     superWindowResolution();
     PatchSetApproval a3 = clone(a1);
     a3.setGranted(TimeUtil.nowTs());
-    b1 = new ChangeBundle(c, messages(), patchSets(), approvals(a1),
-        comments(), NOTE_DB);
-    ChangeBundle b3 = new ChangeBundle(c, messages(), patchSets(),
-        approvals(a3), comments(), REVIEW_DB);
+    b1 = new ChangeBundle(c, messages(), latest(c), approvals(a1), comments(),
+        NOTE_DB);
+    ChangeBundle b3 = new ChangeBundle(c, messages(), latest(c), approvals(a3),
+        comments(), REVIEW_DB);
     String msg = "granted differs for PatchSetApproval.Key "
         + c.getId() + "%2C1,100,Code-Review in NoteDb vs. ReviewDb:"
         + " {2009-09-30 17:00:07.0} != {2009-09-30 17:00:15.0}";
@@ -537,6 +908,39 @@
   }
 
   @Test
+  public void diffPatchSetApprovalsAllowsTruncatedTimestampInNoteDb()
+      throws Exception {
+    Change c = TestChanges.newChange(project, accountId);
+    PatchSetApproval a1 = new PatchSetApproval(
+        new PatchSetApproval.Key(
+            c.currentPatchSetId(), accountId, new LabelId("Code-Review")),
+        (short) 1,
+        c.getCreatedOn());
+    PatchSetApproval a2 = clone(a1);
+    a2.setGranted(new Timestamp(new DateTime(
+            1900, 1, 1, 0, 0, 0, DateTimeZone.forTimeZone(TimeZone.getDefault()))
+        .getMillis()));
+
+    // Both are ReviewDb, exact match is required.
+    ChangeBundle b1 = new ChangeBundle(c, messages(), latest(c), approvals(a1),
+        comments(), REVIEW_DB);
+    ChangeBundle b2 = new ChangeBundle(c, messages(), latest(c), approvals(a2),
+        comments(), REVIEW_DB);
+    assertDiffs(b1, b2,
+        "granted differs for PatchSetApproval.Key "
+            + c.getId() + "%2C1,100,Code-Review:"
+            + " {2009-09-30 17:00:00.0} != {1900-01-01 00:00:00.0}");
+
+    // Truncating NoteDb timestamp is allowed.
+    b1 = new ChangeBundle(c, messages(), latest(c), approvals(a1), comments(),
+        NOTE_DB);
+    b2 = new ChangeBundle(c, messages(), latest(c), approvals(a2), comments(),
+        REVIEW_DB);
+    assertNoDiffs(b1, b2);
+    assertNoDiffs(b2, b1);
+  }
+
+  @Test
   public void diffPatchLineCommentKeySets() throws Exception {
     Change c = TestChanges.newChange(project, accountId);
     int id = c.getId().get();
@@ -549,10 +953,10 @@
             new Patch.Key(c.currentPatchSetId(), "filename2"), "uuid2"),
         5, accountId, null, TimeUtil.nowTs());
 
-    ChangeBundle b1 = new ChangeBundle(c, messages(), patchSets(),
-        approvals(), comments(c1), REVIEW_DB);
-    ChangeBundle b2 = new ChangeBundle(c, messages(), patchSets(),
-        approvals(), comments(c2), REVIEW_DB);
+    ChangeBundle b1 = new ChangeBundle(c, messages(), latest(c), approvals(),
+        comments(c1), REVIEW_DB);
+    ChangeBundle b2 = new ChangeBundle(c, messages(), latest(c), approvals(),
+        comments(c2), REVIEW_DB);
 
     assertDiffs(b1, b2,
         "PatchLineComment.Key sets differ:"
@@ -568,10 +972,10 @@
             new Patch.Key(c.currentPatchSetId(), "filename"), "uuid"),
         5, accountId, null, TimeUtil.nowTs());
     PatchLineComment c2 = clone(c1);
-    ChangeBundle b1 = new ChangeBundle(c, messages(), patchSets(),
-        approvals(), comments(c1), REVIEW_DB);
-    ChangeBundle b2 = new ChangeBundle(c, messages(), patchSets(),
-        approvals(), comments(c2), REVIEW_DB);
+    ChangeBundle b1 = new ChangeBundle(c, messages(), latest(c), approvals(),
+        comments(c1), REVIEW_DB);
+    ChangeBundle b2 = new ChangeBundle(c, messages(), latest(c), approvals(),
+        comments(c2), REVIEW_DB);
 
     assertNoDiffs(b1, b2);
 
@@ -594,36 +998,56 @@
     c2.setWrittenOn(TimeUtil.nowTs());
 
     // Both are ReviewDb, exact timestamp match is required.
-    ChangeBundle b1 = new ChangeBundle(c, messages(), patchSets(),
-        approvals(), comments(c1), REVIEW_DB);
-    ChangeBundle b2 = new ChangeBundle(c, messages(), patchSets(),
-        approvals(), comments(c2), REVIEW_DB);
+    ChangeBundle b1 = new ChangeBundle(c, messages(), latest(c), approvals(),
+        comments(c1), REVIEW_DB);
+    ChangeBundle b2 = new ChangeBundle(c, messages(), latest(c), approvals(),
+        comments(c2), REVIEW_DB);
     assertDiffs(b1, b2,
         "writtenOn differs for PatchLineComment.Key "
             + c.getId() + ",1,filename,uuid:"
             + " {2009-09-30 17:00:02.0} != {2009-09-30 17:00:03.0}");
 
     // One NoteDb, slop is allowed.
-    b1 = new ChangeBundle(c, messages(), patchSets(), approvals(),
-        comments(c1), NOTE_DB);
-    b2 = new ChangeBundle(c, messages(), patchSets(), approvals(),
-        comments(c2), REVIEW_DB);
+    b1 = new ChangeBundle(c, messages(), latest(c), approvals(), comments(c1),
+        NOTE_DB);
+    b2 = new ChangeBundle(c, messages(), latest(c), approvals(), comments(c2),
+        REVIEW_DB);
     assertNoDiffs(b1, b2);
 
     // But not too much slop.
     superWindowResolution();
     PatchLineComment c3 = clone(c1);
     c3.setWrittenOn(TimeUtil.nowTs());
-    b1 = new ChangeBundle(c, messages(), patchSets(), approvals(), comments(c1),
+    b1 = new ChangeBundle(c, messages(), latest(c), approvals(), comments(c1),
         NOTE_DB);
-    ChangeBundle b3 = new ChangeBundle(c, messages(), patchSets(), approvals(),
+    ChangeBundle b3 = new ChangeBundle(c, messages(), latest(c), approvals(),
         comments(c3), REVIEW_DB);
     String msg = "writtenOn differs for PatchLineComment.Key " + c.getId()
         + ",1,filename,uuid in NoteDb vs. ReviewDb:"
         + " {2009-09-30 17:00:02.0} != {2009-09-30 17:00:10.0}";
     assertDiffs(b1, b3, msg);
     assertDiffs(b3, b1, msg);
-}
+  }
+
+  @Test
+  public void diffPatchLineCommentsIgnoresCommentsOnInvalidPatchSet()
+      throws Exception {
+    Change c = TestChanges.newChange(project, accountId);
+    PatchLineComment c1 = new PatchLineComment(
+        new PatchLineComment.Key(
+            new Patch.Key(c.currentPatchSetId(), "filename1"), "uuid1"),
+        5, accountId, null, TimeUtil.nowTs());
+    PatchLineComment c2 = new PatchLineComment(
+        new PatchLineComment.Key(
+            new Patch.Key(new PatchSet.Id(c.getId(), 0), "filename2"), "uuid2"),
+        5, accountId, null, TimeUtil.nowTs());
+
+    ChangeBundle b1 = new ChangeBundle(c, messages(), latest(c), approvals(),
+        comments(c1, c2), REVIEW_DB);
+    ChangeBundle b2 = new ChangeBundle(c, messages(), latest(c), approvals(),
+        comments(c1), REVIEW_DB);
+    assertNoDiffs(b1, b2);
+  }
 
   private static void assertNoDiffs(ChangeBundle a, ChangeBundle b) {
     assertThat(a.differencesFrom(b)).isEmpty();
@@ -653,6 +1077,10 @@
     return Arrays.asList(ents);
   }
 
+  private static List<PatchSet> latest(Change c) {
+    return ImmutableList.of(new PatchSet(c.currentPatchSetId()));
+  }
+
   private static List<PatchSetApproval> approvals(PatchSetApproval... ents) {
     return Arrays.asList(ents);
   }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesParserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
index 2968a62..16cc3b9 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
@@ -17,7 +17,7 @@
 import static org.junit.Assert.fail;
 
 import com.google.gerrit.common.TimeUtil;
-import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
@@ -27,19 +27,18 @@
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
 public class ChangeNotesParserTest extends AbstractChangeNotesTest {
   private TestRepository<InMemoryRepository> testRepo;
-  private RevWalk walk;
+  private ChangeNotesRevWalk walk;
 
   @Before
   public void setUpTestRepo() throws Exception {
     testRepo = new TestRepository<>(repo);
-    walk = new RevWalk(repo);
+    walk = ChangeNotesCommit.newRevWalk(repo);
   }
 
   @After
@@ -53,18 +52,23 @@
         + "\n"
         + "Branch: refs/heads/master\n"
         + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Subject: This is a test change\n");
     assertParseFails(writeCommit("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n",
+        + "Patch-set: 1\n",
         new PersonIdent("Change Owner", "owner@example.com",
           serverIdent.getWhen(), serverIdent.getTimeZone())));
     assertParseFails(writeCommit("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n",
+        + "Patch-set: 1\n",
         new PersonIdent("Change Owner", "x@gerrit",
           serverIdent.getWhen(), serverIdent.getTimeZone())));
+    assertParseFails(writeCommit("Update change\n"
+        + "\n"
+        + "Patch-set: 1\n",
+        new PersonIdent("Change\n\u1234<Owner>", "\n\nx<@>\u0002gerrit",
+          serverIdent.getWhen(), serverIdent.getTimeZone())));
   }
 
   @Test
@@ -73,23 +77,23 @@
         + "\n"
         + "Branch: refs/heads/master\n"
         + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Status: NEW\n"
         + "Subject: This is a test change\n");
     assertParseSucceeds("Update change\n"
         + "\n"
         + "Branch: refs/heads/master\n"
         + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Status: new\n"
         + "Subject: This is a test change\n");
     assertParseFails("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Status: OOPS\n");
     assertParseFails("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Status: NEW\n"
         + "Status: NEW\n");
   }
@@ -100,23 +104,23 @@
         + "\n"
         + "Branch: refs/heads/master\n"
         + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Subject: This is a test change\n");
     assertParseFails("Update change\n"
         + "\n");
     assertParseFails("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
-        + "Patch-Set: 1\n");
+        + "Patch-set: 1\n"
+        + "Patch-set: 1\n");
     assertParseSucceeds("Update change\n"
         + "\n"
         + "Branch: refs/heads/master\n"
         + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Subject: This is a test change\n");
     assertParseFails("Update change\n"
         + "\n"
-        + "Patch-Set: x\n");
+        + "Patch-set: x\n");
   }
 
   @Test
@@ -125,7 +129,7 @@
         + "\n"
         + "Branch: refs/heads/master\n"
         + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Label: Label1=+1\n"
         + "Label: Label2=1\n"
         + "Label: Label3=0\n"
@@ -135,33 +139,33 @@
         + "\n"
         + "Branch: refs/heads/master\n"
         + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Label: -Label1\n"
         + "Label: -Label1 Other Account <2@gerrit>\n"
         + "Subject: This is a test change\n");
     assertParseFails("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Label: Label1=X\n");
     assertParseFails("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Label: Label1 = 1\n");
     assertParseFails("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Label: X+Y\n");
     assertParseFails("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Label: Label1 Other Account <2@gerrit>\n");
     assertParseFails("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Label: -Label!1\n");
     assertParseFails("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Label: -Label!1 Other Account <2@gerrit>\n");
   }
 
@@ -171,7 +175,7 @@
         + "\n"
         + "Branch: refs/heads/master\n"
         + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Subject: This is a test change\n"
         + "Submitted-with: NOT_READY\n"
         + "Submitted-with: OK: Verified: Change Owner <1@gerrit>\n"
@@ -181,19 +185,19 @@
         + "Submitted-with: NEED: Alternative-Code-Review\n");
     assertParseFails("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Submitted-with: OOPS\n");
     assertParseFails("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Submitted-with: NEED: X+Y\n");
     assertParseFails("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Submitted-with: OK: X+Y: Change Owner <1@gerrit>\n");
     assertParseFails("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Submitted-with: OK: Code-Review: 1@gerrit\n");
   }
 
@@ -203,12 +207,12 @@
         + "\n"
         + "Branch: refs/heads/master\n"
         + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Subject: This is a test change\n"
         + "Submission-id: 1-1453387607626-96fabc25");
     assertParseFails("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Submission-id: 1-1453387607626-96fabc25\n"
         + "Submission-id: 1-1453387901516-5d1e2450");
   }
@@ -219,13 +223,13 @@
         + "\n"
         + "Branch: refs/heads/master\n"
         + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Reviewer: Change Owner <1@gerrit>\n"
         + "CC: Other Account <2@gerrit>\n"
         + "Subject: This is a test change\n");
     assertParseFails("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Reviewer: 1@gerrit\n");
   }
 
@@ -235,19 +239,19 @@
         + "\n"
         + "Branch: refs/heads/master\n"
         + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Topic: Some Topic\n"
         + "Subject: This is a test change\n");
     assertParseSucceeds("Update change\n"
         + "\n"
         + "Branch: refs/heads/master\n"
         + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Topic:\n"
         + "Subject: This is a test change\n");
     assertParseFails("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Topic: Some Topic\n"
         + "Topic: Other Topic");
   }
@@ -258,17 +262,17 @@
         + "\n"
         + "Branch: refs/heads/master\n"
         + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Subject: This is a test change\n");
     assertParseSucceeds("Update change\n"
         + "\n"
         + "Branch: master\n"
         + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Subject: This is a test change\n");
     assertParseFails("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Branch: refs/heads/master\n"
         + "Branch: refs/heads/stable");
   }
@@ -279,11 +283,11 @@
         + "\n"
         + "Branch: refs/heads/master\n"
         + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Subject: This is a test change\n");
     assertParseFails("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
         + "Change-id: I159532ef4844d7c18f7f3fd37a0b275590d41b1b");
   }
@@ -292,13 +296,13 @@
   public void parseSubject() throws Exception {
     assertParseSucceeds("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Branch: refs/heads/master\n"
         + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
         + "Subject: Some subject of a change\n");
     assertParseFails("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Subject: Some subject of a change\n"
         + "Subject: Some other subject\n");
   }
@@ -414,6 +418,42 @@
     assertParseFails(writeCommit(msg, serverIdent));
   }
 
+  @Test
+  public void parseTag() throws Exception {
+    assertParseSucceeds("Update change\n"
+        + "\n"
+        + "Patch-set: 1\n"
+        + "Branch: refs/heads/master\n"
+        + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
+        + "Subject: Change subject\n"
+        + "Tag:\n");
+    assertParseSucceeds("Update change\n"
+        + "\n"
+        + "Patch-set: 1\n"
+        + "Branch: refs/heads/master\n"
+        + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
+        + "Subject: Change subject\n"
+        + "Tag: jenkins\n");
+    assertParseFails("Update change\n"
+        + "\n"
+        + "Patch-set: 1\n"
+        + "Branch: refs/heads/master\n"
+        + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
+        + "Subject: Change subject\n"
+        + "Tag: ci\n"
+        + "Tag: jenkins\n");
+  }
+
+  @Test
+  public void caseInsensitiveFooters() throws Exception {
+    assertParseSucceeds("Update change\n"
+        + "\n"
+        + "BRaNch: refs/heads/master\n"
+        + "Change-ID: I577fb248e474018276351785930358ec0450e9f7\n"
+        + "patcH-set: 1\n"
+        + "subject: This is a test change\n");
+  }
+
   private RevCommit writeCommit(String body) throws Exception {
     return writeCommit(body, noteUtil.newIdent(
         changeOwner.getAccount(), TimeUtil.nowTs(), serverIdent,
@@ -441,9 +481,7 @@
   }
 
   private void assertParseSucceeds(RevCommit commit) throws Exception {
-    try (ChangeNotesParser parser = newParser(commit)) {
-      parser.parseAll();
-    }
+    newParser(commit).parseAll();
   }
 
   private void assertParseFails(String body) throws Exception {
@@ -451,8 +489,8 @@
   }
 
   private void assertParseFails(RevCommit commit) throws Exception {
-    try (ChangeNotesParser parser = newParser(commit)) {
-      parser.parseAll();
+    try {
+      newParser(commit).parseAll();
       fail("Expected parse to fail:\n" + commit.getFullMessage());
     } catch (ConfigInvalidException e) {
       // Expected
@@ -460,8 +498,7 @@
   }
 
   private ChangeNotesParser newParser(ObjectId tip) throws Exception {
-    Change c = newChange();
-    return new ChangeNotesParser(c.getProject(), c.getId(), tip, walk,
-        repoManager, noteUtil, args.metrics);
+    return new ChangeNotesParser(
+        newChange().getId(), tip, walk, noteUtil, args.metrics);
   }
 }
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 d2dd48b..3f77498 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
@@ -15,13 +15,14 @@
 package com.google.gerrit.server.notedb;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
+import static com.google.gerrit.reviewdb.client.RefNames.refsDraftComments;
 import static com.google.gerrit.server.notedb.ReviewerStateInternal.CC;
 import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
 import static com.google.gerrit.testutil.TestChanges.incrementPatchSet;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 
-import com.google.common.base.CharMatcher;
 import com.google.common.base.Function;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableListMultimap;
@@ -43,8 +44,10 @@
 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.RefNames;
 import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
+import com.google.gerrit.testutil.TestChanges;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
@@ -69,6 +72,117 @@
   private DraftCommentNotes.Factory draftNotesFactory;
 
   @Test
+  public void tagChangeMessage() throws Exception {
+    String tag = "jenkins";
+    Change c = newChange();
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    update.setChangeMessage("verification from jenkins");
+    update.setTag(tag);
+    update.commit();
+
+    ChangeNotes notes = newNotes(c);
+
+    assertThat(notes.getChangeMessages()).hasSize(1);
+    assertThat(notes.getChangeMessages().get(0).getTag()).isEqualTo(tag);
+  }
+
+  @Test
+  public void tagInlineCommenrts() throws Exception {
+    String tag = "jenkins";
+    Change c = newChange();
+    RevCommit commit = tr.commit().message("PS2").create();
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    update.putComment(newPublishedComment(c.currentPatchSetId(), "a.txt",
+        "uuid1", new CommentRange(1, 2, 3, 4), 1, changeOwner, null,
+        TimeUtil.nowTs(), "Comment", (short) 1, commit.name()));
+    update.setTag(tag);
+    update.commit();
+
+    ChangeNotes notes = newNotes(c);
+
+    ImmutableListMultimap<RevId, PatchLineComment> comments = notes.getComments();
+    assertThat(comments).hasSize(1);
+    assertThat(
+        comments.entries().asList().get(0).getValue().getTag())
+            .isEqualTo(tag);
+  }
+
+  @Test
+  public void tagApprovals() throws Exception {
+    String tag1 = "jenkins";
+    String tag2 = "ip";
+    Change c = newChange();
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    update.putApproval("Verified", (short) -1);
+    update.setTag(tag1);
+    update.commit();
+
+    update = newUpdate(c, changeOwner);
+    update.putApproval("Verified", (short) 1);
+    update.setTag(tag2);
+    update.commit();
+
+    ChangeNotes notes = newNotes(c);
+
+    ImmutableListMultimap<PatchSet.Id, PatchSetApproval> approvals =
+        notes.getApprovals();
+    assertThat(approvals).hasSize(2);
+    assertThat(approvals.entries().asList().get(0).getValue().getTag())
+        .isEqualTo(tag1);
+    assertThat(approvals.entries().asList().get(1).getValue().getTag())
+        .isEqualTo(tag2);
+  }
+
+  @Test
+  public void multipleTags() throws Exception {
+    String ipTag = "ip";
+    String coverageTag = "coverage";
+    String integrationTag = "integration";
+    Change c = newChange();
+
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    update.putApproval("Verified", (short) -1);
+    update.setChangeMessage("integration verification");
+    update.setTag(integrationTag);
+    update.commit();
+
+    RevCommit commit = tr.commit().message("PS2").create();
+    update = newUpdate(c, changeOwner);
+    update.putComment(newPublishedComment(c.currentPatchSetId(), "a.txt",
+        "uuid1", new CommentRange(1, 2, 3, 4), 1, changeOwner, null,
+        TimeUtil.nowTs(), "Comment", (short) 1, commit.name()));
+    update.setChangeMessage("coverage verification");
+    update.setTag(coverageTag);
+    update.commit();
+
+    update = newUpdate(c, changeOwner);
+    update.setChangeMessage("ip clear");
+    update.setTag(ipTag);
+    update.commit();
+
+    ChangeNotes notes = newNotes(c);
+
+    ImmutableListMultimap<PatchSet.Id, PatchSetApproval> approvals =
+        notes.getApprovals();
+    assertThat(approvals).hasSize(1);
+    PatchSetApproval approval = approvals.entries().asList().get(0).getValue();
+    assertThat(approval.getTag()).isEqualTo(integrationTag);
+    assertThat(approval.getValue()).isEqualTo(-1);
+
+    ImmutableListMultimap<RevId, PatchLineComment> comments =
+        notes.getComments();
+    assertThat(comments).hasSize(1);
+    assertThat(comments.entries().asList().get(0).getValue().getTag())
+        .isEqualTo(coverageTag);
+
+    ImmutableList<ChangeMessage> messages = notes.getChangeMessages();
+    assertThat(messages).hasSize(3);
+    assertThat(messages.get(0).getTag()).isEqualTo(integrationTag);
+    assertThat(messages.get(1).getTag()).isEqualTo(coverageTag);
+    assertThat(messages.get(2).getTag()).isEqualTo(ipTag);
+  }
+
+  @Test
   public void approvalsOnePatchSet() throws Exception {
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, changeOwner);
@@ -413,7 +527,7 @@
   @Test
   public void emptyChangeUpdate() throws Exception {
     Change c = newChange();
-    Ref initial = repo.exactRef(ChangeNoteUtil.changeRefName(c.getId()));
+    Ref initial = repo.exactRef(changeMetaRef(c.getId()));
     assertThat(initial).isNotNull();
 
     // Empty update doesn't create a new commit.
@@ -421,7 +535,7 @@
     update.commit();
     assertThat(update.getResult()).isNull();
 
-    Ref updated = repo.exactRef(ChangeNoteUtil.changeRefName(c.getId()));
+    Ref updated = repo.exactRef(changeMetaRef(c.getId()));
     assertThat(updated.getObjectId()).isEqualTo(initial.getObjectId());
   }
 
@@ -616,18 +730,17 @@
     Timestamp ts7 = newNotes(c).getChange().getLastUpdatedOn();
     assertThat(ts7).isGreaterThan(ts6);
 
-    // Updates that should not touch the timestamp.
     update = newUpdate(c, changeOwner);
     update.putReviewer(otherUser.getAccountId(), ReviewerStateInternal.REVIEWER);
     update.commit();
     Timestamp ts8 = newNotes(c).getChange().getLastUpdatedOn();
-    assertThat(ts8).isEqualTo(ts7);
+    assertThat(ts8).isGreaterThan(ts7);
 
     update = newUpdate(c, changeOwner);
     update.setGroups(ImmutableList.of("a", "b"));
     update.commit();
     Timestamp ts9 = newNotes(c).getChange().getLastUpdatedOn();
-    assertThat(ts9).isEqualTo(ts8);
+    assertThat(ts9).isGreaterThan(ts8);
 
     // Finish off by merging the change.
     update = newUpdate(c, changeOwner);
@@ -641,6 +754,34 @@
   }
 
   @Test
+  public void subjectLeadingWhitespaceChangeNotes() throws Exception {
+    Change c = TestChanges.newChange(project, changeOwner.getAccountId());
+    String trimmedSubj = c.getSubject();
+    c.setCurrentPatchSet(c.currentPatchSetId(), "  " + trimmedSubj,
+        c.getOriginalSubject());
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    update.setChangeId(c.getKey().get());
+    update.setBranch(c.getDest().get());
+    update.commit();
+
+    ChangeNotes notes = newNotes(c);
+    assertThat(notes.getChange().getSubject()).isEqualTo(trimmedSubj);
+
+    String tabSubj = "\t\t" + trimmedSubj;
+
+    c = TestChanges.newChange(project, changeOwner.getAccountId());
+    c.setCurrentPatchSet(c.currentPatchSetId(), tabSubj,
+        c.getOriginalSubject());
+    update = newUpdate(c, changeOwner);
+    update.setChangeId(c.getKey().get());
+    update.setBranch(c.getDest().get());
+    update.commit();
+
+    notes = newNotes(c);
+    assertThat(notes.getChange().getSubject()).isEqualTo(tabSubj);
+  }
+
+  @Test
   public void commitChangeNotesUnique() throws Exception {
     // PatchSetId -> RevId must be a one to one mapping
     Change c = newChange();
@@ -787,7 +928,6 @@
       + "\n"
       + "Nor is this a real signature.\n"
       + "-----END PGP SIGNATURE-----\n";
-    String trimmedCert = CharMatcher.is('\n').trimTrailingFrom(pushCert);
 
     // ps2 with push cert
     Change c = newChange();
@@ -804,8 +944,7 @@
     assertThat(readNote(notes, commit)).isEqualTo(pushCert);
     Map<PatchSet.Id, PatchSet> patchSets = notes.getPatchSets();
     assertThat(patchSets.get(psId1).getPushCertificate()).isNull();
-    assertThat(patchSets.get(psId2).getPushCertificate())
-        .isEqualTo(trimmedCert);
+    assertThat(patchSets.get(psId2).getPushCertificate()).isEqualTo(pushCert);
     assertThat(notes.getComments()).isEmpty();
 
     // comment on ps2
@@ -820,8 +959,8 @@
     notes = newNotes(c);
     assertThat(readNote(notes, commit)).isEqualTo(
         pushCert
-        + "Patch-set: 2\n"
         + "Revision: " + commit.name() + "\n"
+        + "Patch-set: 2\n"
         + "File: a.txt\n"
         + "\n"
         + "1:2-3:4\n"
@@ -833,8 +972,7 @@
         + "\n");
     patchSets = notes.getPatchSets();
     assertThat(patchSets.get(psId1).getPushCertificate()).isNull();
-    assertThat(patchSets.get(psId2).getPushCertificate())
-        .isEqualTo(trimmedCert);
+    assertThat(patchSets.get(psId2).getPushCertificate()).isEqualTo(pushCert);
     assertThat(notes.getComments()).isNotEmpty();
   }
 
@@ -908,28 +1046,24 @@
     RevCommit commitWithComments = commitWithApprovals.getParent(0);
     assertThat(commitWithComments).isNotNull();
 
-    try (RevWalk rw = new RevWalk(repo)) {
-      try (ChangeNotesParser notesWithComments = new ChangeNotesParser(
-          project, c.getId(), commitWithComments.copy(), rw, repoManager,
-          noteUtil, args.metrics)) {
-        notesWithComments.parseAll();
-        ImmutableListMultimap<PatchSet.Id, PatchSetApproval> approvals1 =
-            notesWithComments.buildApprovals();
-        assertThat(approvals1).isEmpty();
-        assertThat(notesWithComments.comments).hasSize(1);
-      }
+    try (ChangeNotesRevWalk rw = ChangeNotesCommit.newRevWalk(repo)) {
+      ChangeNotesParser notesWithComments = new ChangeNotesParser(
+          c.getId(), commitWithComments.copy(), rw, noteUtil, args.metrics);
+      notesWithComments.parseAll();
+      ImmutableListMultimap<PatchSet.Id, PatchSetApproval> approvals1 =
+          notesWithComments.buildApprovals();
+      assertThat(approvals1).isEmpty();
+      assertThat(notesWithComments.comments).hasSize(1);
     }
 
-    try (RevWalk rw = new RevWalk(repo)) {
-      try (ChangeNotesParser notesWithApprovals = new ChangeNotesParser(project,
-          c.getId(), commitWithApprovals.copy(), rw, repoManager,
-          noteUtil, args.metrics)) {
-        notesWithApprovals.parseAll();
-        ImmutableListMultimap<PatchSet.Id, PatchSetApproval> approvals2 =
-            notesWithApprovals.buildApprovals();
-        assertThat(approvals2).hasSize(1);
-        assertThat(notesWithApprovals.comments).hasSize(1);
-      }
+    try (ChangeNotesRevWalk rw = ChangeNotesCommit.newRevWalk(repo)) {
+      ChangeNotesParser notesWithApprovals = new ChangeNotesParser(c.getId(),
+          commitWithApprovals.copy(), rw, noteUtil, args.metrics);
+      notesWithApprovals.parseAll();
+      ImmutableListMultimap<PatchSet.Id, PatchSetApproval> approvals2 =
+          notesWithApprovals.buildApprovals();
+      assertThat(approvals2).hasSize(1);
+      assertThat(notesWithApprovals.comments).hasSize(1);
     }
   }
 
@@ -1112,6 +1246,45 @@
   }
 
   @Test
+  public void patchLineCommentsFileComment() throws Exception {
+    Change c = newChange();
+    ChangeUpdate update = newUpdate(c, otherUser);
+    PatchSet.Id psId = c.currentPatchSetId();
+    RevId revId = new RevId("abcd1234abcd1234abcd1234abcd1234abcd1234");
+
+    PatchLineComment comment = newPublishedComment(psId, "file1",
+        "uuid", null, 0, otherUser, null,
+        TimeUtil.nowTs(), "message", (short) 1, revId.get());
+    update.setPatchSetId(psId);
+    update.putComment(comment);
+    update.commit();
+
+    ChangeNotes notes = newNotes(c);
+    assertThat(notes.getComments())
+        .isEqualTo(ImmutableMultimap.of(revId, comment));
+  }
+
+  @Test
+  public void patchLineCommentsZeroColumns() throws Exception {
+    Change c = newChange();
+    ChangeUpdate update = newUpdate(c, otherUser);
+    PatchSet.Id psId = c.currentPatchSetId();
+    RevId revId = new RevId("abcd1234abcd1234abcd1234abcd1234abcd1234");
+    CommentRange range = new CommentRange(1, 0, 2, 0);
+
+    PatchLineComment comment = newPublishedComment(psId, "file1",
+        "uuid", range, range.getEndLine(), otherUser, null,
+        TimeUtil.nowTs(), "message", (short) 1, revId.get());
+    update.setPatchSetId(psId);
+    update.putComment(comment);
+    update.commit();
+
+    ChangeNotes notes = newNotes(c);
+    assertThat(notes.getComments())
+        .isEqualTo(ImmutableMultimap.of(revId, comment));
+  }
+
+  @Test
   public void patchLineCommentNotesFormatSide1() throws Exception {
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, otherUser);
@@ -1163,8 +1336,9 @@
           walk.getObjectReader().open(
               note.getData(), Constants.OBJ_BLOB).getBytes();
       String noteString = new String(bytes, UTF_8);
-      assertThat(noteString).isEqualTo("Patch-set: 1\n"
-          + "Revision: abcd1234abcd1234abcd1234abcd1234abcd1234\n"
+      assertThat(noteString).isEqualTo(
+          "Revision: abcd1234abcd1234abcd1234abcd1234abcd1234\n"
+          + "Patch-set: 1\n"
           + "File: file1\n"
           + "\n"
           + "1:1-2:1\n"
@@ -1233,8 +1407,9 @@
           walk.getObjectReader().open(
               note.getData(), Constants.OBJ_BLOB).getBytes();
       String noteString = new String(bytes, UTF_8);
-      assertThat(noteString).isEqualTo("Base-for-patch-set: 1\n"
-          + "Revision: abcd1234abcd1234abcd1234abcd1234abcd1234\n"
+      assertThat(noteString).isEqualTo(
+          "Revision: abcd1234abcd1234abcd1234abcd1234abcd1234\n"
+          + "Base-for-patch-set: 1\n"
           + "File: file1\n"
           + "\n"
           + "1:1-2:1\n"
@@ -1255,6 +1430,143 @@
   }
 
   @Test
+  public void patchLineCommentNotesFormatMultiplePatchSetsSameRevId()
+      throws Exception {
+    Change c = newChange();
+    String uuid1 = "uuid1";
+    String uuid2 = "uuid2";
+    String uuid3 = "uuid3";
+    String message1 = "comment 1";
+    String message2 = "comment 2";
+    String message3 = "comment 3";
+    CommentRange range1 = new CommentRange(1, 1, 2, 1);
+    CommentRange range2 = new CommentRange(2, 1, 3, 1);
+    Timestamp time = TimeUtil.nowTs();
+    RevId revId = new RevId("abcd1234abcd1234abcd1234abcd1234abcd1234");
+
+    PatchSet.Id psId1 = c.currentPatchSetId();
+    PatchSet.Id psId2 = new PatchSet.Id(c.getId(), psId1.get() + 1);
+
+    PatchLineComment comment1 = newPublishedComment(psId1, "file1",
+        uuid1, range1, range1.getEndLine(), otherUser, null, time, message1,
+        (short) 0, revId.get());
+    PatchLineComment comment2 = newPublishedComment(psId1, "file1",
+        uuid2, range2, range2.getEndLine(), otherUser, null, time, message2,
+        (short) 0, revId.get());
+    PatchLineComment comment3 = newPublishedComment(psId2, "file1",
+        uuid3, range1, range1.getEndLine(), otherUser, null, time, message3,
+        (short) 0, revId.get());
+
+    ChangeUpdate update = newUpdate(c, otherUser);
+    update.setPatchSetId(psId2);
+    update.putComment(comment3);
+    update.putComment(comment2);
+    update.putComment(comment1);
+    update.commit();
+
+    ChangeNotes notes = newNotes(c);
+
+    try (RevWalk walk = new RevWalk(repo)) {
+      ArrayList<Note> notesInTree =
+          Lists.newArrayList(notes.revisionNoteMap.noteMap.iterator());
+      Note note = Iterables.getOnlyElement(notesInTree);
+
+      byte[] bytes =
+          walk.getObjectReader().open(
+              note.getData(), Constants.OBJ_BLOB).getBytes();
+      String noteString = new String(bytes, UTF_8);
+      String timeStr = ChangeNoteUtil.formatTime(serverIdent, time);
+      assertThat(noteString).isEqualTo(
+          "Revision: abcd1234abcd1234abcd1234abcd1234abcd1234\n"
+          + "Base-for-patch-set: 1\n"
+          + "File: file1\n"
+          + "\n"
+          + "1:1-2:1\n"
+          + timeStr + "\n"
+          + "Author: Other Account <2@gerrit>\n"
+          + "UUID: uuid1\n"
+          + "Bytes: 9\n"
+          + "comment 1\n"
+          + "\n"
+          + "2:1-3:1\n"
+          + timeStr + "\n"
+          + "Author: Other Account <2@gerrit>\n"
+          + "UUID: uuid2\n"
+          + "Bytes: 9\n"
+          + "comment 2\n"
+          + "\n"
+          + "Base-for-patch-set: 2\n"
+          + "File: file1\n"
+          + "\n"
+          + "1:1-2:1\n"
+          + timeStr + "\n"
+          + "Author: Other Account <2@gerrit>\n"
+          + "UUID: uuid3\n"
+          + "Bytes: 9\n"
+          + "comment 3\n"
+          + "\n");
+    }
+
+    assertThat(notes.getComments()).isEqualTo(
+        ImmutableMultimap.of(
+            revId, comment1,
+            revId, comment2,
+            revId, comment3));
+  }
+
+  @Test
+  public void patchLineCommentNotesFormatWeirdUser() throws Exception {
+    Account account = new Account(new Account.Id(3), TimeUtil.nowTs());
+    account.setFullName("Weird\n\u0002<User>\n");
+    account.setPreferredEmail(" we\r\nird@ex>ample<.com");
+    accountCache.put(account);
+    IdentifiedUser user = userFactory.create(account.getId());
+
+    Change c = newChange();
+    ChangeUpdate update = newUpdate(c, user);
+    String uuid = "uuid";
+    CommentRange range = new CommentRange(1, 1, 2, 1);
+    Timestamp time = TimeUtil.nowTs();
+    PatchSet.Id psId = c.currentPatchSetId();
+
+    PatchLineComment comment = newPublishedComment(psId, "file1",
+        uuid, range, range.getEndLine(), user, null, time, "comment",
+        (short) 1, "abcd1234abcd1234abcd1234abcd1234abcd1234");
+    update.setPatchSetId(psId);
+    update.putComment(comment);
+    update.commit();
+
+    ChangeNotes notes = newNotes(c);
+
+    try (RevWalk walk = new RevWalk(repo)) {
+      ArrayList<Note> notesInTree =
+          Lists.newArrayList(notes.revisionNoteMap.noteMap.iterator());
+      Note note = Iterables.getOnlyElement(notesInTree);
+
+      byte[] bytes =
+          walk.getObjectReader().open(
+              note.getData(), Constants.OBJ_BLOB).getBytes();
+      String noteString = new String(bytes, UTF_8);
+      String timeStr = ChangeNoteUtil.formatTime(serverIdent, time);
+      assertThat(noteString).isEqualTo(
+          "Revision: abcd1234abcd1234abcd1234abcd1234abcd1234\n"
+          + "Patch-set: 1\n"
+          + "File: file1\n"
+          + "\n"
+          + "1:1-2:1\n"
+          + timeStr + "\n"
+          + "Author: Weird\u0002User <3@gerrit>\n"
+          + "UUID: uuid\n"
+          + "Bytes: 7\n"
+          + "comment\n"
+          + "\n");
+    }
+
+    assertThat(notes.getComments())
+        .isEqualTo(ImmutableMultimap.of(comment.getRevId(), comment));
+  }
+
+  @Test
   public void patchLineCommentMultipleOnePatchsetOneFileBothSides()
       throws Exception {
     Change c = newChange();
@@ -1625,6 +1937,62 @@
   }
 
   @Test
+  public void addingPublishedCommentDoesNotCreateNoOpCommitOnEmptyDraftRef()
+      throws Exception {
+    Change c = newChange();
+    String uuid = "uuid";
+    String rev = "abcd4567abcd4567abcd4567abcd4567abcd4567";
+    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 comment = newComment(ps1, filename, uuid, range,
+        range.getEndLine(), otherUser, null, now, "comment on ps1", side,
+        rev, Status.PUBLISHED);
+    update.putComment(comment);
+    update.commit();
+
+    assertThat(repo.exactRef(changeMetaRef(c.getId()))).isNotNull();
+    String draftRef = refsDraftComments(c.getId(), otherUser.getAccountId());
+    assertThat(exactRefAllUsers(draftRef)).isNull();
+  }
+
+  @Test
+  public void addingPublishedCommentDoesNotCreateNoOpCommitOnNonEmptyDraftRef()
+      throws Exception {
+    Change c = newChange();
+    String rev = "abcd4567abcd4567abcd4567abcd4567abcd4567";
+    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 draft = newComment(ps1, filename, "uuid1", range,
+        range.getEndLine(), otherUser, null, now, "draft comment on ps1", side,
+        rev, Status.DRAFT);
+    update.putComment(draft);
+    update.commit();
+
+    String draftRef = refsDraftComments(c.getId(), otherUser.getAccountId());
+    ObjectId old = exactRefAllUsers(draftRef);
+    assertThat(old).isNotNull();
+
+    update = newUpdate(c, otherUser);
+    PatchLineComment pub = newComment(ps1, filename, "uuid2", range,
+        range.getEndLine(), otherUser, null, now, "comment on ps1", side,
+        rev, Status.PUBLISHED);
+    update.putComment(pub);
+    update.commit();
+
+    assertThat(exactRefAllUsers(draftRef)).isEqualTo(old);
+  }
+
+  @Test
   public void fileComment() throws Exception {
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, otherUser);
@@ -1784,8 +2152,7 @@
     update.putComment(comment2);
     update.commit();
 
-
-    String refName = RefNames.refsDraftComments(otherUserId, c.getId());
+    String refName = refsDraftComments(c.getId(), otherUserId);
     ObjectId oldDraftId = exactRefAllUsers(refName);
 
     update = newUpdate(c, otherUser);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/CommitMessageOutputTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
index 907db6b..46d8818 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
@@ -272,6 +272,60 @@
         update.getResult());
   }
 
+  @Test
+  public void changeMessageWithTag() throws Exception {
+    Change c = newChange();
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    update.setChangeMessage("Change message with tag");
+    update.setTag("jenkins");
+    update.commit();
+
+    assertBodyEquals("Update patch set 1\n"
+        + "\n"
+        + "Change message with tag\n"
+        + "\n"
+        + "Patch-set: 1\n"
+        + "Tag: jenkins\n",
+        update.getResult());
+  }
+
+  @Test
+  public void leadingWhitespace() throws Exception {
+    Change c = TestChanges.newChange(project, changeOwner.getAccountId());
+    c.setCurrentPatchSet(c.currentPatchSetId(), "  " + c.getSubject(),
+        c.getOriginalSubject());
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    update.setChangeId(c.getKey().get());
+    update.setBranch(c.getDest().get());
+    update.commit();
+
+    assertBodyEquals("Update patch set 1\n"
+        + "\n"
+        + "Patch-set: 1\n"
+        + "Change-id: " + c.getKey().get() + "\n"
+        + "Subject:   Change subject\n"
+        + "Branch: refs/heads/master\n"
+        + "Commit: " + update.getCommit().name() + "\n",
+        update.getResult());
+
+    c = TestChanges.newChange(project, changeOwner.getAccountId());
+    c.setCurrentPatchSet(c.currentPatchSetId(), "\t\t" + c.getSubject(),
+        c.getOriginalSubject());
+    update = newUpdate(c, changeOwner);
+    update.setChangeId(c.getKey().get());
+    update.setBranch(c.getDest().get());
+    update.commit();
+
+    assertBodyEquals("Update patch set 1\n"
+        + "\n"
+        + "Patch-set: 1\n"
+        + "Change-id: " + c.getKey().get() + "\n"
+        + "Subject: \t\tChange subject\n"
+        + "Branch: refs/heads/master\n"
+        + "Commit: " + update.getCommit().name() + "\n",
+        update.getResult());
+  }
+
   private RevCommit parseCommit(ObjectId id) throws Exception {
     if (id instanceof RevCommit) {
       return (RevCommit) id;
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/NoteDbChangeStateTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/NoteDbChangeStateTest.java
index 915f94a..216f71b 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/NoteDbChangeStateTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/NoteDbChangeStateTest.java
@@ -149,7 +149,7 @@
     checkArgument(args.length % 2 == 0);
     ImmutableMap.Builder<Account.Id, ObjectId> b = ImmutableMap.builder();
     for (int i = 0; i < args.length / 2; i++) {
-      b.put((Account.Id) args[2*i], (ObjectId) args[2*i+1]);
+      b.put((Account.Id) args[2 * i], (ObjectId) args[2 * i + 1]);
     }
     return b.build();
   }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/RepoSequenceTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/RepoSequenceTest.java
index b726e2d..9c265a8 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/RepoSequenceTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/RepoSequenceTest.java
@@ -160,7 +160,7 @@
 
   @Test
   public void failOnWrongType() throws Exception {
-    try (Repository repo = repoManager.openMetadataRepository(project)) {
+    try (Repository repo = repoManager.openRepository(project)) {
       TestRepository<Repository> tr = new TestRepository<>(repo);
       tr.branch(RefNames.REFS_SEQUENCES + "id").commit().create();
       try {
@@ -206,7 +206,7 @@
 
   private ObjectId writeBlob(String sequenceName, String value) {
     String refName = RefNames.REFS_SEQUENCES + sequenceName;
-    try (Repository repo = repoManager.openMetadataRepository(project);
+    try (Repository repo = repoManager.openRepository(project);
         ObjectInserter ins = repo.newObjectInserter()) {
       ObjectId newId = ins.insert(OBJ_BLOB, value.getBytes(UTF_8));
       ins.flush();
@@ -222,7 +222,7 @@
 
   private String readBlob(String sequenceName) throws Exception {
     String refName = RefNames.REFS_SEQUENCES + sequenceName;
-    try (Repository repo = repoManager.openMetadataRepository(project);
+    try (Repository repo = repoManager.openRepository(project);
         RevWalk rw = new RevWalk(repo)) {
       ObjectId id = repo.exactRef(refName).getObjectId();
       return new String(rw.getObjectReader().open(id).getCachedBytes(), UTF_8);
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 e8c62e93..6f24a4b 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
@@ -41,7 +41,6 @@
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.errors.InvalidNameException;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -76,7 +75,6 @@
 
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
@@ -914,10 +912,5 @@
     public Set<Change.Id> getStarredChanges() {
       return Collections.emptySet();
     }
-
-    @Override
-    public Collection<AccountProjectWatch> getNotificationFilters() {
-      return Collections.emptySet();
-    }
   }
 }
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 129c155..7945740 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
@@ -38,6 +38,7 @@
 import com.google.gerrit.extensions.api.changes.DraftInput;
 import com.google.gerrit.extensions.api.changes.HashtagsInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.StarsInput;
 import com.google.gerrit.extensions.api.groups.GroupInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
@@ -55,6 +56,7 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.Sequences;
+import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.change.ChangeInserter;
@@ -94,7 +96,9 @@
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashSet;
 import java.util.Iterator;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
@@ -536,47 +540,97 @@
   public void byLabel() throws Exception {
     accountManager.authenticate(AuthRequest.forUser("anotheruser"));
     TestRepository<Repo> repo = createProject("repo");
-    ChangeInserter ins = newChange(repo);
-    Change change = insert(repo, ins);
+    ChangeInserter ins = newChange(repo, null, null, null, null);
+    ChangeInserter ins2 = newChange(repo, null, null, null, null);
+    ChangeInserter ins3 = newChange(repo, null, null, null, null);
+    ChangeInserter ins4 = newChange(repo, null, null, null, null);
+    ChangeInserter ins5 = newChange(repo, null, null, null, null);
 
-    gApi.changes().id(change.getId().get()).current()
-      .review(new ReviewInput().label("Code-Review", 1));
+    Change reviewMinus2Change = insert(repo, ins);
+    gApi.changes().id(reviewMinus2Change.getId().get()).current()
+        .review(ReviewInput.reject());
+
+    Change reviewMinus1Change = insert(repo, ins2);
+    gApi.changes().id(reviewMinus1Change.getId().get()).current()
+        .review(ReviewInput.dislike());
+
+    Change noLabelChange = insert(repo, ins3);
+
+    Change reviewPlus1Change = insert(repo, ins4);
+    gApi.changes().id(reviewPlus1Change.getId().get()).current()
+        .review(ReviewInput.recommend());
+
+    Change reviewPlus2Change = insert(repo, ins5);
+    gApi.changes().id(reviewPlus2Change.getId().get()).current()
+        .review(ReviewInput.approve());
+
     Map<String, Short> m = gApi.changes()
-        .id(change.getId().get())
+        .id(reviewPlus1Change.getId().get())
         .reviewer(user.getAccountId().toString())
         .votes();
     assertThat(m).hasSize(1);
     assertThat(m).containsEntry("Code-Review", new Short((short)1));
 
-    assertQuery("label:Code-Review=-2");
-    assertQuery("label:Code-Review-2");
-    assertQuery("label:Code-Review=-1");
-    assertQuery("label:Code-Review-1");
-    assertQuery("label:Code-Review=0");
-    assertQuery("label:Code-Review=+1", change);
-    assertQuery("label:Code-Review=1", change);
-    assertQuery("label:Code-Review+1", change);
-    assertQuery("label:Code-Review=+2");
-    assertQuery("label:Code-Review=2");
-    assertQuery("label:Code-Review+2");
+    Map<Integer, Change> changes = new LinkedHashMap<>(5);
+    changes.put(2, reviewPlus2Change);
+    changes.put(1, reviewPlus1Change);
+    changes.put(0, noLabelChange);
+    changes.put(-1, reviewMinus1Change);
+    changes.put(-2, reviewMinus2Change);
 
-    assertQuery("label:Code-Review>=0", change);
-    assertQuery("label:Code-Review>0", change);
-    assertQuery("label:Code-Review>=1", change);
-    assertQuery("label:Code-Review>1");
-    assertQuery("label:Code-Review>=2");
+    assertQuery("label:Code-Review=-2", reviewMinus2Change);
+    assertQuery("label:Code-Review-2", reviewMinus2Change);
+    assertQuery("label:Code-Review=-1", reviewMinus1Change);
+    assertQuery("label:Code-Review-1", reviewMinus1Change);
+    assertQuery("label:Code-Review=0", noLabelChange);
+    assertQuery("label:Code-Review=+1", reviewPlus1Change);
+    assertQuery("label:Code-Review=1", reviewPlus1Change);
+    assertQuery("label:Code-Review+1", reviewPlus1Change);
+    assertQuery("label:Code-Review=+2", reviewPlus2Change);
+    assertQuery("label:Code-Review=2", reviewPlus2Change);
+    assertQuery("label:Code-Review+2", reviewPlus2Change);
 
-    assertQuery("label: Code-Review<=2", change);
-    assertQuery("label: Code-Review<2", change);
-    assertQuery("label: Code-Review<=1", change);
-    assertQuery("label:Code-Review<1");
-    assertQuery("label:Code-Review<=0");
+    assertQuery("label:Code-Review>-3", codeReviewInRange(changes, -2, 2));
+    assertQuery("label:Code-Review>=-2", codeReviewInRange(changes, -2, 2));
+    assertQuery("label:Code-Review>-2", codeReviewInRange(changes, -1, 2));
+    assertQuery("label:Code-Review>=-1", codeReviewInRange(changes, -1, 2));
+    assertQuery("label:Code-Review>-1", codeReviewInRange(changes, 0, 2));
+    assertQuery("label:Code-Review>=0", codeReviewInRange(changes, 0, 2));
+    assertQuery("label:Code-Review>0", codeReviewInRange(changes, 1, 2));
+    assertQuery("label:Code-Review>=1", codeReviewInRange(changes, 1, 2));
+    assertQuery("label:Code-Review>1", reviewPlus2Change);
+    assertQuery("label:Code-Review>=2", reviewPlus2Change);
+    assertQuery("label:Code-Review>2");
+
+    assertQuery("label:Code-Review<=2", codeReviewInRange(changes, -2, 2));
+    assertQuery("label:Code-Review<2", codeReviewInRange(changes, -2, 1));
+    assertQuery("label:Code-Review<=1", codeReviewInRange(changes, -2, 1));
+    assertQuery("label:Code-Review<1", codeReviewInRange(changes, -2, 0));
+    assertQuery("label:Code-Review<=0", codeReviewInRange(changes, -2, 0));
+    assertQuery("label:Code-Review<0", codeReviewInRange(changes, -2, -1));
+    assertQuery("label:Code-Review<=-1", codeReviewInRange(changes, -2, -1));
+    assertQuery("label:Code-Review<-1", reviewMinus2Change);
+    assertQuery("label:Code-Review<=-2", reviewMinus2Change);
+    assertQuery("label:Code-Review<-2");
 
     assertQuery("label:Code-Review=+1,anotheruser");
-    assertQuery("label:Code-Review=+1,user", change);
-    assertQuery("label:Code-Review=+1,user=user", change);
-    assertQuery("label:Code-Review=+1,Administrators", change);
-    assertQuery("label:Code-Review=+1,group=Administrators", change);
+    assertQuery("label:Code-Review=+1,user", reviewPlus1Change);
+    assertQuery("label:Code-Review=+1,user=user", reviewPlus1Change);
+    assertQuery("label:Code-Review=+1,Administrators", reviewPlus1Change);
+    assertQuery("label:Code-Review=+1,group=Administrators", reviewPlus1Change);
+  }
+
+  private Change[] codeReviewInRange(Map<Integer, Change> changes, int start,
+      int end) {
+    int size = 0;
+    Change[] range = new Change[end - start + 1];
+    for (int i : changes.keySet()) {
+      if (i >= start && i <= end) {
+        range[size] = changes.get(i);
+        size++;
+      }
+    }
+    return range;
   }
 
   private String createGroup(String name, String owner) throws Exception {
@@ -652,7 +706,7 @@
   @Test
   public void start() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
-    List<Change> changes = Lists.newArrayList();
+    List<Change> changes = new ArrayList<>();
     for (int i = 0; i < 2; i++) {
       changes.add(insert(repo, newChange(repo)));
     }
@@ -666,7 +720,7 @@
   @Test
   public void startWithLimit() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
-    List<Change> changes = Lists.newArrayList();
+    List<Change> changes = new ArrayList<>();
     for (int i = 0; i < 3; i++) {
       changes.add(insert(repo, newChange(repo)));
     }
@@ -696,8 +750,8 @@
   public void updateOrder() throws Exception {
     resetTimeWithClockStep(2, MINUTES);
     TestRepository<Repo> repo = createProject("repo");
-    List<ChangeInserter> inserters = Lists.newArrayList();
-    List<Change> changes = Lists.newArrayList();
+    List<ChangeInserter> inserters = new ArrayList<>();
+    List<Change> changes = new ArrayList<>();
     for (int i = 0; i < 5; i++) {
       inserters.add(newChange(repo));
       changes.add(insert(repo, inserters.get(i)));
@@ -1020,7 +1074,7 @@
 
   @Test
   public void byHashtagWithNoteDb() throws Exception {
-    assume().that(notesMigration.enabled()).isTrue();
+    assume().that(notesMigration.readChanges()).isTrue();
     List<Change> changes = setUpHashtagChanges();
     assertQuery("hashtag:foo", changes.get(1), changes.get(0));
     assertQuery("hashtag:bar", changes.get(1));
@@ -1033,7 +1087,7 @@
 
   @Test
   public void byHashtagWithoutNoteDb() throws Exception {
-    assume().that(notesMigration.enabled()).isFalse();
+    assume().that(notesMigration.readChanges()).isFalse();
     setUpHashtagChanges();
     assertQuery("hashtag:foo");
     assertQuery("hashtag:bar");
@@ -1173,6 +1227,52 @@
   }
 
   @Test
+  public void byStarredBy() throws Exception {
+    TestRepository<Repo> repo = createProject("repo");
+    Change change1 = insert(repo, newChange(repo));
+    Change change2 = insert(repo, newChange(repo));
+    insert(repo, newChange(repo));
+
+    gApi.accounts().self().starChange(change1.getId().toString());
+    gApi.accounts().self().starChange(change2.getId().toString());
+
+    int user2 = accountManager.authenticate(AuthRequest.forUser("anotheruser"))
+        .getAccountId().get();
+
+    assertQuery("starredby:self", change2, change1);
+    assertQuery("starredby:" + user2);
+  }
+
+  @Test
+  public void byStar() throws Exception {
+    TestRepository<Repo> repo = createProject("repo");
+    Change change1 = insert(repo, newChange(repo));
+    Change change2 = insert(repo, newChangeWithStatus(repo, Change.Status.MERGED));
+    insert(repo, newChangeWithStatus(repo, Change.Status.MERGED));
+
+    gApi.accounts()
+        .self()
+        .setStars(change1.getId().toString(),
+            new StarsInput(new HashSet<>(Arrays.asList("red", "blue"))));
+    gApi.accounts()
+        .self()
+        .setStars(change2.getId().toString(),
+            new StarsInput(new HashSet<>(Arrays.asList(
+                StarredChangesUtil.DEFAULT_LABEL, "green", "blue"))));
+
+    // check labeled stars
+    assertQuery("star:red", change1);
+    assertQuery("star:blue", change2, change1);
+    assertQuery("has:stars", change2, change1);
+
+    // check default star
+    assertQuery("has:star", change2);
+    assertQuery("is:starred", change2);
+    assertQuery("starredby:self", change2);
+    assertQuery("star:" + StarredChangesUtil.DEFAULT_LABEL, change2);
+  }
+
+  @Test
   public void byFrom() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
     Change change1 = insert(repo, newChange(repo));
@@ -1315,7 +1415,7 @@
 
   @Test
   public void prepopulatedFields() throws Exception {
-    assume().that(notesMigration.enabled()).isFalse();
+    assume().that(notesMigration.readChanges()).isFalse();
     TestRepository<Repo> repo = createProject("repo");
     Change change = insert(repo, newChange(repo));
 
@@ -1344,7 +1444,7 @@
 
   @Test
   public void prepopulateOnlyRequestedFields() throws Exception {
-    assume().that(notesMigration.enabled()).isFalse();
+    assume().that(notesMigration.readChanges()).isFalse();
     TestRepository<Repo> repo = createProject("repo");
     Change change = insert(repo, newChange(repo));
 
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 87b5322..cd6e825 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
@@ -21,7 +21,6 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.LabelValue;
@@ -44,6 +43,7 @@
 import java.io.IOException;
 import java.sql.ResultSet;
 import java.sql.SQLException;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 
@@ -111,7 +111,7 @@
 
   @Test
   public void testCreateSchema_LabelTypes() throws Exception {
-    List<String> labels = Lists.newArrayList();
+    List<String> labels = new ArrayList<>();
     for (LabelType label : getLabelTypes().getLabelTypes()) {
       labels.add(label.getName());
     }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/HookTestCase.java b/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/HookTestCase.java
index 56e460d..69a8487 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/HookTestCase.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/HookTestCase.java
@@ -52,8 +52,6 @@
 
 import static com.google.common.truth.Truth.assert_;
 
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
 import com.google.common.io.ByteStreams;
 
 import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
@@ -67,14 +65,16 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.URL;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.TreeMap;
 
 @Ignore
 public abstract class HookTestCase extends LocalDiskRepositoryTestCase {
   protected Repository repository;
-  private final Map<String, File> hooks = Maps.newTreeMap();
-  private final List<File> cleanup = Lists.newArrayList();
+  private final Map<String, File> hooks = new TreeMap<>();
+  private final List<File> cleanup = new ArrayList<>();
 
   @Override
   @Before
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 3945da7..ba62cf7 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
@@ -21,7 +21,6 @@
 import static org.easymock.EasyMock.verify;
 import static org.junit.Assert.assertEquals;
 
-import com.google.common.collect.Sets;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
@@ -37,6 +36,7 @@
 import java.net.URI;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 import java.util.TreeMap;
@@ -88,7 +88,7 @@
         new Branch.NameKey(new Project.NameKey("super-project"),
             "refs/heads/master");
 
-    Set<SubmoduleSubscription> expectedSubscriptions = Sets.newHashSet();
+    Set<SubmoduleSubscription> expectedSubscriptions = new HashSet<>();
     expectedSubscriptions
         .add(new SubmoduleSubscription(superBranchNameKey, new Branch.NameKey(
             new Project.NameKey("a"), "refs/heads/master"), "a"));
@@ -135,7 +135,7 @@
         new Branch.NameKey(new Project.NameKey("super-project"),
             "refs/heads/master");
 
-    Set<SubmoduleSubscription> expectedSubscriptions = Sets.newHashSet();
+    Set<SubmoduleSubscription> expectedSubscriptions = new HashSet<>();
     expectedSubscriptions
         .add(new SubmoduleSubscription(superBranchNameKey, new Branch.NameKey(
             new Project.NameKey("a"), "refs/heads/master"), "a"));
@@ -204,7 +204,7 @@
         new Branch.NameKey(new Project.NameKey("super-project"),
             "refs/heads/master");
 
-    Set<SubmoduleSubscription> expectedSubscriptions = Sets.newHashSet();
+    Set<SubmoduleSubscription> expectedSubscriptions = new HashSet<>();
     expectedSubscriptions
         .add(new SubmoduleSubscription(superBranchNameKey, new Branch.NameKey(
             new Project.NameKey("a/b"), "refs/heads/master"), "a/b"));
@@ -268,7 +268,7 @@
     private final String path;
     private final String branch;
 
-    public SubmoduleSection(final String url, final String path,
+    SubmoduleSection(final String url, final String path,
         final String branch) {
       this.url = url;
       this.path = path;
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/DisabledReviewDb.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/DisabledReviewDb.java
index fa7a5c4..ad9a46a 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/DisabledReviewDb.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/DisabledReviewDb.java
@@ -24,7 +24,6 @@
 import com.google.gerrit.reviewdb.server.AccountGroupNameAccess;
 import com.google.gerrit.reviewdb.server.AccountPatchReviewAccess;
 import com.google.gerrit.reviewdb.server.AccountProjectWatchAccess;
-import com.google.gerrit.reviewdb.server.AccountSshKeyAccess;
 import com.google.gerrit.reviewdb.server.ChangeAccess;
 import com.google.gerrit.reviewdb.server.ChangeMessageAccess;
 import com.google.gerrit.reviewdb.server.PatchLineCommentAccess;
@@ -32,7 +31,6 @@
 import com.google.gerrit.reviewdb.server.PatchSetApprovalAccess;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.reviewdb.server.SchemaVersionAccess;
-import com.google.gerrit.reviewdb.server.StarredChangeAccess;
 import com.google.gerrit.reviewdb.server.SystemConfigAccess;
 import com.google.gwtorm.server.Access;
 import com.google.gwtorm.server.StatementExecutor;
@@ -98,11 +96,6 @@
   }
 
   @Override
-  public AccountSshKeyAccess accountSshKeys() {
-    throw new Disabled();
-  }
-
-  @Override
   public AccountGroupAccess accountGroups() {
     throw new Disabled();
   }
@@ -123,11 +116,6 @@
   }
 
   @Override
-  public StarredChangeAccess starredChanges() {
-    throw new Disabled();
-  }
-
-  @Override
   public AccountProjectWatchAccess accountProjectWatches() {
     throw new Disabled();
   }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeAccountCache.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeAccountCache.java
index 011d69d..d9841a3 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeAccountCache.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeAccountCache.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.testutil;
 
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Maps;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
@@ -23,6 +22,7 @@
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
 
+import java.util.HashMap;
 import java.util.Map;
 
 /** Fake implementation of {@link AccountCache} for testing. */
@@ -31,8 +31,8 @@
   private final Map<String, AccountState> byUsername;
 
   public FakeAccountCache() {
-    byId = Maps.newHashMap();
-    byUsername = Maps.newHashMap();
+    byId = new HashMap<>();
+    byUsername = new HashMap<>();
   }
 
   @Override
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
index 7f8fc32..bbcb6a9 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/FilesystemLoggingMockingTestCase.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/FilesystemLoggingMockingTestCase.java
@@ -15,17 +15,17 @@
 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.ArrayList;
 import java.util.Collection;
 
 public abstract class FilesystemLoggingMockingTestCase extends LoggingMockingTestCase {
 
-  private Collection<File> toCleanup = Lists.newArrayList();
+  private Collection<File> toCleanup = new ArrayList<>();
 
   /**
    * Asserts that a given file exists.
@@ -118,7 +118,7 @@
    * @throws IOException If a file could not be created.
    */
   private File createTempFile(String suffix) throws IOException {
-    String prefix ="gerrit_test_";
+    String prefix = "gerrit_test_";
     if (!Strings.isNullOrEmpty(getName())) {
       prefix += getName() + "_";
     }
@@ -175,4 +175,4 @@
     cleanupCreatedFiles();
     super.tearDown();
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryH2Type.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryH2Type.java
index 25a6534..7edfa1a 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryH2Type.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryH2Type.java
@@ -27,4 +27,4 @@
     // not used
     throw new UnsupportedOperationException();
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryRepositoryManager.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryRepositoryManager.java
index 656185d..74fa22d 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryRepositoryManager.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryRepositoryManager.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.testutil;
 
 import com.google.common.collect.ImmutableSortedSet;
-import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -26,6 +25,7 @@
 import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 
+import java.util.HashMap;
 import java.util.Map;
 import java.util.SortedSet;
 
@@ -64,7 +64,7 @@
     }
   }
 
-  private Map<String, Repo> repos = Maps.newHashMap();
+  private Map<String, Repo> repos = new HashMap<>();
 
   @Override
   public synchronized Repo openRepository(Project.NameKey name)
@@ -89,12 +89,6 @@
   }
 
   @Override
-  public synchronized Repo openMetadataRepository(
-      Project.NameKey name) throws RepositoryNotFoundException {
-    return openRepository(name);
-  }
-
-  @Override
   public synchronized SortedSet<Project.NameKey> list() {
     SortedSet<Project.NameKey> names = Sets.newTreeSet();
     for (DfsRepository repo : repos.values()) {
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
index 218fa0a..d7140ec 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/LoggingMockingTestCase.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/LoggingMockingTestCase.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.testutil;
 
-import com.google.common.collect.Lists;
 import com.google.gerrit.testutil.log.LogUtil;
 
 import org.apache.log4j.LogManager;
@@ -22,6 +21,7 @@
 import org.apache.log4j.spi.LoggingEvent;
 import org.junit.After;
 
+import java.util.ArrayList;
 import java.util.Iterator;
 
 /**
@@ -102,14 +102,14 @@
   @Override
   public void setUp() throws Exception {
     super.setUp();
-    loggedEvents = Lists.newArrayList();
+    loggedEvents = new ArrayList<>();
 
     // 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);
+    loggerName = loggerName.substring(0, loggerName.length() - 4);
     loggerSettings = LogUtil.logToCollection(loggerName, loggedEvents);
   }
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbChecker.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbChecker.java
index 95101ab..66eed8b 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbChecker.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbChecker.java
@@ -20,12 +20,12 @@
 import com.google.common.collect.Iterables;
 import com.google.gerrit.reviewdb.client.Change;
 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.reviewdb.server.ReviewDbUtil;
 import com.google.gerrit.server.PatchLineCommentsUtil;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.ChangeBundle;
-import com.google.gerrit.server.notedb.ChangeNoteUtil;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeRebuilder;
 import com.google.gerrit.server.schema.DisabledChangesReviewDbWrapper;
@@ -117,9 +117,8 @@
 
   public void assertNoChangeRef(Project.NameKey project, Change.Id changeId)
       throws Exception {
-    try (Repository repo = repoManager.openMetadataRepository(project)) {
-      assertThat(repo.exactRef(ChangeNoteUtil.changeRefName(changeId)))
-          .isNull();
+    try (Repository repo = repoManager.openRepository(project)) {
+      assertThat(repo.exactRef(RefNames.changeMetaRef(changeId))).isNull();
     }
   }
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestNotesMigration.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestNotesMigration.java
index 07d20f8..f015636 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestNotesMigration.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestNotesMigration.java
@@ -22,6 +22,7 @@
 public class TestNotesMigration extends NotesMigration {
   private volatile boolean readChanges;
   private volatile boolean writeChanges;
+  private volatile boolean failOnLoad;
 
   @Override
   public boolean readChanges() {
@@ -43,6 +44,11 @@
     return false;
   }
 
+  @Override
+  public boolean failOnLoad() {
+    return failOnLoad;
+  }
+
   public TestNotesMigration setReadChanges(boolean readChanges) {
     this.readChanges = readChanges;
     return this;
@@ -53,6 +59,11 @@
     return this;
   }
 
+  public TestNotesMigration setFailOnLoad(boolean failOnLoad) {
+    this.failOnLoad = failOnLoad;
+    return this;
+  }
+
   public TestNotesMigration setAllEnabled(boolean enabled) {
     return setReadChanges(enabled).setWriteChanges(enabled);
   }
@@ -70,6 +81,8 @@
       case CHECK:
       case OFF:
       default:
+        setWriteChanges(false);
+        setReadChanges(false);
         break;
     }
     return this;
diff --git a/gerrit-sshd/BUCK b/gerrit-sshd/BUCK
index ae70857..54b83e2 100644
--- a/gerrit-sshd/BUCK
+++ b/gerrit-sshd/BUCK
@@ -24,12 +24,12 @@
     '//lib/guice:guice',
     '//lib/guice:guice-assistedinject',
     '//lib/guice:guice-servlet',  # SSH should not depend on servlet
+    '//lib/jgit/org.eclipse.jgit:jgit',
+    '//lib/jgit/org.eclipse.jgit.archive:jgit-archive',
     '//lib/log:api',
     '//lib/log:log4j',
     '//lib/mina:core',
     '//lib/mina:sshd',
-    '@jgit//org.eclipse.jgit:jgit',
-    '@jgit//org.eclipse.jgit.archive:jgit-archive',
   ],
   provided_deps = [
     '//lib/bouncycastle:bcprov',
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java
index dac0263..bbad2be 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java
@@ -15,12 +15,10 @@
 package com.google.gerrit.sshd;
 
 import com.google.common.base.Throwables;
-import com.google.common.collect.Lists;
 import com.google.common.util.concurrent.Atomics;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.CapabilityControl;
-import com.google.inject.Provider;
 
 import org.apache.sshd.server.Command;
 import org.apache.sshd.server.Environment;
@@ -33,12 +31,12 @@
 /** Command that executes some other command. */
 public class AliasCommand extends BaseCommand {
   private final DispatchCommandProvider root;
-  private final Provider<CurrentUser> currentUser;
+  private final CurrentUser currentUser;
   private final CommandName command;
   private final AtomicReference<Command> atomicCmd;
 
   AliasCommand(@CommandName(Commands.ROOT) DispatchCommandProvider root,
-      Provider<CurrentUser> currentUser, CommandName command) {
+      CurrentUser currentUser, CommandName command) {
     this.root = root;
     this.currentUser = currentUser;
     this.command = command;
@@ -108,19 +106,18 @@
   private void checkRequiresCapability(Command cmd) throws UnloggedFailure {
     RequiresCapability rc = cmd.getClass().getAnnotation(RequiresCapability.class);
     if (rc != null) {
-      CurrentUser user = currentUser.get();
-      CapabilityControl ctl = user.getCapabilities();
+      CapabilityControl ctl = currentUser.getCapabilities();
       if (!ctl.canPerform(rc.value()) && !ctl.canAdministrateServer()) {
         String msg = String.format(
             "fatal: %s does not have \"%s\" capability.",
-            user.getUserName(), rc.value());
+            currentUser.getUserName(), rc.value());
         throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
       }
     }
   }
 
   private static LinkedList<String> chain(CommandName command) {
-    LinkedList<String> chain = Lists.newLinkedList();
+    LinkedList<String> chain = new LinkedList<>();
     while (command != null) {
       chain.addFirst(command.value());
       command = Commands.parentOf(command);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommandProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommandProvider.java
index ee28e03..432844c 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommandProvider.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommandProvider.java
@@ -29,7 +29,7 @@
   private DispatchCommandProvider root;
 
   @Inject
-  private Provider<CurrentUser> currentUser;
+  private CurrentUser currentUser;
 
   public AliasCommandProvider(CommandName command) {
     this.command = command;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
index ec49f5c..f296ef3 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
@@ -33,7 +33,6 @@
 import com.google.gerrit.util.cli.CmdLineParser;
 import com.google.gerrit.util.cli.EndOfOptionsHandler;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 
 import org.apache.sshd.common.SshException;
 import org.apache.sshd.server.Command;
@@ -89,10 +88,10 @@
   private WorkQueue.Executor executor;
 
   @Inject
-  private Provider<CurrentUser> user;
+  private CurrentUser user;
 
   @Inject
-  private Provider<SshScope.Context> contextProvider;
+  private SshScope.Context context;
 
   /** Commands declared by a plugin can be scoped by the plugin name. */
   @Inject(optional = true)
@@ -278,7 +277,7 @@
     final TaskThunk tt = new TaskThunk(thunk);
 
     if (isAdminHighPriorityCommand()
-        && user.get().getCapabilities().canAdministrateServer()) {
+        && user.getCapabilities().canAdministrateServer()) {
       // Admin commands should not block the main work threads (there
       // might be an interactive shell there), nor should they wait
       // for the main work threads.
@@ -289,7 +288,7 @@
     }
   }
 
-  private final boolean isAdminHighPriorityCommand() {
+  private boolean isAdminHighPriorityCommand() {
     return getClass().getAnnotation(AdminHighPriorityCommand.class) != null;
   }
 
@@ -332,8 +331,8 @@
     if (!(e instanceof UnloggedFailure)) {
       final StringBuilder m = new StringBuilder();
       m.append("Internal server error");
-      if (user.get().isIdentifiedUser()) {
-        final IdentifiedUser u = user.get().asIdentifiedUser();
+      if (user.isIdentifiedUser()) {
+        final IdentifiedUser u = user.asIdentifiedUser();
         m.append(" (user ");
         m.append(u.getAccount().getUserName());
         m.append(" account ");
@@ -341,7 +340,7 @@
         m.append(")");
       }
       m.append(" during ");
-      m.append(contextProvider.get().getCommandLine());
+      m.append(context.getCommandLine());
       log.error(m.toString(), e);
     }
 
@@ -388,18 +387,16 @@
 
   private final class TaskThunk implements CancelableRunnable, ProjectRunnable {
     private final CommandRunnable thunk;
-    private final Context context;
     private final String taskName;
     private Project.NameKey projectName;
 
     private TaskThunk(final CommandRunnable thunk) {
       this.thunk = thunk;
-      this.context = contextProvider.get();
 
       StringBuilder m = new StringBuilder();
       m.append(context.getCommandLine());
-      if (user.get().isIdentifiedUser()) {
-        IdentifiedUser u = user.get().asIdentifiedUser();
+      if (user.isIdentifiedUser()) {
+        IdentifiedUser u = user.asIdentifiedUser();
         m.append(" (").append(u.getAccount().getUserName()).append(")");
       }
       this.taskName = m.toString();
@@ -488,12 +485,12 @@
   }
 
   /** Runnable function which can throw an exception. */
-  public static interface CommandRunnable {
+  public interface CommandRunnable {
     void run() throws Exception;
   }
 
   /** Runnable function which can retrieve a project name related to the task */
-  public static interface ProjectCommandRunnable extends CommandRunnable {
+  public interface ProjectCommandRunnable extends CommandRunnable {
     // execute parser command before running, in order to be able to retrieve
     // project name
     void executeParseCommand() throws Exception;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandMetaData.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandMetaData.java
index 7fb9226..5b8f5fa 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandMetaData.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandMetaData.java
@@ -27,7 +27,7 @@
 @Target({ElementType.TYPE})
 @Retention(RUNTIME)
 public @interface CommandMetaData {
-  public enum Mode {
+  enum Mode {
     MASTER, MASTER_OR_SLAVE;
     public boolean isSupported(boolean slaveMode) {
       return this == MASTER_OR_SLAVE || !slaveMode;
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 64fc48aaf..8873be9 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
@@ -23,7 +23,6 @@
 import com.google.gerrit.server.account.CapabilityUtils;
 import com.google.gerrit.server.args4j.SubcommandHandler;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 
 import org.apache.sshd.server.Command;
@@ -45,7 +44,7 @@
     DispatchCommand create(Map<String, CommandProvider> map);
   }
 
-  private final Provider<CurrentUser> currentUser;
+  private final CurrentUser currentUser;
   private final Map<String, CommandProvider> commands;
   private final AtomicReference<Command> atomicCmd;
 
@@ -56,7 +55,7 @@
   private List<String> args = new ArrayList<>();
 
   @Inject
-  DispatchCommand(final Provider<CurrentUser> cu,
+  DispatchCommand(CurrentUser cu,
       @Assisted final Map<String, CommandProvider> all) {
     currentUser = cu;
     commands = all;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java
index 8c43438a..8b468a7 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java
@@ -18,7 +18,6 @@
 
 import com.google.common.base.Preconditions;
 import com.google.common.collect.LinkedListMultimap;
-import com.google.common.collect.Maps;
 import com.google.common.collect.Multimap;
 import com.google.gerrit.extensions.annotations.Export;
 import com.google.gerrit.server.plugins.InvalidPluginException;
@@ -30,12 +29,13 @@
 import org.apache.sshd.server.Command;
 
 import java.lang.annotation.Annotation;
+import java.util.HashMap;
 import java.util.Map;
 
 class SshAutoRegisterModuleGenerator
     extends AbstractModule
     implements ModuleGenerator {
-  private final Map<String, Class<Command>> commands = Maps.newHashMap();
+  private final Map<String, Class<Command>> commands = new HashMap<>();
   private final Multimap<TypeLiteral<?>, Class<?>> listeners = LinkedListMultimap.create();
   private CommandName command;
 
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 0e33a50..5121717 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
@@ -21,7 +21,6 @@
 import com.google.common.base.Strings;
 import com.google.common.base.Supplier;
 import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
 import com.google.gerrit.common.Version;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.metrics.Counter0;
@@ -140,7 +139,7 @@
   private static final Logger sshDaemonLog =
       LoggerFactory.getLogger(SshDaemon.class);
 
-  public static enum SshSessionBackend {
+  public enum SshSessionBackend {
     MINA,
     NIO2
   }
@@ -489,21 +488,21 @@
         do {
           bits = next(31);
           val = bits % n;
-        } while (bits - val + (n-1) < 0);
+        } while (bits - val + (n - 1) < 0);
         return val;
       }
       throw new IllegalArgumentException();
     }
 
     protected final int next(int numBits) {
-      int bytes = (numBits+7)/8;
+      int bytes = (numBits + 7) / 8;
       byte[] next = new byte[bytes];
       int ret = 0;
       random.nextBytes(next);
       for (int i = 0; i < bytes; i++) {
         ret = (next[i] & 0xFF) | (ret << 8);
       }
-      return ret >>> (bytes*8 - numBits);
+      return ret >>> (bytes * 8 - numBits);
     }
   }
 
@@ -625,8 +624,7 @@
   }
 
   private void initCompression(boolean enableCompression) {
-    List<NamedFactory<Compression>> compressionFactories =
-        Lists.newArrayList();
+    List<NamedFactory<Compression>> compressionFactories = new ArrayList<>();
 
     // Always support no compression over SSHD.
     compressionFactories.add(BuiltinCompressions.none);
@@ -661,7 +659,7 @@
   private void initUserAuth(final PublickeyAuthenticator pubkey,
       final GSSAuthenticator kerberosAuthenticator,
       String kerberosKeytab, String kerberosPrincipal) {
-    List<NamedFactory<UserAuth>> authFactories = Lists.newArrayList();
+    List<NamedFactory<UserAuth>> authFactories = new ArrayList<>();
     if (kerberosKeytab != null) {
       authFactories.add(UserAuthGSSFactory.INSTANCE);
       log.info("Enabling kerberos with keytab " + kerberosKeytab);
@@ -674,7 +672,7 @@
         try {
           kerberosPrincipal = "host/" +
               InetAddress.getLocalHost().getCanonicalHostName();
-        } catch(UnknownHostException e) {
+        } catch (UnknownHostException e) {
           kerberosPrincipal = "host/localhost";
         }
       }
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 4654069..bf3e6bc 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
@@ -18,13 +18,13 @@
 
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
-import com.google.gerrit.common.errors.InvalidSshKeyException;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.client.AccountSshKey;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.VersionedAuthorizedKeys;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.ssh.SshKeyCache;
-import com.google.gwtorm.server.OrmException;
+import com.google.gerrit.server.ssh.SshKeyCreator;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Module;
@@ -32,12 +32,11 @@
 import com.google.inject.TypeLiteral;
 import com.google.inject.name.Named;
 
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.security.NoSuchAlgorithmException;
-import java.security.NoSuchProviderException;
-import java.security.spec.InvalidKeySpecException;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -64,6 +63,7 @@
           .loader(Loader.class);
         bind(SshKeyCacheImpl.class);
         bind(SshKeyCache.class).to(SshKeyCacheImpl.class);
+        bind(SshKeyCreator.class).to(SshKeyCreatorImpl.class);
       }
     };
   }
@@ -97,48 +97,34 @@
     }
   }
 
-  @Override
-  public AccountSshKey create(AccountSshKey.Id id, String encoded)
-      throws InvalidSshKeyException {
-    try {
-      final AccountSshKey key =
-          new AccountSshKey(id, SshUtil.toOpenSshPublicKey(encoded));
-      SshUtil.parse(key);
-      return key;
-    } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
-      throw new InvalidSshKeyException();
-
-    } catch (NoSuchProviderException e) {
-      log.error("Cannot parse SSH key", e);
-      throw new InvalidSshKeyException();
-    }
-  }
-
   static class Loader extends CacheLoader<String, Iterable<SshKeyCacheEntry>> {
     private final SchemaFactory<ReviewDb> schema;
+    private final VersionedAuthorizedKeys.Accessor authorizedKeys;
 
     @Inject
-    Loader(SchemaFactory<ReviewDb> schema) {
+    Loader(SchemaFactory<ReviewDb> schema,
+        VersionedAuthorizedKeys.Accessor authorizedKeys) {
       this.schema = schema;
+      this.authorizedKeys = authorizedKeys;
     }
 
     @Override
     public Iterable<SshKeyCacheEntry> load(String username) throws Exception {
       try (ReviewDb db = schema.open()) {
-        final AccountExternalId.Key key =
+        AccountExternalId.Key key =
             new AccountExternalId.Key(SCHEME_USERNAME, username);
-        final AccountExternalId user = db.accountExternalIds().get(key);
+        AccountExternalId user = db.accountExternalIds().get(key);
         if (user == null) {
           return NO_SUCH_USER;
         }
 
-        final List<SshKeyCacheEntry> kl = new ArrayList<>(4);
-        for (AccountSshKey k : db.accountSshKeys().byAccount(
-            user.getAccountId())) {
+        List<SshKeyCacheEntry> kl = new ArrayList<>(4);
+        for (AccountSshKey k : authorizedKeys.getKeys(user.getAccountId())) {
           if (k.isValid()) {
-            add(db, kl, k);
+            add(kl, k);
           }
         }
+
         if (kl.isEmpty()) {
           return NO_KEYS;
         }
@@ -146,7 +132,7 @@
       }
     }
 
-    private void add(ReviewDb db, List<SshKeyCacheEntry> kl, AccountSshKey k) {
+    private void add(List<SshKeyCacheEntry> kl, AccountSshKey k) {
       try {
         kl.add(new SshKeyCacheEntry(k.getKey(), SshUtil.parse(k)));
       } catch (OutOfMemoryError e) {
@@ -155,16 +141,16 @@
         //
         throw e;
       } catch (Throwable e) {
-        markInvalid(db, k);
+        markInvalid(k);
       }
     }
 
-    private void markInvalid(final ReviewDb db, final AccountSshKey k) {
+    private void markInvalid(AccountSshKey k) {
       try {
         log.info("Flagging SSH key " + k.getKey() + " invalid");
+        authorizedKeys.markKeyInvalid(k.getAccount(), k.getKey().get());
         k.setInvalid();
-        db.accountSshKeys().update(Collections.singleton(k));
-      } catch (OrmException e) {
+      } catch (IOException | ConfigInvalidException e) {
         log.error("Failed to mark SSH key" + k.getKey() + " invalid", e);
       }
     }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCreatorImpl.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCreatorImpl.java
new file mode 100644
index 0000000..0fd6db4
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCreatorImpl.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd;
+
+import com.google.gerrit.common.errors.InvalidSshKeyException;
+import com.google.gerrit.reviewdb.client.AccountSshKey;
+import com.google.gerrit.server.ssh.SshKeyCreator;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.spec.InvalidKeySpecException;
+
+public class SshKeyCreatorImpl implements SshKeyCreator {
+  private static final Logger log =
+      LoggerFactory.getLogger(SshKeyCreatorImpl.class);
+
+  @Override
+  public AccountSshKey create(AccountSshKey.Id id, String encoded)
+      throws InvalidSshKeyException {
+    try {
+      AccountSshKey key =
+          new AccountSshKey(id, SshUtil.toOpenSshPublicKey(encoded));
+      SshUtil.parse(key);
+      return key;
+    } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
+      throw new InvalidSshKeyException();
+
+    } catch (NoSuchProviderException e) {
+      log.error("Cannot parse SSH key", e);
+      throw new InvalidSshKeyException();
+    }
+  }
+}
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 974b233..3429587 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
@@ -17,7 +17,6 @@
 import static com.google.gerrit.extensions.registration.PrivateInternals_DynamicTypes.registerInParentInjectors;
 import static com.google.inject.Scopes.SINGLETON;
 
-import com.google.common.collect.Maps;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.server.PeerDaemonUser;
 import com.google.gerrit.server.RemotePeer;
@@ -42,6 +41,7 @@
 import org.eclipse.jgit.lib.Config;
 
 import java.net.SocketAddress;
+import java.util.HashMap;
 import java.util.Map;
 
 /** Configures standard dependencies for {@link SshDaemon}. */
@@ -50,7 +50,7 @@
 
   @Inject
   SshModule(@GerritServerConfig Config cfg) {
-    aliases = Maps.newHashMap();
+    aliases = new HashMap<>();
     for (String name : cfg.getNames("ssh-alias", true)) {
       aliases.put(name, cfg.getString("ssh-alias", null, name));
     }
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 e3455e3..9616aec 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
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.sshd;
 
-import com.google.common.collect.Maps;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
@@ -32,6 +31,7 @@
 import com.google.inject.Scope;
 import com.google.inject.util.Providers;
 
+import java.util.HashMap;
 import java.util.Map;
 
 /** Guice scopes for state during an SSH connection. */
@@ -44,7 +44,7 @@
 
   class Context implements RequestContext {
     private final RequestCleanup cleanup = new RequestCleanup();
-    private final Map<Key<?>, Object> map = Maps.newHashMap();
+    private final Map<Key<?>, Object> map = new HashMap<>();
     private final SchemaFactory<ReviewDb> schemaFactory;
     private final SshSession session;
     private final String commandLine;
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 89b8014..4308db9 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
@@ -25,7 +25,6 @@
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.sshd.SshScope.Context;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 
 import org.apache.sshd.server.Command;
 import org.apache.sshd.server.Environment;
@@ -50,8 +49,8 @@
   private final DispatchCommandProvider dispatcher;
 
   private boolean enableRunAs;
-  private Provider<CurrentUser> caller;
-  private Provider<SshSession> session;
+  private CurrentUser caller;
+  private SshSession session;
   private IdentifiedUser.GenericFactory userFactory;
   private SshScope.Context callingContext;
 
@@ -69,7 +68,8 @@
   @Inject
   SuExec(final SshScope sshScope,
       @CommandName(Commands.ROOT) final DispatchCommandProvider dispatcher,
-      final Provider<CurrentUser> caller, final Provider<SshSession> session,
+      final CurrentUser caller,
+      final SshSession session,
       final IdentifiedUser.GenericFactory userFactory,
       final SshScope.Context callingContext,
       AuthConfig config) {
@@ -112,12 +112,12 @@
   }
 
   private void checkCanRunAs() throws UnloggedFailure {
-    if (caller.get() instanceof PeerDaemonUser) {
+    if (caller instanceof PeerDaemonUser) {
       // OK.
     } else if (!enableRunAs) {
       throw new UnloggedFailure(1,
           "fatal: suexec disabled by auth.enableRunAs = false");
-    } else if (!caller.get().getCapabilities().canRunAs()) {
+    } else if (!caller.getCapabilities().canRunAs()) {
       throw new UnloggedFailure(1, "fatal: suexec not permitted");
     }
   }
@@ -125,16 +125,15 @@
   private SshSession newSession() {
     final SocketAddress peer;
     if (peerAddress == null) {
-      peer = session.get().getRemoteAddress();
+      peer = session.getRemoteAddress();
     } else {
       peer = peerAddress;
     }
-    CurrentUser self = caller.get();
-    if (self instanceof PeerDaemonUser) {
-      self = null;
+    if (caller instanceof PeerDaemonUser) {
+      caller = null;
     }
-    return new SshSession(session.get(), peer,
-        userFactory.runAs(peer, accountId, self));
+    return new SshSession(session, peer,
+        userFactory.runAs(peer, accountId, caller));
   }
 
   private static String join(List<String> args) {
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 2fb91f6..b4594d4 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
@@ -17,7 +17,6 @@
 import com.google.common.base.Function;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
-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;
@@ -33,7 +32,6 @@
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -79,9 +77,9 @@
   private AllProjectsName allProjectsName;
 
   @Inject
-  private Provider<ListChildProjects> listChildProjects;
+  private ListChildProjects listChildProjects;
 
-  private Project.NameKey newParentKey = null;
+  private Project.NameKey newParentKey;
 
   @Override
   protected void run() throws Failure {
@@ -116,7 +114,7 @@
       }
     }
 
-    final List<Project.NameKey> childProjects = Lists.newArrayList();
+    final List<Project.NameKey> childProjects = new ArrayList<>();
     for (final ProjectControl pc : children) {
       childProjects.add(pc.getProject().getNameKey());
     }
@@ -176,7 +174,7 @@
    * that were specified to be excluded from reparenting.
    */
   private List<Project.NameKey> getChildrenForReparenting(final ProjectControl parent) {
-    final List<Project.NameKey> childProjects = Lists.newArrayList();
+    final List<Project.NameKey> childProjects = new ArrayList<>();
     final List<Project.NameKey> excluded =
         new ArrayList<>(excludedChildren.size());
     for (final ProjectControl excludedChild : excludedChildren) {
@@ -187,7 +185,7 @@
     if (newParentKey != null) {
       automaticallyExcluded.addAll(getAllParents(newParentKey));
     }
-    for (final ProjectInfo child : listChildProjects.get().apply(
+    for (final ProjectInfo child : listChildProjects.apply(
         new ProjectResource(parent))) {
       final Project.NameKey childName = new Project.NameKey(child.name);
       if (!excluded.contains(childName)) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ApproveOption.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ApproveOption.java
index 59892a3..fb961d8 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ApproveOption.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ApproveOption.java
@@ -123,6 +123,7 @@
   public static class Handler extends OneArgumentOptionHandler<Short> {
     private final ApproveOption cmdOption;
 
+    // CS IGNORE RedundantModifier FOR NEXT 1 LINES. REASON: needed by org.kohsuke.args4j.Option
     public Handler(final CmdLineParser parser, final OptionDef option,
         final Setter<Short> setter) {
       super(parser, option, setter);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AproposCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AproposCommand.java
index 0aa12c4..cc393ce 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AproposCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AproposCommand.java
@@ -36,7 +36,7 @@
   @Inject
   @CanonicalWebUrl String url;
 
-  @Argument(index=0, required = true, metaVar = "QUERY")
+  @Argument(index = 0, required = true, metaVar = "QUERY")
   private String q;
 
   @Override
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 acbc50e..a84fe04 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
@@ -29,6 +29,7 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
 
@@ -64,7 +65,8 @@
   private CreateAccount.Factory createAccountFactory;
 
   @Override
-  protected void run() throws OrmException, IOException, UnloggedFailure {
+  protected void run() throws OrmException, IOException, ConfigInvalidException,
+      UnloggedFailure {
     CreateAccount.Input input = new CreateAccount.Input();
     input.username = username;
     input.email = email;
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 2bcd4cf..fcf365c 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
@@ -29,7 +29,6 @@
 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;
 
@@ -51,7 +50,7 @@
   private boolean list;
 
   @Inject
-  private Provider<ListCaches> listCaches;
+  private ListCaches listCaches;
 
   @Inject
   private PostCaches postCaches;
@@ -94,7 +93,7 @@
 
   @SuppressWarnings("unchecked")
   private void doList() {
-    for (String name : (List<String>) listCaches.get()
+    for (String name : (List<String>) listCaches
         .setFormat(OutputFormat.LIST).apply(new ConfigResource())) {
       stderr.print(name);
       stderr.print('\n');
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexActivateCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexActivateCommand.java
index dc67ac3..c508b1d 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexActivateCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexActivateCommand.java
@@ -24,19 +24,25 @@
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
+import org.kohsuke.args4j.Argument;
+
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
 @CommandMetaData(name = "activate",
   description = "Activate the latest index version available",
   runsAt = MASTER)
 public class IndexActivateCommand extends SshCommand {
 
+  @Argument(index = 0, required = true, metaVar = "INDEX",
+      usage = "index name to activate")
+  private String name;
+
   @Inject
   private LuceneVersionManager luceneVersionManager;
 
   @Override
   protected void run() throws UnloggedFailure {
     try {
-      if (luceneVersionManager.activateLatestIndex()) {
+      if (luceneVersionManager.activateLatestIndex(name)) {
         stdout.println("Activated latest index version");
       } else {
         stdout.println("Not activating index, already using latest version");
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexStartCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexStartCommand.java
index 1b3b819..c2c565f 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexStartCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexStartCommand.java
@@ -24,18 +24,24 @@
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
+import org.kohsuke.args4j.Argument;
+
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
 @CommandMetaData(name = "start", description = "Start the online reindexer",
   runsAt = MASTER)
 public class IndexStartCommand extends SshCommand {
 
+  @Argument(index = 0, required = true, metaVar = "INDEX",
+      usage = "index name to start")
+  private String name;
+
   @Inject
   private LuceneVersionManager luceneVersionManager;
 
   @Override
   protected void run() throws UnloggedFailure {
     try {
-      if (luceneVersionManager.startReindexer()) {
+      if (luceneVersionManager.startReindexer(name)) {
         stdout.println("Reindexer started");
       } else {
         stdout.println("Nothing to reindex, index is already the latest version");
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
index 75072e8..bd97286 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
@@ -72,7 +72,7 @@
         final GroupControl.GenericFactory genericGroupControlFactory,
         final Provider<IdentifiedUser> identifiedUser,
         final IdentifiedUser.GenericFactory userFactory,
-        final Provider<GetGroups> accountGetGroups,
+        final GetGroups accountGetGroups,
         final GroupJson json,
         GroupBackend groupBackend) {
       super(groupCache, groupControlFactory, genericGroupControlFactory,
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 0529b90..4c930c8 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
@@ -51,7 +51,7 @@
     QueryShell create(@Assisted InputStream in, @Assisted OutputStream out);
   }
 
-  public static enum OutputFormat {
+  public enum OutputFormat {
     PRETTY, JSON, JSON_SINGLE
   }
 
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 00cf53f..dad8672 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
@@ -17,7 +17,6 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.base.Strings;
-import com.google.common.collect.Maps;
 import com.google.common.io.CharStreams;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelValue;
@@ -42,7 +41,6 @@
 import com.google.gson.JsonSyntaxException;
 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;
@@ -52,10 +50,12 @@
 import java.io.IOException;
 import java.io.InputStreamReader;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.TreeMap;
 
 @CommandMetaData(name = "review", description = "Apply reviews to one or more patch sets")
 public class ReviewCommand extends SshCommand {
@@ -124,6 +124,9 @@
       + "specified can be applied to the given patch set(s)")
   private boolean strictLabels;
 
+  @Option(name = "--tag", aliases = "-t", usage = "applies a tag to the given review", metaVar = "TAG")
+  private String changeTag;
+
   @Option(name = "--label", aliases = "-l", usage = "custom label(s) to assign", metaVar = "LABEL=VALUE")
   void addLabel(final String token) {
     LabelVote v = LabelVote.parseWithEquals(token);
@@ -138,7 +141,7 @@
   private AllProjectsName allProjects;
 
   @Inject
-  private Provider<GerritApi> gApi;
+  private GerritApi gApi;
 
   @Inject
   private PatchSetParser psParser;
@@ -198,6 +201,9 @@
       if (rebaseChange) {
         throw error("json and rebase actions are mutually exclusive");
       }
+      if (changeTag != null) {
+        throw error("json and tag actions are mutually exclusive");
+      }
     }
     if (rebaseChange) {
       if (deleteDraftPatchSet) {
@@ -245,7 +251,7 @@
 
   private void applyReview(PatchSet patchSet,
       final ReviewInput review) throws RestApiException {
-    gApi.get().changes()
+    gApi.changes()
         .id(patchSet.getId().getParentKey().get())
         .revision(patchSet.getRevision().get())
         .review(review);
@@ -268,8 +274,9 @@
 
     ReviewInput review = new ReviewInput();
     review.message = Strings.emptyToNull(changeComment);
+    review.tag = Strings.emptyToNull(changeTag);
     review.notify = notify;
-    review.labels = Maps.newTreeMap();
+    review.labels = new TreeMap<>();
     review.drafts = ReviewInput.DraftHandling.PUBLISH;
     review.strictLabels = strictLabels;
     for (ApproveOption ao : optionList) {
@@ -300,7 +307,7 @@
         applyReview(patchSet, review);
       }
 
-      if (rebaseChange){
+      if (rebaseChange) {
         revisionApi(patchSet).rebase();
       }
 
@@ -319,7 +326,7 @@
   }
 
   private ChangeApi changeApi(PatchSet patchSet) throws RestApiException {
-    return gApi.get().changes().id(patchSet.getId().getParentKey().get());
+    return gApi.changes().id(patchSet.getId().getParentKey().get());
   }
 
   private RevisionApi revisionApi(PatchSet patchSet) throws RestApiException {
@@ -329,7 +336,7 @@
   @Override
   protected void parseCommandLine() throws UnloggedFailure {
     optionList = new ArrayList<>();
-    customLabels = Maps.newHashMap();
+    customLabels = new HashMap<>();
 
     ProjectControl allProjectsControl;
     try {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ScpCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ScpCommand.java
index 194e65f9..5fc877c 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ScpCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ScpCommand.java
@@ -198,7 +198,7 @@
   private void header(final Entry dir, final int len) throws IOException,
       UnsupportedEncodingException {
     final StringBuilder buf = new StringBuilder();
-    switch(dir.getType()){
+    switch (dir.getType()) {
       case DIR:
         buf.append(TYPE_DIR);
         break;
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 6963df1..6e03ac1 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
@@ -47,6 +47,8 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
 
@@ -167,7 +169,8 @@
     }
   }
 
-  private void setAccount() throws OrmException, IOException, UnloggedFailure {
+  private void setAccount() throws OrmException, IOException, UnloggedFailure,
+      ConfigInvalidException {
     user = genericUserFactory.create(id);
     rsrc = new AccountResource(user);
     try {
@@ -220,7 +223,7 @@
   }
 
   private void addSshKeys(List<String> sshKeys) throws RestApiException,
-      OrmException, IOException {
+      OrmException, IOException, ConfigInvalidException {
     for (final String sshKey : sshKeys) {
       AddSshKey.Input in = new AddSshKey.Input();
       in.raw = RawInputUtil.create(sshKey.getBytes(), "plain/text");
@@ -228,8 +231,9 @@
     }
   }
 
-  private void deleteSshKeys(List<String> sshKeys) throws RestApiException,
-      OrmException {
+  private void deleteSshKeys(List<String> sshKeys)
+      throws RestApiException, OrmException, RepositoryNotFoundException,
+      IOException, ConfigInvalidException {
     List<SshKeyInfo> infos = getSshKeys.apply(rsrc);
     if (sshKeys.contains("ALL")) {
       for (SshKeyInfo i : infos) {
@@ -247,7 +251,8 @@
     }
   }
 
-  private void deleteSshKey(SshKeyInfo i) throws AuthException, OrmException {
+  private void deleteSshKey(SshKeyInfo i) throws AuthException, OrmException,
+      RepositoryNotFoundException, IOException, ConfigInvalidException {
     AccountSshKey sshKey = new AccountSshKey(
         new AccountSshKey.Id(user.getAccountId(), i.seq), i.sshPublicKey);
     deleteSshKey.apply(
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetLoggingLevelCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetLoggingLevelCommand.java
index 49edf14..6277eb4 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetLoggingLevelCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetLoggingLevelCommand.java
@@ -40,7 +40,7 @@
   private static final String LOG_CONFIGURATION = "log4j.properties";
   private static final String JAVA_OPTIONS_LOG_CONFIG = "log4j.configuration";
 
-  private static enum LevelOption {
+  private enum LevelOption {
     ALL,
     TRACE,
     DEBUG,
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetMembersCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetMembersCommand.java
index 923865a..a7e01c5 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetMembersCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetMembersCommand.java
@@ -34,44 +34,44 @@
 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.Argument;
 import org.kohsuke.args4j.Option;
 
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
 import java.util.List;
 
 @CommandMetaData(name = "set-members", description = "Modify members of specific group or number of groups")
 public class SetMembersCommand extends SshCommand {
 
   @Option(name = "--add", aliases = {"-a"}, metaVar = "USER", usage = "users that should be added as group member")
-  private List<Account.Id> accountsToAdd = Lists.newArrayList();
+  private List<Account.Id> accountsToAdd = new ArrayList<>();
 
   @Option(name = "--remove", aliases = {"-r"}, metaVar = "USER", usage = "users that should be removed from the group")
-  private List<Account.Id> accountsToRemove = Lists.newArrayList();
+  private List<Account.Id> accountsToRemove = new ArrayList<>();
 
   @Option(name = "--include", aliases = {"-i"}, metaVar = "GROUP", usage = "group that should be included as group member")
-  private List<AccountGroup.UUID> groupsToInclude = Lists.newArrayList();
+  private List<AccountGroup.UUID> groupsToInclude = new ArrayList<>();
 
   @Option(name = "--exclude", aliases = {"-e"}, metaVar = "GROUP", usage = "group that should be excluded from the group")
-  private List<AccountGroup.UUID> groupsToRemove = Lists.newArrayList();
+  private List<AccountGroup.UUID> groupsToRemove = new ArrayList<>();
 
   @Argument(index = 0, required = true, multiValued = true, metaVar = "GROUP", usage = "groups to modify")
-  private List<AccountGroup.UUID> groups = Lists.newArrayList();
+  private List<AccountGroup.UUID> groups = new ArrayList<>();
 
   @Inject
-  private Provider<AddMembers> addMembers;
+  private AddMembers addMembers;
 
   @Inject
-  private Provider<DeleteMembers> deleteMembers;
+  private DeleteMembers deleteMembers;
 
   @Inject
-  private Provider<AddIncludedGroups> addIncludedGroups;
+  private AddIncludedGroups addIncludedGroups;
 
   @Inject
-  private Provider<DeleteIncludedGroups> deleteIncludedGroups;
+  private DeleteIncludedGroups deleteIncludedGroups;
 
   @Inject
   private GroupsCollection groupsCollection;
@@ -89,19 +89,19 @@
           groupsCollection.parse(TopLevelResource.INSTANCE,
               IdString.fromUrl(groupUuid.get()));
       if (!accountsToRemove.isEmpty()) {
-        deleteMembers.get().apply(resource, fromMembers(accountsToRemove));
+        deleteMembers.apply(resource, fromMembers(accountsToRemove));
         reportMembersAction("removed from", resource, accountsToRemove);
       }
       if (!groupsToRemove.isEmpty()) {
-        deleteIncludedGroups.get().apply(resource, fromGroups(groupsToRemove));
+        deleteIncludedGroups.apply(resource, fromGroups(groupsToRemove));
         reportGroupsAction("excluded from", resource, groupsToRemove);
       }
       if (!accountsToAdd.isEmpty()) {
-        addMembers.get().apply(resource, fromMembers(accountsToAdd));
+        addMembers.apply(resource, fromMembers(accountsToAdd));
         reportMembersAction("added to", resource, accountsToAdd);
       }
       if (!groupsToInclude.isEmpty()) {
-        addIncludedGroups.get().apply(resource, fromGroups(groupsToInclude));
+        addIncludedGroups.apply(resource, fromGroups(groupsToInclude));
         reportGroupsAction("included to", resource, groupsToInclude);
       }
     }
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 0718dc6..63cb54f 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
@@ -33,7 +33,6 @@
 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;
@@ -82,13 +81,13 @@
   private ReviewerResource.Factory reviewerFactory;
 
   @Inject
-  private Provider<PostReviewers> postReviewersProvider;
+  private PostReviewers postReviewers;
 
   @Inject
-  private Provider<DeleteReviewer> deleteReviewerProvider;
+  private DeleteReviewer deleteReviewer;
 
   @Inject
-  private Provider<CurrentUser> userProvider;
+  private CurrentUser currentUser;
 
   @Inject
   private ChangesCollection changesCollection;
@@ -123,12 +122,11 @@
 
     // Remove reviewers
     //
-    DeleteReviewer delete = deleteReviewerProvider.get();
     for (Account.Id reviewer : toRemove) {
       ReviewerResource rsrc = reviewerFactory.create(changeRsrc, reviewer);
       String error = null;
       try {
-        delete.apply(rsrc, new DeleteReviewer.Input());
+        deleteReviewer.apply(rsrc, new DeleteReviewer.Input());
       } catch (ResourceNotFoundException e) {
         error = String.format("could not remove %s: not found", reviewer);
       } catch (Exception e) {
@@ -143,14 +141,13 @@
 
     // Add reviewers
     //
-    PostReviewers post = postReviewersProvider.get();
     for (String reviewer : toAdd) {
       AddReviewerInput input = new AddReviewerInput();
       input.reviewer = reviewer;
       input.confirmed = true;
       String error;
       try {
-        error = post.apply(changeRsrc, input).error;
+        error = postReviewers.apply(changeRsrc, input).error;
       } catch (Exception e) {
         error = String.format("could not add %s: %s", reviewer, e.getMessage());
       }
@@ -164,8 +161,7 @@
   }
 
   private void addChangeImpl(String id) throws UnloggedFailure, OrmException {
-    List<ChangeControl> matched =
-        changeFinder.find(id, userProvider.get());
+    List<ChangeControl> matched = changeFinder.find(id, currentUser);
     List<ChangeControl> toAdd = new ArrayList<>(changes.size());
     for (ChangeControl ctl : matched) {
       if (!changes.containsKey(ctl.getId()) && inProject(ctl.getProject())
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 b5801bf..3e0cec3 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
@@ -38,7 +38,6 @@
 import com.google.gerrit.sshd.SshCommand;
 import com.google.gerrit.sshd.SshDaemon;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 
 import org.apache.sshd.common.io.IoAcceptor;
 import org.apache.sshd.common.io.IoSession;
@@ -84,13 +83,13 @@
   private SshDaemon daemon;
 
   @Inject
-  private Provider<ListCaches> listCaches;
+  private ListCaches listCaches;
 
   @Inject
-  private Provider<GetSummary> getSummary;
+  private GetSummary getSummary;
 
   @Inject
-  private Provider<CurrentUser> self;
+  private CurrentUser self;
 
   @Option(name = "--width", aliases = {"-w"}, metaVar = "COLS", usage = "width of output table")
   private int columns = 80;
@@ -125,7 +124,7 @@
     stdout.print('\n');
 
     stdout.print(String.format(//
-        "%1s %-"+nw+"s|%-21s|  %-5s |%-9s|\n" //
+        "%1s %-" + nw + "s|%-21s|  %-5s |%-9s|\n" //
         , "" //
         , "Name" //
         , "Entries" //
@@ -133,7 +132,7 @@
         , "Hit Ratio" //
     ));
     stdout.print(String.format(//
-        "%1s %-"+nw+"s|%6s %6s %7s|  %-5s  |%-4s %-4s|\n" //
+        "%1s %-" + nw + "s|%6s %6s %7s|  %-5s  |%-4s %-4s|\n" //
         , "" //
         , "" //
         , "Mem" //
@@ -155,11 +154,11 @@
     printDiskCaches(caches);
     stdout.print('\n');
 
-    if (self.get().getCapabilities().canMaintainServer()) {
+    if (self.getCapabilities().canMaintainServer()) {
       sshSummary();
 
       SummaryInfo summary =
-          getSummary.get().setGc(gc).setJvm(showJVM).apply(new ConfigResource());
+          getSummary.setGc(gc).setJvm(showJVM).apply(new ConfigResource());
       taskSummary(summary.taskSummary);
       memSummary(summary.memSummary);
       threadSummary(summary.threadSummary);
@@ -175,7 +174,7 @@
   private Collection<CacheInfo> getCaches() {
     @SuppressWarnings("unchecked")
     Map<String, CacheInfo> caches =
-        (Map<String, CacheInfo>) listCaches.get().apply(new ConfigResource());
+        (Map<String, CacheInfo>) listCaches.apply(new ConfigResource());
     for (Map.Entry<String, CacheInfo> entry : caches.entrySet()) {
       CacheInfo cache = entry.getValue();
       cache.name = entry.getKey();
@@ -209,7 +208,7 @@
 
   private void printCache(CacheInfo cache) {
     stdout.print(String.format(
-        "%1s %-"+nw+"s|%6s %6s %7s| %7s |%4s %4s|\n",
+        "%1s %-" + nw + "s|%6s %6s %7s| %7s |%4s %4s|\n",
         CacheType.DISK.equals(cache.type) ? "D" : "",
         cache.name,
         nullToEmpty(cache.entries.mem),
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 5971b8d..34f281e 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
@@ -52,7 +52,7 @@
   private IdentifiedUser currentUser;
 
   private int columns = 80;
-  private int taskNameWidth;
+  private int maxCommandWidth;
 
   @Override
   public void start(Environment env) throws IOException {
@@ -69,7 +69,7 @@
 
   @Override
   protected void run() throws UnloggedFailure {
-    taskNameWidth = wide ? Integer.MAX_VALUE : columns - 8 - 12 - 12 - 4 - 4;
+    maxCommandWidth = wide ? Integer.MAX_VALUE : columns - 8 - 12 - 12 - 4 - 4;
     stdout.print(String.format("%-8s %-12s %-12s %-4s %s\n", //
         "Task", "State", "StartTime", "", "Command"));
     stdout.print("----------------------------------------------"
@@ -97,9 +97,9 @@
 
         // Shows information about tasks depending on the user rights
         if (viewAll || task.projectName == null) {
-          String command = task.command.length() < taskNameWidth
+          String command = task.command.length() < maxCommandWidth
               ? task.command
-              : task.command.substring(0, taskNameWidth);
+              : task.command.substring(0, maxCommandWidth);
 
           stdout.print(String.format("%8s %-12s %-12s %-4s %s\n",
               task.id, start, startTime(task.startTime), "", command));
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 29b7987..2da1b6d 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
@@ -81,8 +81,8 @@
 
   /** Special event to notify clients they missed other events. */
   private static final class DroppedOutputEvent extends Event {
-    private final static String TYPE = "dropped-output";
-    public DroppedOutputEvent() {
+    private static final String TYPE = "dropped-output";
+    DroppedOutputEvent() {
       super(TYPE);
     }
   }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitRuleCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitRuleCommand.java
index 9c4a680..7e74ef0 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitRuleCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitRuleCommand.java
@@ -20,16 +20,15 @@
 import com.google.gerrit.server.change.TestSubmitRule;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 
 /** Command that allows testing of prolog submit-rules in a live instance. */
 @CommandMetaData(name = "rule", description = "Test prolog submit rules")
 final class TestSubmitRuleCommand extends BaseTestPrologCommand {
   @Inject
-  private Provider<TestSubmitRule> view;
+  private TestSubmitRule view;
 
   @Override
   protected RestModifyView<RevisionResource, TestSubmitRuleInput> createView() {
-    return view.get();
+    return view;
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitTypeCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitTypeCommand.java
index 34d4bdc..3a885f9 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitTypeCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitTypeCommand.java
@@ -21,15 +21,14 @@
 import com.google.gerrit.server.change.TestSubmitType;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 
 @CommandMetaData(name = "type", description = "Test prolog submit type")
 final class TestSubmitTypeCommand extends BaseTestPrologCommand {
   @Inject
-  private Provider<TestSubmitType> view;
+  private TestSubmitType view;
 
   @Override
   protected RestModifyView<RevisionResource, TestSubmitRuleInput> createView() {
-    return view.get();
+    return view;
   }
 }
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 d278f4b..b420a5f 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
@@ -27,7 +27,6 @@
 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;
@@ -39,7 +38,7 @@
 /** Publishes Git repositories over SSH using the Git upload-pack protocol. */
 final class Upload extends AbstractGitCommand {
   @Inject
-  private Provider<ReviewDb> db;
+  private ReviewDb db;
 
   @Inject
   private TransferConfig config;
@@ -69,10 +68,8 @@
     }
 
     final UploadPack up = new UploadPack(repo);
-    if (!projectControl.allRefsAreVisible()) {
-      up.setAdvertiseRefsHook(new VisibleRefFilter(tagCache, changeCache, repo,
-          projectControl, db.get(), true));
-    }
+    up.setAdvertiseRefsHook(new VisibleRefFilter(tagCache, changeCache, repo,
+        projectControl, db, true));
     up.setPackConfig(config.getPackConfig());
     up.setTimeout(config.getTimeout());
     up.setPostUploadHook(uploadMetrics);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/UploadArchive.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/UploadArchive.java
index c3dae3a..5e2480e 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/UploadArchive.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/UploadArchive.java
@@ -17,13 +17,11 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Lists;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.change.ArchiveFormat;
 import com.google.gerrit.server.change.GetArchive;
 import com.google.gerrit.sshd.AbstractGitCommand;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 
 import org.eclipse.jgit.api.ArchiveCommand;
 import org.eclipse.jgit.api.errors.GitAPIException;
@@ -39,6 +37,7 @@
 import org.kohsuke.args4j.Option;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
@@ -104,7 +103,7 @@
   @Inject
   private GetArchive.AllowedFormats allowedFormats;
   @Inject
-  private Provider<ReviewDb> db;
+  private ReviewDb db;
   private Options options = new Options();
 
   /**
@@ -114,7 +113,7 @@
    */
   protected void readArguments() throws IOException, Failure {
     String argCmd = "argument ";
-    List<String> args = Lists.newArrayList();
+    List<String> args = new ArrayList<>();
 
     // Read arguments in Pkt-Line format
     PacketLineIn packetIn = new PacketLineIn(in);
@@ -127,7 +126,7 @@
         throw new Failure(1, "fatal: 'argument' token or flush expected");
       }
       String[] parts = s.substring(argCmd.length()).split("=", 2);
-      for(String p : parts) {
+      for (String p : parts) {
         args.add(p);
       }
     }
@@ -218,7 +217,7 @@
   private boolean canRead(ObjectId revId) throws IOException {
     try (RevWalk rw = new RevWalk(repo)) {
       RevCommit commit = rw.parseCommit(revId);
-      return projectControl.canReadCommit(db.get(), rw, commit);
+      return projectControl.canReadCommit(db, rw, commit);
     }
   }
 }
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 e70ab72..b5888b50 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
@@ -37,7 +37,6 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.LinkedHashMultimap;
 import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
 import com.google.common.collect.Multimap;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -59,6 +58,7 @@
 import java.lang.annotation.Annotation;
 import java.lang.reflect.AnnotatedElement;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.ResourceBundle;
@@ -282,7 +282,7 @@
 
   @SuppressWarnings("rawtypes")
   private static Map<String, OptionHandler> index(List<OptionHandler> in) {
-    Map<String, OptionHandler> m = Maps.newHashMap();
+    Map<String, OptionHandler> m = new HashMap<>();
     for (OptionHandler handler : in) {
       if (handler.option instanceof NamedOptionDef) {
         NamedOptionDef def = (NamedOptionDef) handler.option;
@@ -352,7 +352,7 @@
     private void ensureOptionsInitialized() {
       if (optionsList == null) {
         help = new HelpOption();
-        optionsList = Lists.newArrayList();
+        optionsList = new ArrayList<>();
         addOption(help, help);
       }
     }
diff --git a/gerrit-util-http/BUCK b/gerrit-util-http/BUCK
index 3294851..cfab096 100644
--- a/gerrit-util-http/BUCK
+++ b/gerrit-util-http/BUCK
@@ -15,7 +15,7 @@
     '//lib:guava',
     '//lib:servlet-api-3_1',
     '//lib/httpcomponents:httpclient',
-    '@jgit//org.eclipse.jgit:jgit',
+    '//lib/jgit/org.eclipse.jgit:jgit',
   ],
   visibility = ['PUBLIC'],
 )
diff --git a/gerrit-war/BUCK b/gerrit-war/BUCK
index b1f0487..6d74a83 100644
--- a/gerrit-war/BUCK
+++ b/gerrit-war/BUCK
@@ -23,8 +23,8 @@
     '//lib:gwtorm',
     '//lib/guice:guice',
     '//lib/guice:guice-servlet',
+    '//lib/jgit/org.eclipse.jgit:jgit',
     '//lib/log:api',
-    '@jgit//org.eclipse.jgit:jgit',
   ],
   provided_deps = ['//lib:servlet-api-3_1'],
   visibility = [
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/UnzippedDistribution.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/UnzippedDistribution.java
index 665d420..d35f31d 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/UnzippedDistribution.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/UnzippedDistribution.java
@@ -17,7 +17,6 @@
 import static com.google.gerrit.pgm.init.InitPlugins.JAR;
 import static com.google.gerrit.pgm.init.InitPlugins.PLUGIN_DIR;
 
-import com.google.common.collect.Lists;
 import com.google.gerrit.pgm.init.PluginsDistribution;
 import com.google.inject.Singleton;
 
@@ -26,6 +25,7 @@
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
+import java.util.ArrayList;
 import java.util.List;
 
 import javax.servlet.ServletContext;
@@ -36,7 +36,7 @@
   private ServletContext servletContext;
   private File pluginsDir;
 
-  public UnzippedDistribution(ServletContext servletContext) {
+  UnzippedDistribution(ServletContext servletContext) {
     this.servletContext = servletContext;
   }
 
@@ -57,7 +57,7 @@
 
   @Override
   public List<String> listPluginNames() throws FileNotFoundException {
-    List<String> names = Lists.newArrayList();
+    List<String> names = new ArrayList<>();
     String[] list = getPluginsDir().list();
     if (list != null) {
       for (String pluginJarName : list) {
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 d11ebed..8da5168 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
@@ -366,7 +366,6 @@
     modules.add(RequestMetricsFilter.module());
     modules.add(sysInjector.getInstance(GitOverHttpModule.class));
     modules.add(sysInjector.getInstance(WebModule.class));
-    modules.add(sysInjector.getInstance(StaticModule.class));
     modules.add(sysInjector.getInstance(RequireSslFilter.Module.class));
     if (sshInjector != null) {
       modules.add(sshInjector.getInstance(WebSshGlueModule.class));
@@ -384,6 +383,9 @@
     }
     modules.add(sysInjector.getInstance(GetUserFilter.Module.class));
 
+    // StaticModule contains a "/*" wildcard, place it last.
+    modules.add(sysInjector.getInstance(StaticModule.class));
+
     return sysInjector.createChildInjector(modules);
   }
 
diff --git a/lib/BUCK b/lib/BUCK
index 98fdf2c..65bdd5e 100644
--- a/lib/BUCK
+++ b/lib/BUCK
@@ -261,3 +261,11 @@
   sha1 = 'a9cbcdfb7e9f86fbc74d3afae65f2248bfbf82a0',
   license = 'DO_NOT_DISTRIBUTE',
 )
+
+maven_jar(
+  name = 'blame-cache',
+  id = 'com/google/gitiles:blame-cache:0.1-9',
+  sha1 = '51d35e6f8bbc2412265066cea9653dd758c95826',
+  license = 'Apache2.0',
+  repository = GERRIT,
+)
diff --git a/lib/JGIT_VERSION b/lib/JGIT_VERSION
index 16db0c6..6878780 100644
--- a/lib/JGIT_VERSION
+++ b/lib/JGIT_VERSION
@@ -1,4 +1,4 @@
 include_defs('//lib/maven.defs')
 
 REPO = GERRIT # Leave here even if set to MAVEN_CENTRAL.
-VERS = '4.2.0.201601211800-r.136-g8efdaaf'
+VERS = '4.3.0.201604071810-r.23-gc9b0028'
diff --git a/lib/asciidoctor/BUCK b/lib/asciidoctor/BUCK
index 98ad93c..4cf692e 100644
--- a/lib/asciidoctor/BUCK
+++ b/lib/asciidoctor/BUCK
@@ -16,6 +16,7 @@
     '//lib:args4j',
     '//lib:guava',
     '//lib/log:api',
+    '//lib/log:nop',
   ],
   visibility = ['//tools/eclipse:classpath'],
 )
diff --git a/lib/asciidoctor/java/AsciiDoctor.java b/lib/asciidoctor/java/AsciiDoctor.java
index dce939d..8e18feb1 100644
--- a/lib/asciidoctor/java/AsciiDoctor.java
+++ b/lib/asciidoctor/java/AsciiDoctor.java
@@ -28,6 +28,7 @@
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
+import java.io.FilenameFilter;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -104,7 +105,7 @@
 
     for (String attribute : attributes) {
       int equalsIndex = attribute.indexOf('=');
-      if(equalsIndex > -1) {
+      if (equalsIndex > -1) {
         String name = attribute.substring(0, equalsIndex);
         String value = attribute.substring(equalsIndex + 1, attribute.length());
 
@@ -148,6 +149,16 @@
         renderInput(options, new File(inputFile));
         zipFile(out, outName, zip);
       }
+
+      File[] cssFiles = tmpdir.listFiles(new FilenameFilter() {
+        @Override
+        public boolean accept(File dir, String name) {
+          return name.endsWith(".css");
+        }
+      });
+      for (File css : cssFiles) {
+        zipFile(css, css.getName(), zip);
+      }
     }
   }
 
diff --git a/lib/codemirror/BUCK b/lib/codemirror/BUCK
index 30d29ae..9b8a146 100644
--- a/lib/codemirror/BUCK
+++ b/lib/codemirror/BUCK
@@ -1,14 +1,14 @@
 include_defs('//lib/maven.defs')
 include_defs('//lib/codemirror/cm.defs')
 
-VERSION = '5.13.4'
+VERSION = '5.14.2'
 TOP = 'META-INF/resources/webjars/codemirror/%s' % VERSION
 TOP_MINIFIED = 'META-INF/resources/webjars/codemirror-minified/%s' % VERSION
 
 maven_jar(
   name = 'codemirror-minified',
   id = 'org.webjars.npm:codemirror-minified:' + VERSION,
-  sha1 = 'fb207d777aad82423d098c3660bd2ca72775bf6e',
+  sha1 = 'c69056d2a0e07432326e67ea8fe2abb91a065030',
   attach_source = False,
   license = 'codemirror-minified',
   visibility = [],
@@ -17,12 +17,24 @@
 maven_jar(
   name = 'codemirror-original',
   id = 'org.webjars.npm:codemirror:' + VERSION,
-  sha1 = '4130b0a7e998d304277a86062351cb9e2840cbfa',
+  sha1 = '1ed9697531be85c85edb70fcdf58f10045563f7b',
   attach_source = False,
   license = 'codemirror-original',
   visibility = [],
 )
 
+DIFF_MATCH_PATCH_VERSION = '20121119-1'
+DIFF_MATCH_PATCH_TOP = ('META-INF/resources/webjars/google-diff-match-patch/%s'
+    % DIFF_MATCH_PATCH_VERSION)
+
+maven_jar(
+  name = 'diff-match-patch',
+  id = 'org.webjars:google-diff-match-patch:' + DIFF_MATCH_PATCH_VERSION,
+  sha1 = '0cf1782dbcb8359d95070da9176059a5a9d37709',
+  license = 'Apache2.0',
+  attach_source = False,
+)
+
 for archive, suffix, top in [('codemirror-original', '', TOP), ('codemirror-minified', '_r', TOP_MINIFIED)]:
   # Main JavaScript and addons
   genrule(
@@ -58,11 +70,12 @@
     genrule (
       name = 'mode_%s%s' % (n, suffix),
       cmd = ';'.join([
-        "echo '/** @license' >$OUT",
-        'unzip -p $(location :%s) %s/LICENSE >>$OUT' % (archive, top),
-        "echo '*/' >>$OUT",
-        'unzip -p $(location :%s) %s/mode/%s/%s.js >>$OUT' % (archive, top, n, n),
-        ]),
+          "echo '/** @license' >$OUT",
+          'unzip -p $(location :%s) %s/LICENSE >>$OUT' % (archive, top),
+          "echo '*/' >>$OUT",
+          'unzip -p $(location :%s) %s/mode/%s/%s.js >>$OUT' % (archive, top, n, n),
+        ]
+      ),
       out = 'mode_%s%s.js' % (n, suffix),
     )
 
@@ -80,19 +93,39 @@
       out = 'theme_%s%s.css' % (n, suffix),
     )
 
+  # Merge Addon bundled with diff-match-patch
+  genrule(
+    name = 'addon_merge%s' % suffix,
+    cmd = ';'.join([
+        "echo '/** @license' >$OUT",
+        'unzip -p $(location :%s) %s/LICENSE >>$OUT' % (archive, top),
+        "echo '*/\n' >>$OUT",
+        "echo '// The google-diff-match-patch library is from https://google-diff-match-patch.googlecode.com/svn-history/r106/trunk/javascript/diff_match_patch.js\n' >> $OUT",
+        "echo '/** @license' >>$OUT",
+        'cat $(location //lib:LICENSE-Apache2.0) >>$OUT',
+        "echo '*/' >>$OUT",
+        'unzip -p $(location :diff-match-patch) %s/diff_match_patch.js >>$OUT' % DIFF_MATCH_PATCH_TOP,
+        "echo ';' >> $OUT",
+        'unzip -p $(location :%s) %s/addon/merge/merge.js >>$OUT' % (archive, top)
+      ]
+    ),
+    out = 'addon_merge%s.js' % suffix,
+  )
+
   # Jar packaging
   genrule(
     name = 'jar' + suffix,
     cmd = ';'.join([
       'cd $TMP',
-      'mkdir -p net/codemirror/{lib,mode,theme}',
+      'mkdir -p net/codemirror/{addon,lib,mode,theme}',
       'cp $(location :css%s) net/codemirror/lib/cm.css' % suffix,
       'cp $(location :cm%s) net/codemirror/lib/cm.js' % suffix]
       + ['cp $(location :mode_%s%s) net/codemirror/mode/%s.js' % (n, suffix, n)
          for n in CM_MODES]
       + ['cp $(location :theme_%s%s) net/codemirror/theme/%s.css' % (n, suffix, n)
          for n in CM_THEMES]
-      + ['zip -qr $OUT net/codemirror/{lib,mode,theme}']),
+      + ['cp $(location :addon_merge%s) net/codemirror/addon/merge_bundled.js' % suffix]
+      + ['zip -qr $OUT net/codemirror/{addon,lib,mode,theme}']),
     out = 'codemirror%s.jar' % suffix,
   )
 
diff --git a/lib/codemirror/cm.defs b/lib/codemirror/cm.defs
index edd8dd6..baf2ce5 100644
--- a/lib/codemirror/cm.defs
+++ b/lib/codemirror/cm.defs
@@ -1,15 +1,18 @@
 CM_CSS = [
   'lib/codemirror.css',
   'addon/dialog/dialog.css',
+  'addon/merge/merge.css',
   'addon/scroll/simplescrollbars.css',
   'addon/search/matchesonscrollbar.css',
+  'addon/lint/lint.css',
 ]
 
 CM_JS = [
   'lib/codemirror.js',
   'mode/meta.js',
-  'keymap/vim.js',
   'keymap/emacs.js',
+  'keymap/sublime.js',
+  'keymap/vim.js',
 ]
 
 CM_ADDONS = [
@@ -19,6 +22,7 @@
   'edit/trailingspace.js',
   'scroll/annotatescrollbar.js',
   'scroll/simplescrollbars.js',
+  'search/jump-to-line.js',
   'search/matchesonscrollbar.js',
   'search/searchcursor.js',
   'search/search.js',
@@ -26,18 +30,57 @@
   'mode/multiplex.js',
   'mode/overlay.js',
   'mode/simple.js',
+  'lint/lint.js',
 ]
 
 # Available themes must be enumerated here,
 # in gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/Theme.java,
 # in gerrit-gwtui/src/main/java/net/codemirror/theme/Themes.java
 CM_THEMES = [
+  '3024-day',
+  '3024-night',
+  'abcdef',
+  'ambiance',
+  'base16-dark',
+  'base16-light',
+  'bespin',
+  'blackboard',
+  'cobalt',
+  'colorforth',
+  'dracula',
   'eclipse',
   'elegant',
+  'erlang-dark',
+  'hopscotch',
+  'icecoder',
+  'isotope',
+  'lesser-dark',
+  'liquibyte',
+  'material',
+  'mbo',
+  'mdn-like',
   'midnight',
+  'monokai',
   'neat',
+  'neo',
   'night',
+  'paraiso-dark',
+  'paraiso-light',
+  'pastel-on-dark',
+  'railscasts',
+  'rubyblue',
+  'seti',
+  'solarized',
+  'the-matrix',
+  'tomorrow-night-bright',
+  'tomorrow-night-eighties',
+  'ttcn',
   'twilight',
+  'vibrant-ink',
+  'xq-dark',
+  'xq-light',
+  'yeti',
+  'zenburn',
 ]
 
 # Available modes must be enumerated here,
@@ -98,6 +141,7 @@
   'lua',
   'markdown',
   'mathematica',
+  'mbox',
   'mirc',
   'mllike',
   'modelica',
@@ -113,6 +157,7 @@
   'perl',
   'php',
   'pig',
+  'powershell',
   'properties',
   'protobuf',
   'puppet',
@@ -123,6 +168,7 @@
   'rst',
   'ruby',
   'rust',
+  'sas',
   'sass',
   'scheme',
   'shell',
@@ -155,8 +201,10 @@
   'verilog',
   'vhdl',
   'vue',
+  'webidl',
   'xml',
   'xquery',
+  'yacas',
   'yaml-frontmatter',
   'yaml',
   'z80',
diff --git a/lib/jgit/.buckconfig b/lib/jgit/.buckconfig
deleted file mode 100644
index e67976e..0000000
--- a/lib/jgit/.buckconfig
+++ /dev/null
@@ -1,7 +0,0 @@
-[cache]
-  mode = dir
-  dir = ~/.gerritcodereview/buck-cache/locally-built-artifacts
-
-[repositories]
-  jgit = .
-
diff --git a/lib/jgit/.gitignore b/lib/jgit/.gitignore
deleted file mode 100644
index 0a0ddb2..0000000
--- a/lib/jgit/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/buck-out
diff --git a/lib/jgit/lib/BUCK b/lib/jgit/lib/BUCK
deleted file mode 100644
index fe0bbb1..0000000
--- a/lib/jgit/lib/BUCK
+++ /dev/null
@@ -1,6 +0,0 @@
-include_defs('//lib/maven.defs')
-
-define_license(name = 'Apache2.0')
-define_license(name = 'jgit')
-
-define_license(name = 'DO_NOT_DISTRIBUTE')
diff --git a/lib/jgit/lib/JGIT_VERSION b/lib/jgit/lib/JGIT_VERSION
deleted file mode 120000
index d52b3f9..0000000
--- a/lib/jgit/lib/JGIT_VERSION
+++ /dev/null
@@ -1 +0,0 @@
-../../JGIT_VERSION
\ No newline at end of file
diff --git a/lib/jgit/lib/LICENSE-Apache2.0 b/lib/jgit/lib/LICENSE-Apache2.0
deleted file mode 120000
index de5a79a..0000000
--- a/lib/jgit/lib/LICENSE-Apache2.0
+++ /dev/null
@@ -1 +0,0 @@
-../../LICENSE-Apache2.0
\ No newline at end of file
diff --git a/lib/jgit/lib/LICENSE-DO_NOT_DISTRIBUTE b/lib/jgit/lib/LICENSE-DO_NOT_DISTRIBUTE
deleted file mode 120000
index 6b0859b..0000000
--- a/lib/jgit/lib/LICENSE-DO_NOT_DISTRIBUTE
+++ /dev/null
@@ -1 +0,0 @@
-../../LICENSE-DO_NOT_DISTRIBUTE
\ No newline at end of file
diff --git a/lib/jgit/lib/LICENSE-jgit b/lib/jgit/lib/LICENSE-jgit
deleted file mode 120000
index bfa3da5..0000000
--- a/lib/jgit/lib/LICENSE-jgit
+++ /dev/null
@@ -1 +0,0 @@
-../../LICENSE-jgit
\ No newline at end of file
diff --git a/lib/jgit/lib/maven.defs b/lib/jgit/lib/maven.defs
deleted file mode 120000
index 7f9292f..0000000
--- a/lib/jgit/lib/maven.defs
+++ /dev/null
@@ -1 +0,0 @@
-../../maven.defs
\ No newline at end of file
diff --git a/lib/jgit/org.eclipse.jgit.archive/BUCK b/lib/jgit/org.eclipse.jgit.archive/BUCK
index 49b773c..10ab2b0 100644
--- a/lib/jgit/org.eclipse.jgit.archive/BUCK
+++ b/lib/jgit/org.eclipse.jgit.archive/BUCK
@@ -4,10 +4,10 @@
 maven_jar(
   name = 'jgit-archive',
   id = 'org.eclipse.jgit:org.eclipse.jgit.archive:' + VERS,
-  sha1 = '1b3a0cb7b8c2629e33902b3daf1067accca62eaf',
+  sha1 = 'c612e5bd40ebf6226032cb32c14b396d7ebfe036',
   license = 'jgit',
   repository = REPO,
-  deps = ['@jgit//org.eclipse.jgit:jgit'],
+  deps = ['//lib/jgit/org.eclipse.jgit:jgit'],
   unsign = True,
   exclude = [
     'about.html',
diff --git a/lib/jgit/org.eclipse.jgit.http.server/BUCK b/lib/jgit/org.eclipse.jgit.http.server/BUCK
index 6493dc2..8ebc18df 100644
--- a/lib/jgit/org.eclipse.jgit.http.server/BUCK
+++ b/lib/jgit/org.eclipse.jgit.http.server/BUCK
@@ -4,10 +4,10 @@
 maven_jar(
   name = 'jgit-servlet',
   id = 'org.eclipse.jgit:org.eclipse.jgit.http.server:' + VERS,
-  sha1 = 'b493401b9778cdc38f7c08f111c6385a434b2ac2',
+  sha1 = 'bb01841b74a48abe506c2e44f238e107188e6c8f',
   license = 'jgit',
   repository = REPO,
-  deps = ['@jgit//org.eclipse.jgit:jgit'],
+  deps = ['//lib/jgit/org.eclipse.jgit:jgit'],
   unsign = True,
   exclude = [
     'about.html',
diff --git a/lib/jgit/org.eclipse.jgit.junit/BUCK b/lib/jgit/org.eclipse.jgit.junit/BUCK
index 12eea2c..4b06573 100644
--- a/lib/jgit/org.eclipse.jgit.junit/BUCK
+++ b/lib/jgit/org.eclipse.jgit.junit/BUCK
@@ -4,9 +4,9 @@
 maven_jar(
   name = 'junit',
   id = 'org.eclipse.jgit:org.eclipse.jgit.junit:' + VERS,
-  sha1 = 'c68ddb3e2aaca05b1d2f8250dd107b5f484ed603',
+  sha1 = '62dddedccdcd67b622d0d35a4bfb15c7eab8e171',
   license = 'DO_NOT_DISTRIBUTE',
   repository = REPO,
   unsign = True,
-  deps = ['@jgit//org.eclipse.jgit:jgit'],
+  deps = ['//lib/jgit/org.eclipse.jgit:jgit'],
 )
diff --git a/lib/jgit/org.eclipse.jgit/BUCK b/lib/jgit/org.eclipse.jgit/BUCK
index 17182d0..0d19343 100644
--- a/lib/jgit/org.eclipse.jgit/BUCK
+++ b/lib/jgit/org.eclipse.jgit/BUCK
@@ -4,8 +4,8 @@
 maven_jar(
   name = 'jgit',
   id = 'org.eclipse.jgit:org.eclipse.jgit:' + VERS,
-  bin_sha1 = '32f50e3c0c4f53d8fdca147d3ff0b6ef0dc02eb0',
-  src_sha1 = '1ff155886d6ce2e6e566a90960862aa0e5b226d8',
+  bin_sha1 = 'dc4464c876cbf3815fd6cf6cb9d29d375566d6b1',
+  src_sha1 = 'ab3f9344d524f71c74307e68c82c698266e4bcec',
   license = 'jgit',
   repository = REPO,
   unsign = True,
diff --git a/lib/jgit/tools/BUCK b/lib/jgit/tools/BUCK
deleted file mode 120000
index 7e536d7..0000000
--- a/lib/jgit/tools/BUCK
+++ /dev/null
@@ -1 +0,0 @@
-../../../tools/BUCK
\ No newline at end of file
diff --git a/lib/jgit/tools/__init__.py b/lib/jgit/tools/__init__.py
deleted file mode 120000
index 737f585..0000000
--- a/lib/jgit/tools/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-../../../tools/__init__.py
\ No newline at end of file
diff --git a/lib/jgit/tools/download_file.py b/lib/jgit/tools/download_file.py
deleted file mode 120000
index e49dc4c..0000000
--- a/lib/jgit/tools/download_file.py
+++ /dev/null
@@ -1 +0,0 @@
-../../../tools/download_file.py
\ No newline at end of file
diff --git a/lib/jgit/tools/merge_jars.py b/lib/jgit/tools/merge_jars.py
deleted file mode 120000
index 399e418..0000000
--- a/lib/jgit/tools/merge_jars.py
+++ /dev/null
@@ -1 +0,0 @@
-../../../tools/merge_jars.py
\ No newline at end of file
diff --git a/lib/jgit/tools/pack_war.py b/lib/jgit/tools/pack_war.py
deleted file mode 120000
index 8621a6c..0000000
--- a/lib/jgit/tools/pack_war.py
+++ /dev/null
@@ -1 +0,0 @@
-../../../tools/pack_war.py
\ No newline at end of file
diff --git a/lib/jgit/tools/util.py b/lib/jgit/tools/util.py
deleted file mode 120000
index 8041ac5..0000000
--- a/lib/jgit/tools/util.py
+++ /dev/null
@@ -1 +0,0 @@
-../../../tools/util.py
\ No newline at end of file
diff --git a/lib/jgit/tools/util_test.py b/lib/jgit/tools/util_test.py
deleted file mode 120000
index b85ecbd..0000000
--- a/lib/jgit/tools/util_test.py
+++ /dev/null
@@ -1 +0,0 @@
-../../../tools/util_test.py
\ No newline at end of file
diff --git a/lib/js.defs b/lib/js.defs
index 570ed1e..c9a4256 100644
--- a/lib/js.defs
+++ b/lib/js.defs
@@ -144,7 +144,9 @@
   genrule(
     name = '%s__vulcanized' % name,
     cmd = ' '.join([
-      'unzip', '-qd', '$SRCDIR', '$(location %s)' % components,
+      'unzip', '-qd', '$TMP', '$(location %s)' % components,
+      '&&', 'rm', '-rf', '$SRCDIR/bower_components',
+      '&&', 'ln', '-s', '-f', '$TMP/bower_components', '.',
       '&&', run_npm_binary('//lib/js:vulcanize')
     ] + VULCANIZE_FLAGS + extra_flags + [
       '--out-html', '$OUT',
diff --git a/lib/js/BUCK b/lib/js/BUCK
index 86d69e6..36a3d19 100644
--- a/lib/js/BUCK
+++ b/lib/js/BUCK
@@ -134,18 +134,6 @@
 )
 
 bower_component(
-  name = 'iron-ajax',
-  package = 'polymerelements/iron-ajax',
-  version = '1.2.0',
-  deps = [
-    ':polymer',
-    ':promise-polyfill',
-  ],
-  license = 'polymer',
-  sha1 = 'f195d0d0ddef73a20573b0a02ce6a505cc1d7014',
-)
-
-bower_component(
   name = 'iron-autogrow-textarea',
   package = 'polymerelements/iron-autogrow-textarea',
   version = '1.0.12',
diff --git a/lib/maven.defs b/lib/maven.defs
index 4107770..913be35 100644
--- a/lib/maven.defs
+++ b/lib/maven.defs
@@ -16,6 +16,7 @@
 GERRIT_API = 'GERRIT_API:'
 MAVEN_CENTRAL = 'MAVEN_CENTRAL:'
 MAVEN_LOCAL = 'MAVEN_LOCAL:'
+MAVEN_SNAPSHOT = 'MAVEN_SNAPSHOT:'
 
 def define_license(name):
   n = 'LICENSE-' + name
@@ -43,37 +44,48 @@
     local_license = False):
   from os import path
 
+  def maven_snapshot(parts):
+    if len(parts) != 4:
+      raise NameError('%s:\nexpected id="groupId:artifactId:version:snapshot]"'
+                      % id)
+    group, artifact, version, snapshot = parts
+    jar = path.join(name,
+      version + '-SNAPSHOT',
+      '-'.join([artifact.lower(), version, snapshot]))
+    url = '/'.join([
+      repository,
+      group.replace('.', '/'),
+      artifact,
+      version + '-SNAPSHOT',
+      '-'.join([artifact.lower(), version, snapshot])])
+    return jar, url
+
+  def maven_release(parts):
+    if len(parts) not in [3, 4]:
+      raise NameError('%s:\nexpected id="groupId:artifactId:version[:classifier]"'
+                      % id)
+    if len(parts) == 4:
+      group, artifact, version, classifier = parts
+      file_version = version + '-' + classifier
+    else:
+      group, artifact, version = parts
+      file_version = version
+
+    jar = path.join(name, artifact.lower() + '-' + file_version)
+    url = '/'.join([
+      repository,
+      group.replace('.', '/'),
+      artifact,
+      version,
+      artifact + '-' + file_version])
+
+    return jar, url
+
   parts = id.split(':')
-  if len(parts) not in [3, 4]:
-    raise NameError('%s:\nexpected id="groupId:artifactId:version[:classifier]"'
-                    % id)
-  if len(parts) == 4:
-    group, artifact, version, classifier = parts
+  if repository.startswith(MAVEN_SNAPSHOT):
+    jar, url = maven_snapshot(parts)
   else:
-    group, artifact, version = parts
-    classifier = None
-
-  # 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'
-  else:
-    file_version = version
-
-  if classifier is not None:
-    file_version += '-' + classifier
-
-  jar = path.join(name, artifact.lower() + '-' + file_version)
-
-  url = '/'.join([
-    repository,
-    group.replace('.', '/'), artifact, version,
-    artifact + '-' + file_version])
+    jar, url = maven_release(parts)
 
   binjar = jar + '.jar'
   binurl = url + '.jar'
diff --git a/plugins/cookbook-plugin b/plugins/cookbook-plugin
index fb6e534..3e801bd 160000
--- a/plugins/cookbook-plugin
+++ b/plugins/cookbook-plugin
@@ -1 +1 @@
-Subproject commit fb6e534d297e121a9ddef503f6c4f393b50f9238
+Subproject commit 3e801bd7d488c0b750422b32e4d4729beafcc00c
diff --git a/plugins/download-commands b/plugins/download-commands
index 1f743e5..fb05fb9 160000
--- a/plugins/download-commands
+++ b/plugins/download-commands
@@ -1 +1 @@
-Subproject commit 1f743e5a877a8ef97b1f3a9e72933c0d87e11d6e
+Subproject commit fb05fb988e17b1a2eb7ef476b8613419312afabd
diff --git a/plugins/replication b/plugins/replication
index 80926ae..945c842 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 80926aeea61cf73ab60c7e2379ee091c3d118925
+Subproject commit 945c842f9c884469ec0fb2d883d6cda552e97747
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index 0ea78c9..c167df0 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit 0ea78c9ca7c70515c81681af05a65ec4dc32a542
+Subproject commit c167df08a8550d8c6c7ccf12b7df4fa6bfc6d432
diff --git a/plugins/singleusergroup b/plugins/singleusergroup
index f6df712..141898f 160000
--- a/plugins/singleusergroup
+++ b/plugins/singleusergroup
@@ -1 +1 @@
-Subproject commit f6df7121d2704e73c2a315a660e5cc4e12ab1ab9
+Subproject commit 141898f9e160c190bdf28999e31f5c667a477f9c
diff --git a/polygerrit-ui/BUCK b/polygerrit-ui/BUCK
index fa7f52c..614e85c 100644
--- a/polygerrit-ui/BUCK
+++ b/polygerrit-ui/BUCK
@@ -4,7 +4,6 @@
   name = 'polygerrit_components',
   deps = [
     '//lib/js:fetch',
-    '//lib/js:iron-ajax',
     '//lib/js:iron-autogrow-textarea',
     '//lib/js:iron-dropdown',
     '//lib/js:iron-input',
diff --git a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html
new file mode 100644
index 0000000..6b35328
--- /dev/null
+++ b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html
@@ -0,0 +1,18 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<link rel="import" href="../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../elements/shared/gr-tooltip/gr-tooltip.html">
+<script src="gr-tooltip-behavior.js"></script>
diff --git a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
new file mode 100644
index 0000000..2da6846
--- /dev/null
+++ b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
@@ -0,0 +1,110 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function(window) {
+  'use strict';
+
+  var BOTTOM_OFFSET = 10;
+
+  var TooltipBehavior = {
+
+    properties: {
+      hasTooltip: Boolean,
+
+      _tooltip: Element,
+      _titleText: String,
+    },
+
+    attached: function() {
+      if (!this.hasTooltip) { return; }
+
+      this.addEventListener('mouseover', this._handleShowTooltip.bind(this));
+      this.addEventListener('mouseout', this._handleHideTooltip.bind(this));
+      this.addEventListener('focusin', this._handleShowTooltip.bind(this));
+      this.addEventListener('focusout', this._handleHideTooltip.bind(this));
+      this.listen(window, 'scroll', '_handleWindowScroll');
+    },
+
+    detached: function() {
+      this.unlisten(window, 'scroll', '_handleWindowScroll');
+    },
+
+    _handleShowTooltip: function(e) {
+      if (!this.hasAttribute('title') || this._tooltip) { return; }
+
+      // Store the title attribute text then set it to an empty string to
+      // prevent it from showing natively.
+      this._titleText = this.getAttribute('title');
+      this.setAttribute('title', '');
+
+      var tooltip = document.createElement('gr-tooltip');
+      tooltip.text = this._titleText;
+
+      // Set visibility to hidden before appending to the DOM so that
+      // calculations can be made based on the element’s size.
+      tooltip.style.visibility = 'hidden';
+      Polymer.dom(document.body).appendChild(tooltip);
+      this._positionTooltip(tooltip);
+      tooltip.style.visibility = null;
+
+      this._tooltip = tooltip;
+    },
+
+    _handleHideTooltip: function(e) {
+      if (!this.hasAttribute('title') ||
+          !this._titleText ||
+          this === document.activeElement) { return; }
+
+      this.setAttribute('title', this._titleText);
+      if (this._tooltip && this._tooltip.parentNode) {
+        this._tooltip.parentNode.removeChild(this._tooltip);
+      }
+      this._tooltip = null;
+    },
+
+    _handleWindowScroll: function(e) {
+      if (!this._tooltip) { return; }
+
+      this._positionTooltip(this._tooltip);
+    },
+
+    _positionTooltip: function(tooltip) {
+      var offset = this._getOffset(this);
+      var top = offset.top;
+      var left = offset.left;
+
+      top -= this.offsetHeight + BOTTOM_OFFSET;
+      left -= (tooltip.offsetWidth / 2) - (this.offsetWidth / 2);
+      left = Math.max(0, left);
+      top = Math.max(0, top);
+
+      tooltip.style.left = left + 'px';
+      tooltip.style.top = top + 'px';
+    },
+
+    _getOffset: function(el) {
+      var top = 0;
+      var left = 0;
+      for (var node = el; node; node = node.offsetParent) {
+        if (node.offsetTop) { top += node.offsetTop; }
+        if (node.offsetLeft) { left += node.offsetLeft; }
+      }
+      top += window.scrollY;
+      left += window.scrollX;
+      return {top: top, left: left};
+    },
+  };
+
+  window.Gerrit = window.Gerrit || {};
+  window.Gerrit.TooltipBehavior = TooltipBehavior;
+})(window);
diff --git a/polygerrit-ui/app/behaviors/rest-client-behavior.html b/polygerrit-ui/app/behaviors/rest-client-behavior.html
index f6897cd..c515664 100644
--- a/polygerrit-ui/app/behaviors/rest-client-behavior.html
+++ b/polygerrit-ui/app/behaviors/rest-client-behavior.html
@@ -102,6 +102,11 @@
     changePath: function(changeNum) {
       return '/c/' + changeNum;
     },
+
+    changeIsOpen: function(status) {
+      return status === this.ChangeStatus.NEW ||
+          status === this.ChangeStatus.DRAFT;
+    },
   };
 
   window.Gerrit = window.Gerrit || {};
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
index 1d88b28..24f915a 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
@@ -68,9 +68,12 @@
     <span class="cell keyboard">
       <span class="positionIndicator">&#x25b6;</span>
     </span>
-    <span class="cell star" hidden$="[[!showStar]]">
+    <span class="cell star" hidden$="[[!showStar]]" hidden>
       <gr-change-star change="{{change}}"></gr-change-star>
     </span>
+    <a class="cell number" href$="[[changeURL]]" hidden$="[[!showNumber]]" hidden>
+      [[change._number]]
+    </a>
     <a class="cell subject" href$="[[changeURL]]">[[change.subject]]</a>
     <span class="cell status">[[_computeChangeStatusString(change)]]</span>
     <span class="cell owner">
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
index bcceb35..d160933 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
@@ -127,7 +127,7 @@
     },
 
     _computeProjectURL: function(project) {
-      return '/projects/' + project + ',dashboards/default';
+      return '/q/status:open+project:' + project;
     },
 
     _computeProjectBranchURL: function(project, branch) {
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
index 8447215..5507b9f 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
@@ -118,7 +118,7 @@
           {labels: {Verified: {rejected: true}}}, 'Verified'), '✕');
 
       assert.equal(element._computeProjectURL('combustible-stuff'),
-          '/projects/combustible-stuff,dashboards/default');
+          '/q/status:open+project:combustible-stuff');
 
       assert.equal(element._computeProjectBranchURL(
           'combustible-stuff', 'lemons'),
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
index 6788cce..1d3968f 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
@@ -15,8 +15,7 @@
 -->
 
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/rest-client-behavior.html">
-<link rel="import" href="../../shared/gr-ajax/gr-ajax.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../gr-change-list/gr-change-list.html">
 
 <dom-module id="gr-change-list-view">
@@ -25,25 +24,15 @@
       :host {
         background-color: var(--view-background-color);
         display: block;
-        margin: 0 var(--default-horizontal-margin);
-      }
-      .loading,
-      .error {
-        margin-top: 1em;
-        background-color: #f1f2f3;
       }
       .loading {
         color: #666;
-      }
-      .error {
-        color: #D32F2F;
+        padding: 1em var(--default-horizontal-margin);
       }
       gr-change-list {
-        margin-top: 1em;
         width: 100%;
       }
       nav {
-        margin-bottom: 1em;
         padding: .5em 0;
         text-align: center;
       }
@@ -54,39 +43,27 @@
         margin-right: .5em;
       }
       @media only screen and (max-width: 50em) {
-        :host {
-          margin: 0;
-        }
         .loading,
         .error {
           padding: 0 var(--default-horizontal-margin);
         }
       }
     </style>
-    <gr-ajax
-        auto
-        url="/changes/"
-        params="[[_computeQueryParams(_query, _offset, changesPerPage)]]"
-        last-response="{{_changes}}"
-        last-error="{{_lastError}}"
-        loading="{{_loading}}"></gr-ajax>
-    <div class="loading" hidden$="[[!_loading]]" hidden>Loading...</div>
-    <div class="error" hidden$="[[_computeErrorHidden(_loading, _lastError)]]" hidden>
-      [[_lastError.request.xhr.responseText]]
-    </div>
-    <div hidden$="[[_computeListHidden(_loading, _lastError)]]" hidden>
+    <div class="loading" hidden$="[[!_loading]]">Loading...</div>
+    <div hidden$="[[_loading]]">
       <gr-change-list
           changes="{{_changes}}"
           selected-index="{{viewState.selectedChangeIndex}}"
           show-star="[[loggedIn]]"></gr-change-list>
       <nav>
-        <a href$="[[_computeNavLink(_query, _offset, -1, changesPerPage)]]"
+        <a href$="[[_computeNavLink(_query, _offset, -1, _changesPerPage)]]"
            hidden$="[[_hidePrevArrow(_offset)]]">&larr; Prev</a>
-        <a href$="[[_computeNavLink(_query, _offset, 1, changesPerPage)]]"
-           hidden$="[[_hideNextArrow(_changes.length, changesPerPage)]]">
+        <a href$="[[_computeNavLink(_query, _offset, 1, _changesPerPage)]]"
+           hidden$="[[_hideNextArrow(_changes.length, _changesPerPage)]]">
           Next &rarr;</a>
       </nav>
     </div>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-change-list-view.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
index a694fc9..79531ac 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
@@ -49,7 +49,7 @@
         value: function() { return {}; },
       },
 
-      changesPerPage: Number,
+      _changesPerPage: Number,
 
       /**
        * Currently active query.
@@ -67,11 +67,6 @@
       _changes: Array,
 
       /**
-       * Contains error of last request (in case of change loading error).
-       */
-      _lastError: Object,
-
-      /**
        * For showing a "loading..." string during ajax requests.
        */
       _loading: {
@@ -80,10 +75,6 @@
       },
     },
 
-    behaviors: [
-      Gerrit.RESTClientBehavior,
-    ],
-
     attached: function() {
       this.fire('title-change', {title: this._query});
     },
@@ -91,6 +82,7 @@
     _paramsChanged: function(value) {
       if (value.view != this.tagName.toLowerCase()) { return; }
 
+      this._loading = true;
       this._query = value.query;
       this._offset = value.offset || 0;
       if (this.viewState.query != this._query ||
@@ -101,22 +93,23 @@
       }
 
       this.fire('title-change', {title: this._query});
+
+      this._getPreferences().then(function(prefs) {
+        this._changesPerPage = prefs.changes_per_page;
+        return this._getChanges();
+      }.bind(this)).then(function(changes) {
+        this._changes = changes;
+        this._loading = false;
+      }.bind(this));
     },
 
-    _computeQueryParams: function(query, offset, changesPerPage) {
-      var options = this.listChangesOptionsToHex(
-          this.ListChangesOption.LABELS,
-          this.ListChangesOption.DETAILED_ACCOUNTS
-      );
-      var obj = {
-        n: changesPerPage,
-        O: options,
-        S: offset || 0,
-      };
-      if (query && query.length > 0) {
-        obj.q = query;
-      }
-      return obj;
+    _getChanges: function() {
+      return this.$.restAPI.getChanges(this._changesPerPage, this._query,
+          this._offset);
+    },
+
+    _getPreferences: function() {
+      return this.$.restAPI.getPreferences();
     },
 
     _computeNavLink: function(query, offset, direction, changesPerPage) {
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html
index 8ff66cb..bab2014 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html
@@ -18,6 +18,7 @@
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html">
 <link rel="import" href="../../../behaviors/rest-client-behavior.html">
 <link rel="import" href="../../../styles/gr-change-list-styles.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../gr-change-list-item/gr-change-list-item.html">
 
 <dom-module id="gr-change-list">
@@ -31,7 +32,8 @@
     <style include="gr-change-list-styles"></style>
     <div class="headerRow">
       <span class="topHeader keyboard"></span> <!-- keyboard position indicator -->
-      <span class="topHeader star" hidden$="[[!showStar]]"></span>
+      <span class="topHeader star" hidden$="[[!showStar]]" hidden></span>
+      <span class="topHeader number" hidden$="[[!showNumber]]" hidden>#</span>
       <span class="topHeader subject">Subject</span>
       <span class="topHeader status">Status</span>
       <span class="topHeader owner">Owner</span>
@@ -57,10 +59,12 @@
             selected$="[[_computeItemSelected(index, groupIndex, selectedIndex)]]"
             needs-review="[[_computeItemNeedsReview(account, change, showReviewedState)]]"
             change="[[change]]"
+            show-number="[[showNumber]]"
             show-star="[[showStar]]"
             label-names="[[labelNames]]"></gr-change-list-item>
       </template>
     </template>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-change-list.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
index ef71be3..4e17253 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
@@ -58,6 +58,7 @@
         type: Number,
         notify: true,
       },
+      showNumber: Boolean, // No default value to prevent flickering.
       showStar: {
         type: Boolean,
         value: false,
@@ -77,6 +78,31 @@
       Gerrit.RESTClientBehavior,
     ],
 
+    attached: function() {
+      this._loadPreferences();
+    },
+
+    _loadPreferences: function() {
+      return this._getLoggedIn().then(function(loggedIn) {
+        if (!loggedIn) {
+          this.showNumber = false;
+          return;
+        }
+        return this._getPreferences().then(function(preferences) {
+          this.showNumber = !!(preferences &&
+              preferences.legacycid_in_change_table);
+        }.bind(this));
+      }.bind(this));
+    },
+
+    _getLoggedIn: function() {
+      return this.$.restAPI.getLoggedIn();
+    },
+
+    _getPreferences: function() {
+      return this.$.restAPI.getPreferences();
+    },
+
     _computeLabelNames: function(groups) {
       if (!groups) { return []; }
       var labels = [];
@@ -119,7 +145,7 @@
 
     _computeItemNeedsReview: function(account, change, showReviewedState) {
       return showReviewedState && !change.reviewed &&
-          change.status != this.ChangeStatus.MERGED &&
+          this.changeIsOpen(change.status) &&
           account._account_id != change.owner._account_id;
     },
 
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
index 25f6d80..aa77b77 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
@@ -46,6 +46,60 @@
       element = fixture('basic');
     });
 
+    function stubRestAPI(preferences) {
+      var loggedInPromise = Promise.resolve(preferences !== null);
+      var preferencesPromise = Promise.resolve(preferences);
+      stub('gr-rest-api-interface', {
+        getLoggedIn: sinon.stub().returns(loggedInPromise),
+        getPreferences: sinon.stub().returns(preferencesPromise),
+      });
+      return Promise.all([loggedInPromise, preferencesPromise]);
+    }
+
+    suite('test show change number not logged in', function() {
+      setup(function(done) {
+        return stubRestAPI(null).then(function() {
+          element = fixture('basic');
+          element._loadPreferences().then(function() { done(); });
+        });
+      });
+
+      test('show number disabled', function() {
+        assert.isFalse(element.showNumber);
+      });
+    });
+
+    suite('test show change number preference enabled', function() {
+      setup(function(done) {
+        return stubRestAPI(
+          {legacycid_in_change_table: true, time_format: 'HHMM_12'}
+        ).then(function() {
+          element = fixture('basic');
+          element._loadPreferences().then(function() { done(); });
+        });
+      });
+
+      test('show number enabled', function() {
+        assert.isTrue(element.showNumber);
+      });
+    });
+
+    suite('test show change number preference disabled', function() {
+      setup(function(done) {
+        // legacycid_in_change_table is not set when false.
+        return stubRestAPI(
+          {time_format: 'HHMM_12'}
+        ).then(function() {
+          element = fixture('basic');
+          element._loadPreferences().then(function() { done(); });
+        });
+      });
+
+      test('show number disabled', function() {
+        assert.isFalse(element.showNumber);
+      });
+    });
+
     test('computed fields', function() {
       assert.equal(element._computeLabelNames(
           [[{_number: 0, labels: {}}]]).length, 0);
@@ -108,48 +162,58 @@
       element.changes = [
         {
           _number: 0,
+          status: 'NEW',
           reviewed: true,
-          owner: { _account_id: 0 },
+          owner: {_account_id: 0},
         },
         {
           _number: 1,
-          owner: { _account_id: 0 },
+          status: 'NEW',
+          owner: {_account_id: 0},
         },
         {
           _number: 2,
           status: 'MERGED',
-          owner: { _account_id: 0 },
+          owner: {_account_id: 0},
         },
         {
           _number: 3,
-          owner: { _account_id: 42 },
+          status: 'DRAFT',
+          owner: {_account_id: 42},
+        },
+        {
+          _number: 4,
+          status: 'ABANDONED',
+          owner: {_account_id: 0},
         }
       ];
       flushAsynchronousOperations();
       var elementItems = Polymer.dom(element.root).querySelectorAll(
           'gr-change-list-item');
-      assert.equal(elementItems.length, 4);
+      assert.equal(elementItems.length, 5);
       for (var i = 0; i < elementItems.length; i++) {
         assert.isFalse(elementItems[i].hasAttribute('needs-review'));
       }
 
       element.showReviewedState = true;
-      var elementItems = Polymer.dom(element.root).querySelectorAll(
+      elementItems = Polymer.dom(element.root).querySelectorAll(
           'gr-change-list-item');
-      assert.equal(elementItems.length, 4);
+      assert.equal(elementItems.length, 5);
       assert.isFalse(elementItems[0].hasAttribute('needs-review'));
       assert.isTrue(elementItems[1].hasAttribute('needs-review'));
       assert.isFalse(elementItems[2].hasAttribute('needs-review'));
       assert.isTrue(elementItems[3].hasAttribute('needs-review'));
+      assert.isFalse(elementItems[4].hasAttribute('needs-review'));
 
-      element.account = { _account_id: 42 };
-      var elementItems = Polymer.dom(element.root).querySelectorAll(
+      element.account = {_account_id: 42};
+      elementItems = Polymer.dom(element.root).querySelectorAll(
           'gr-change-list-item');
-      assert.equal(elementItems.length, 4);
+      assert.equal(elementItems.length, 5);
       assert.isFalse(elementItems[0].hasAttribute('needs-review'));
       assert.isTrue(elementItems[1].hasAttribute('needs-review'));
       assert.isFalse(elementItems[2].hasAttribute('needs-review'));
       assert.isFalse(elementItems[3].hasAttribute('needs-review'));
+      assert.isFalse(elementItems[4].hasAttribute('needs-review'));
     });
 
     test('no changes', function() {
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html
index e351f44..ce413ca 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html
@@ -15,7 +15,7 @@
 -->
 
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/rest-client-behavior.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
 <dom-module id="gr-dashboard-view">
   <template>
@@ -23,32 +23,20 @@
       :host {
         background-color: var(--view-background-color);
         display: block;
-        margin: 0 var(--default-horizontal-margin);
       }
       .loading {
-        margin-top: 1em;
         color: #666;
-        background-color: #f1f2f3;
+        padding: 1em var(--default-horizontal-margin);
       }
       gr-change-list {
-        margin-top: 1em;
         width: 100%;
       }
       @media only screen and (max-width: 50em) {
-        :host {
-          margin: 0;
-        }
         .loading {
           padding: 0 var(--default-horizontal-margin);
         }
       }
     </style>
-    <gr-ajax
-        auto
-        url="/changes/"
-        params="[[_computeQueryParams()]]"
-        last-response="{{_results}}"
-        loading="{{_loading}}"></gr-ajax>
     <div class="loading" hidden$="[[!_loading]]">Loading...</div>
     <div hidden$="[[_loading]]" hidden>
       <gr-change-list
@@ -59,6 +47,7 @@
           groups="{{_results}}"
           group-titles="[[_groupTitles]]"></gr-change-list>
     </div>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-dashboard-view.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
index fc6a3ff..3ac6463 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
@@ -49,28 +49,21 @@
       },
     },
 
-    behaviors: [
-      Gerrit.RESTClientBehavior,
-    ],
-
     attached: function() {
       this.fire('title-change', {title: 'My Reviews'});
+
+      this._loading = true;
+      this._getDashboardChanges().then(function(results) {
+        this._results = results;
+        this._loading = false;
+      }.bind(this)).catch(function(err) {
+        this._loading = false;
+        console.error(err.message);
+      }.bind(this));
     },
 
-    _computeQueryParams: function() {
-      var options = this.listChangesOptionsToHex(
-          this.ListChangesOption.LABELS,
-          this.ListChangesOption.DETAILED_ACCOUNTS,
-          this.ListChangesOption.REVIEWED
-      );
-      return {
-        O: options,
-        q: [
-          'is:open owner:self',
-          'is:open reviewer:self -owner:self',
-          'is:closed (owner:self OR reviewer:self) -age:4w limit:10',
-        ],
-      };
+    _getDashboardChanges: function() {
+      return this.$.restAPI.getDashboardChanges();
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
index de04c00..fc2e929 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
@@ -25,6 +25,7 @@
 
 <link rel="import" href="../gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html">
 <link rel="import" href="../gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html">
+<link rel="import" href="../gr-confirm-revert-dialog/gr-confirm-revert-dialog.html">
 
 <dom-module id="gr-change-actions">
   <template>
@@ -92,10 +93,16 @@
           hidden></gr-confirm-rebase-dialog>
       <gr-confirm-cherrypick-dialog id="confirmCherrypick"
           class="confirmDialog"
-          message="[[commitMessage]]"
+          commit-info="[[commitInfo]]"
           on-confirm="_handleCherrypickConfirm"
           on-cancel="_handleConfirmDialogCancel"
           hidden></gr-confirm-cherrypick-dialog>
+      <gr-confirm-revert-dialog id="confirmRevertDialog"
+          class="confirmDialog"
+          commit-info="[[commitInfo]]"
+          on-confirm="_handleRevertDialogConfirm"
+          on-cancel="_handleConfirmDialogCancel"
+          hidden></gr-confirm-revert-dialog>
     </gr-overlay>
     <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
index d201447..dd63764 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -19,6 +19,7 @@
     ABANDON: 'abandon',
     DELETE: '/',
     RESTORE: 'restore',
+    REVERT: 'revert',
   };
 
   // TODO(andybons): Add the rest of the revision actions.
@@ -37,6 +38,7 @@
     'publish': 'Publishing...',
     'rebase': 'Rebasing...',
     'restore': 'Restoring...',
+    'revert': 'Reverting...',
     'submit': 'Submitting...',
   };
 
@@ -60,7 +62,10 @@
       },
       changeNum: String,
       patchNum: String,
-      commitMessage: String,
+      commitInfo: {
+        type: Object,
+        readOnly: true,
+      },
       _loading: {
         type: Boolean,
         value: true,
@@ -83,6 +88,8 @@
 
       this._loading = true;
       return this._getRevisionActions().then(function(revisionActions) {
+        if (!revisionActions) { return; }
+
         this._revisionActions = revisionActions;
         this._loading = false;
       }.bind(this)).catch(function(err) {
@@ -90,7 +97,7 @@
             'and contact the PolyGerrit team for assistance.');
         this._loading = false;
         throw err;
-      });
+      }.bind(this));
     },
 
     _getRevisionActions: function() {
@@ -143,26 +150,35 @@
       e.preventDefault();
       var el = Polymer.dom(e).rootTarget;
       var key = el.getAttribute('data-action-key');
-      if (key === RevisionActions.SUBMIT &&
-          this._canSubmitChange() === false) {
-        return;
-      }
       var type = el.getAttribute('data-action-type');
       if (type === ActionType.REVISION) {
-        if (key === RevisionActions.REBASE) {
-          this._showActionDialog(this.$.confirmRebase);
-          return;
-        } else if (key === RevisionActions.CHERRYPICK) {
-          this._showActionDialog(this.$.confirmCherrypick);
-          return;
-        }
-        this._fireAction(this._prependSlash(key),
-            this._revisionActions[key], true);
+        this._handleRevisionAction(key);
+      } else if (key === ChangeActions.REVERT) {
+        this._showActionDialog(this.$.confirmRevertDialog);
       } else {
         this._fireAction(this._prependSlash(key), this.actions[key], false);
       }
     },
 
+    _handleRevisionAction: function(key) {
+      switch (key) {
+        case RevisionActions.REBASE:
+          this._showActionDialog(this.$.confirmRebase);
+          break;
+        case RevisionActions.CHERRYPICK:
+          this._showActionDialog(this.$.confirmCherrypick);
+          break;
+        case RevisionActions.SUBMIT:
+          if (!this._canSubmitChange()) {
+            return;
+          }
+          /* falls through */ // required by JSHint
+        default:
+          this._fireAction(this._prependSlash(key),
+              this._revisionActions[key], true);
+      }
+    },
+
     _prependSlash: function(key) {
       return key === '/' ? key : '/' + key;
     },
@@ -219,6 +235,25 @@
       );
     },
 
+    _handleRevertDialogConfirm: function() {
+      var el = this.$.confirmRevertDialog;
+      if (!el.message) {
+        // TODO(viktard): Fix validation.
+        alert('The revert commit message can’t be empty.');
+        return;
+      }
+      this.$.overlay.close();
+      el.hidden = false;
+      this._fireAction(
+          '/revert',
+          this.actions.revert,
+          false,
+          {
+            message: el.message,
+          }
+      );
+    },
+
     _setLoadingOnButtonWithKey: function(key) {
       var buttonEl = this.$$('[data-action-key="' + key + '"]');
       buttonEl.setAttribute('loading', true);
@@ -226,7 +261,7 @@
       return function() {
         buttonEl.removeAttribute('loading');
         buttonEl.disabled = false;
-      }
+      };
     },
 
     _fireAction: function(endpoint, action, revAction, opt_payload) {
@@ -244,6 +279,7 @@
     _handleResponse: function(action, response) {
       return this.$.restAPI.getResponseObject(response).then(function(obj) {
         switch (action.__key) {
+          case ChangeActions.REVERT:
           case RevisionActions.CHERRYPICK:
             page.show(this.changePath(obj._number));
             break;
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
index edb04cd..098e097 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -34,10 +34,7 @@
 <script>
   suite('gr-change-actions tests', function() {
     var element;
-    var response = {
-      ok: true,
-    };
-    setup(function(done) {
+    setup(function() {
       stub('gr-rest-api-interface', {
         getChangeRevisionActions: function() {
           return Promise.resolve({
@@ -82,7 +79,7 @@
       element = fixture('basic');
       element.changeNum = '42';
       element.patchNum = '2';
-      element.reload().then(function() { done(); });
+      return element.reload();
     });
 
     test('submit, rebase, and cherry-pick buttons show', function(done) {
@@ -156,11 +153,21 @@
       });
     });
 
-    test('cherry-pick change', function(done) {
-      var fireActionStub = sinon.stub(element, '_fireAction');
-      var alertStub = sinon.stub(window, 'alert');
+    suite('cherry-pick', function() {
+      var fireActionStub;
+      var alertStub;
 
-      flush(function() {
+      setup(function() {
+        fireActionStub = sinon.stub(element, '_fireAction');
+        alertStub = sinon.stub(window, 'alert');
+      });
+
+      teardown(function() {
+        alertStub.restore();
+        fireActionStub.restore();
+      });
+
+      test('works', function() {
         var rebaseButton = element.$$('gr-button[data-action-key="rebase"]');
         MockInteractions.tap(rebaseButton);
         var action = {
@@ -188,13 +195,59 @@
             message: 'foo message',
           }
         ]);
-
-        fireActionStub.restore();
-        alertStub.restore();
-        done();
       });
     });
 
+    suite('revert change', function() {
+      var alertStub;
+      var fireActionStub;
 
+      setup(function() {
+        fireActionStub = sinon.stub(element, '_fireAction');
+        alertStub = sinon.stub(window, 'alert');
+        element.actions = {
+          revert: {
+            method: 'POST',
+            label: 'Revert',
+            title: 'Revert the change',
+            enabled: true
+          }
+        };
+        return element.reload();
+      });
+
+      teardown(function() {
+        alertStub.restore();
+        fireActionStub.restore();
+      });
+
+      test('validation', function() {
+        element._handleRevertDialogConfirm();
+        assert.notOk(fireActionStub.called);
+        assert.ok(alertStub.called);
+      });
+
+      test('works', function() {
+        var revertButton = element.$$('gr-button[data-action-key="revert"]');
+        MockInteractions.tap(revertButton);
+
+        element.$.confirmRevertDialog.message = 'foo message';
+        element._handleRevertDialogConfirm();
+        assert.notOk(alertStub.called);
+
+        var action = {
+          __key: 'revert',
+          __type: 'change',
+          enabled: true,
+          label: 'Revert',
+          method: 'POST',
+          title: 'Revert the change',
+        };
+        assert.deepEqual(fireActionStub.lastCall.args, [
+          '/revert', action, false, {
+            message: 'foo message',
+          }]);
+      });
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
index f6733d1..4fa98ca 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -36,9 +36,7 @@
     ],
 
     _computeHideStrategy: function(change) {
-      var open = change.status == this.ChangeStatus.NEW ||
-          change.status == this.ChangeStatus.DRAFT;
-      return !open;
+      return !this.changeIsOpen(change.status);
     },
 
     _computeStrategy: function(change) {
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index 8b77489..9a7537b 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -21,6 +21,7 @@
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-change-star/gr-change-star.html">
 <link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
+<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
 <link rel="import" href="../../shared/gr-linked-text/gr-linked-text.html">
 <link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
@@ -36,14 +37,12 @@
 <dom-module id="gr-change-view">
   <template>
     <style>
-      .container {
-        margin: 1em var(--default-horizontal-margin);
-      }
       .container:not(.loading) {
         background-color: var(--view-background-color);
       }
       .container.loading {
         color: #666;
+        padding: 1em var(--default-horizontal-margin);
       }
       .headerContainer {
         height: 4.1em;
@@ -63,7 +62,7 @@
         position: fixed;
         top: 0;
         transition: box-shadow 250ms linear;
-        width: calc(100% - (2 * var(--default-horizontal-margin)));
+        width: 100%;
       }
       .header-title {
         flex: 1;
@@ -137,36 +136,29 @@
         align-content: flex-start;
         display: flex;
         flex: 1;
-        flex-wrap: wrap;
         overflow-x: hidden;
       }
+      .relatedChanges {
+        flex: 1 1 auto;
+        font-size: .9em;
+        overflow: hidden;
+      }
       gr-file-list {
         margin-bottom: 1em;
         padding: 0 var(--default-horizontal-margin);
       }
       @media screen and (max-width: 50em) {
-        .container {
-          margin: .5em 0 !important;
-        }
-        .container.loading {
-          margin: 1em var(--default-horizontal-margin) !important;
-        }
         .headerContainer {
           height: 5.15em;
         }
         .header {
           align-items: flex-start;
           flex-direction: column;
-          padding: .5em var(--default-horizontal-margin) !important;
+          padding: .5em var(--default-horizontal-margin);
         }
         gr-change-star {
           vertical-align: middle;
         }
-        .header-title,
-        .header-actions,
-        .header.pinned {
-          width: 100% !important;
-        }
         .header-title {
           font-size: 1.1em;
         }
@@ -184,15 +176,15 @@
           display: none;
         }
         .patchSelectLabel {
-          margin-left: 0 !important;
+          margin-left: 0;
           margin-right: .5em;
         }
         .header select {
-          margin-left: 0 !important;
+          margin-left: 0;
           margin-right: .5em;
         }
         .header .reply {
-          margin-left: 0 !important;
+          margin-left: 0;
           margin-right: .5em;
         }
         .changeInfo-column:not(:last-of-type) {
@@ -204,8 +196,11 @@
           flex-direction: column;
           flex-wrap: nowrap;
         }
+        .relatedChanges,
         .changeMetadata {
           font-size: 1em;
+        }
+        .changeMetadata {
           border-right: none;
           margin-bottom: 1em;
           margin-top: .25em;
@@ -225,7 +220,7 @@
             <gr-change-star change="{{_change}}" hidden$="[[!_loggedIn]]"></gr-change-star>
             <a href$="[[_computeChangePermalink(_change._number)]]">[[_change._number]]</a><span>:</span>
             <span>[[_change.subject]]</span>
-            <span class="changeStatus">[[_computeChangeStatus(_change, _patchNum)]]</span>
+            <span class="changeStatus">[[_computeChangeStatus(_change, _patchRange.patchNum)]]</span>
           </span>
           <span class="header-actions">
             <gr-button hidden
@@ -238,7 +233,7 @@
               <label class="patchSelectLabel" for="patchSetSelect">Patch set</label>
               <select id="patchSetSelect" on-change="_handlePatchChange">
                 <template is="dom-repeat" items="{{_allPatchSets}}" as="patchNumber">
-                  <option value$="[[patchNumber]]" selected$="[[_computePatchIndexIsSelected(index, _patchNum)]]">
+                  <option value$="[[patchNumber]]" selected$="[[_computePatchIndexIsSelected(index, _patchRange.patchNum)]]">
                     <span>[[patchNumber]]</span>
                     /
                     <span>[[_computeLatestPatchNum(_change)]]</span>
@@ -258,8 +253,8 @@
           <gr-change-actions id="actions"
               actions="[[_change.actions]]"
               change-num="[[_changeNum]]"
-              patch-num="[[_patchNum]]"
-              commit-message="[[_commitInfo.message]]"
+              patch-num="[[_patchRange.patchNum]]"
+              commit-info="[[_commitInfo]]"
               on-reload-change="_handleReloadChange"></gr-change-actions>
         </div>
         <div class="changeInfo-column commitAndRelated">
@@ -271,17 +266,18 @@
           </div>
           <div class="relatedChanges">
             <gr-related-changes-list id="relatedChanges"
-              change="[[_change]]"
-              server-config="[[serverConfig]]"
-              patch-num="[[_patchNum]]"></gr-related-changes-list>
+                change="[[_change]]"
+                patch-num="[[_patchRange.patchNum]]"></gr-related-changes-list>
           </div>
         </div>
       </section>
       <gr-file-list id="fileList"
           change-num="[[_changeNum]]"
-          patch-num="[[_patchNum]]"
+          patch-range="[[_patchRange]]"
           comments="[[_comments]]"
           drafts="[[_diffDrafts]]"
+          revisions="[[_change.revisions]]"
+          projectConfig="[[projectConfig]]"
           selected-index="{{viewState.selectedFileIndex}}"></gr-file-list>
       <gr-messages-list id="messageList"
           change-num="[[_changeNum]]"
@@ -294,7 +290,7 @@
     <gr-overlay id="downloadOverlay" with-backdrop>
       <gr-download-dialog
           change="[[_change]]"
-          patch-num="[[_patchNum]]"
+          patch-num="[[_patchRange.patchNum]]"
           config="[[serverConfig.download]]"
           on-close="_handleDownloadDialogClose"></gr-download-dialog>
     </gr-overlay>
@@ -303,7 +299,8 @@
         with-backdrop>
       <gr-reply-dialog id="replyDialog"
           change-num="[[_changeNum]]"
-          patch-num="[[_patchNum]]"
+          patch-num="[[_patchRange.patchNum]]"
+          revisions="[[_change.revisions]]"
           labels="[[_change.labels]]"
           permitted-labels="[[_change.permitted_labels]]"
           diff-drafts="[[_diffDrafts]]"
@@ -311,6 +308,7 @@
           on-cancel="_handleReplyCancel"
           hidden$="[[!_loggedIn]]">Reply</gr-reply-dialog>
     </gr-overlay>
+    <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-change-view.js"></script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index 27e2cb8..428d067 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -56,7 +56,7 @@
       _commitInfo: Object,
       _changeNum: String,
       _diffDrafts: Object,
-      _patchNum: String,
+      _patchRange: Object,
       _allPatchSets: {
         type: Array,
         computed: '_computeAllPatchSets(_change)',
@@ -171,46 +171,73 @@
     },
 
     _paramsChanged: function(value) {
-      if (value.view != this.tagName.toLowerCase()) { return; }
+      if (value.view !== this.tagName.toLowerCase()) { return; }
 
       this._changeNum = value.changeNum;
-      this._patchNum = value.patchNum;
-      if (this.viewState.changeNum != this._changeNum ||
-          this.viewState.patchNum != this._patchNum) {
-        this.set('viewState.selectedFileIndex', 0);
-        this.set('viewState.changeNum', this._changeNum);
-        this.set('viewState.patchNum', this._patchNum);
+      this._patchRange = {
+        patchNum: value.patchNum,
+        basePatchNum: value.basePatchNum || 'PARENT',
+      };
+
+      // If the change number or patch range is different, then reset the
+      // selected file index.
+      var patchRangeState = this.viewState.patchRange;
+      if (this.viewState.changeNum !== this._changeNum ||
+          patchRangeState.basePatchNum !== this._patchRange.basePatchNum ||
+          patchRangeState.patchNum !== this._patchRange.patchNum) {
+        this._resetFileListViewState();
       }
-      if (!this._changeNum) {
-        return;
-      }
+
       this._reload().then(function() {
         this.$.messageList.topMargin = this._headerEl.offsetHeight;
+        this.$.fileList.topMargin = this._headerEl.offsetHeight;
 
         // Allow the message list to render before scrolling.
         this.async(function() {
-          var msgPrefix = '#message-';
-          var hash = window.location.hash;
-          if (hash.indexOf(msgPrefix) == 0) {
-            this.$.messageList.scrollToMessage(hash.substr(msgPrefix.length));
-          }
+          this._maybeScrollToMessage();
         }.bind(this), 1);
 
-        this._getLoggedIn().then(function(loggedIn) {
-          if (!loggedIn) { return; }
+        this._maybeShowReplyDialog();
 
-          if (this.viewState.showReplyDialog) {
-            this.$.replyOverlay.open();
-            this.set('viewState.showReplyDialog', false);
-          }
-        }.bind(this));
+        this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.SHOW_CHANGE, {
+          change: this._change,
+          patchNum: this._patchRange.patchNum,
+        });
       }.bind(this));
     },
 
+    _maybeScrollToMessage: function() {
+      var msgPrefix = '#message-';
+      var hash = window.location.hash;
+      if (hash.indexOf(msgPrefix) === 0) {
+        this.$.messageList.scrollToMessage(hash.substr(msgPrefix.length));
+      }
+    },
+
+    _maybeShowReplyDialog: function() {
+      this._getLoggedIn().then(function(loggedIn) {
+        if (!loggedIn) { return; }
+
+        if (this.viewState.showReplyDialog) {
+          this.$.replyOverlay.open();
+          this.set('viewState.showReplyDialog', false);
+        }
+      }.bind(this));
+    },
+
+    _resetFileListViewState: function() {
+      this.set('viewState.selectedFileIndex', 0);
+      this.set('viewState.changeNum', this._changeNum);
+      this.set('viewState.patchRange', this._patchRange);
+    },
+
     _changeChanged: function(change) {
       if (!change) { return; }
-      this._patchNum = this._patchNum ||
-          change.revisions[change.current_revision]._number;
+      this.set('_patchRange.basePatchNum',
+          this._patchRange.basePatchNum || 'PARENT');
+      this.set('_patchRange.patchNum',
+          this._patchRange.patchNum ||
+              change.revisions[change.current_revision]._number);
 
       var title = change.subject + ' (' + change.change_id.substr(0, 9) + ')';
       this.fire('title-change', {title: title});
@@ -363,7 +390,7 @@
 
     _getCommitInfo: function() {
       return this.$.restAPI.getChangeCommitInfo(
-          this._changeNum, this._patchNum).then(
+          this._changeNum, this._patchRange.patchNum).then(
               function(commitInfo) {
                 this._commitInfo = commitInfo;
               }.bind(this));
@@ -393,8 +420,6 @@
       this._getComments();
 
       var reloadPatchNumDependentResources = function() {
-        if (!this._change) { return Promise.resolve(); }
-
         return Promise.all([
           this._getCommitInfo(),
           this.$.actions.reload(),
@@ -412,7 +437,7 @@
 
       this._resetHeaderEl();
 
-      if (this._patchNum) {
+      if (this._patchRange.patchNum) {
         return reloadPatchNumDependentResources().then(function() {
           return detailCompletes;
         }).then(reloadDetailDependentResources);
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index ee08ba1..7fcbd32 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -83,7 +83,10 @@
 
     test('patch num change', function(done) {
       element._changeNum = '42';
-      element._patchNum = 2;
+      element._patchRange = {
+        basePatchNum: 'PARENT',
+        patchNum: 2,
+      };
       element._change = {
         change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
         revisions: {
@@ -133,7 +136,10 @@
 
     test('change status new', function() {
       element._changeNum = '1';
-      element._patchNum = 1;
+      element._patchRange = {
+        basePatchNum: 'PARENT',
+        patchNum: 1,
+      };
       element._change = {
         change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
         revisions: {
@@ -149,7 +155,10 @@
 
     test('change status draft', function() {
       element._changeNum = '1';
-      element._patchNum = 1;
+      element._patchRange = {
+        basePatchNum: 'PARENT',
+        patchNum: 1,
+      };
       element._change = {
         change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
         revisions: {
@@ -165,7 +174,10 @@
 
     test('revision status draft', function() {
       element._changeNum = '1';
-      element._patchNum = 2;
+      element._patchRange = {
+        basePatchNum: 'PARENT',
+        patchNum: 1,
+      };
       element._change = {
         change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
         revisions: {
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js
index f27e4e2..97342d1 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js
@@ -32,6 +32,16 @@
     properties: {
       branch: String,
       message: String,
+      commitInfo: {
+        type: Object,
+        readOnly: true,
+        observer: '_commitInfoChanged',
+      },
+    },
+
+    _commitInfoChanged: function(commitInfo) {
+      // Pre-populate cherry-pick message for editing from commit info.
+      this.message = commitInfo.message;
     },
 
     _handleConfirmTap: function(e) {
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html
new file mode 100644
index 0000000..a3ab470
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html
@@ -0,0 +1,66 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../shared/gr-confirm-dialog/gr-confirm-dialog.html">
+
+<dom-module id="gr-confirm-revert-dialog">
+  <template>
+    <style>
+      :host {
+        display: block;
+        width: 30em;
+      }
+      :host([disabled]) {
+        opacity: .5;
+        pointer-events: none;
+      }
+      label {
+        cursor: pointer;
+      }
+      iron-autogrow-textarea {
+        padding: 0;
+      }
+      .main label,
+      .main input[type="text"] {
+        display: block;
+        font: inherit;
+        width: 100%;
+      }
+      .main .message {
+        border: groove;
+        width: 100%;
+      }
+    </style>
+    <gr-confirm-dialog
+        confirm-label="Revert"
+        on-confirm="_handleConfirmTap"
+        on-cancel="_handleCancelTap">
+      <div class="header">Revert Merged Change</div>
+      <div class="main">
+        <label for="messageInput">
+          Revert Commit Message
+        </label>
+        <iron-autogrow-textarea
+            id="messageInput"
+            class="message"
+            bind-value="{{message}}"></iron-autogrow-textarea>
+      </div>
+    </gr-confirm-dialog>
+  </template>
+  <script src="gr-confirm-revert-dialog.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
new file mode 100644
index 0000000..5371ce5
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
@@ -0,0 +1,65 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-confirm-revert-dialog',
+
+    /**
+     * Fired when the confirm button is pressed.
+     *
+     * @event confirm
+     */
+
+    /**
+     * Fired when the cancel button is pressed.
+     *
+     * @event cancel
+     */
+
+    properties: {
+      branch: String,
+      message: String,
+      commitInfo: {
+        type: Object,
+        readOnly: true,
+        observer: '_commitInfoChanged',
+      },
+    },
+
+    _commitInfoChanged: function(commitInfo) {
+      // Strip 'Change-Id: xxx'
+      var commitMessage = commitInfo.message.replace(
+          /\n{1,2}\nChange-Id: \w+\n/gm, '');
+      var revertCommitText = 'This reverts commit ';
+      // Selector for previous revert text and commit.
+      var previousRevertText =
+          new RegExp('\n{1,2}' + revertCommitText + '\\w+.\n*', 'gm');
+      commitMessage = commitMessage.replace(previousRevertText, '');
+      this.message = 'Revert "' + commitMessage + '"\n\n' +
+          revertCommitText + commitInfo.commit + '.';
+    },
+
+    _handleConfirmTap: function(e) {
+      e.preventDefault();
+      this.fire('confirm', null, {bubbles: false});
+    },
+
+    _handleCancelTap: function(e) {
+      e.preventDefault();
+      this.fire('cancel', null, {bubbles: false});
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
index d94a828..97e5bdc 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
@@ -16,7 +16,8 @@
 
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html">
-<link rel="import" href="../../shared/gr-request/gr-request.html">
+<link rel="import" href="../../diff/gr-diff/gr-diff.html">
+<link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
 <dom-module id="gr-file-list">
@@ -29,10 +30,15 @@
         display: flex;
         padding: .1em .25em;
       }
-      .header {
+      header {
+        display: flex;
         font-weight: bold;
+        justify-content: space-between;
+        margin-bottom: .5em;
       }
-      .positionIndicator,
+      .rightControls {
+        font-weight: normal;
+      }
       .reviewed,
       .status {
         align-items: center;
@@ -40,34 +46,33 @@
       }
       .reviewed,
       .status {
-        justify-content: center;
+        display: inline-block;
+        text-align: center;
         width: 1.5em;
       }
-      .positionIndicator {
-        justify-content: flex-start;
-        visibility: hidden;
-        width: 1.25em;
-      }
       .row:not(.header):hover {
         background-color: #f5fafd;
       }
       .row[selected] {
         background-color: #ebf5fb;
       }
-      .row[selected] .positionIndicator {
-        visibility: visible;
-      }
       .path {
         flex: 1;
-        overflow: hidden;
         padding-left: .35em;
         text-decoration: none;
-        text-overflow: ellipsis;
         white-space: nowrap;
       }
-      .row:not(.header) .path:hover {
+      .path:hover :first-child {
         text-decoration: underline;
       }
+      .path,
+      .path div {
+        overflow: hidden;
+        text-overflow: ellipsis;
+      }
+      .oldPath {
+        color: #999;
+      }
       .comments,
       .stats {
         text-align: right;
@@ -90,18 +95,19 @@
       .removed {
         color: #D32F2F;
       }
-      .reviewed input[type="checkbox"] {
-        display: inline-block;
-      }
       .drafts {
         color: #C62828;
         font-weight: bold;
       }
+      gr-diff {
+        box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
+        display: block;
+        margin: .25em 0 1em;
+      }
       @media screen and (max-width: 50em) {
         .row[selected] {
           background-color: transparent;
         }
-        .positionIndicator,
         .stats {
           display: none;
         }
@@ -114,17 +120,29 @@
         }
       }
     </style>
-    <div class="row header">
-      <div class="positionIndicator"></div>
-      <div class="reviewed" hidden$="[[!_loggedIn]]" hidden></div>
-      <div class="status"></div>
-      <div class="path">Path</div>
-      <div class="comments">Comments</div>
-      <div class="stats">Stats</div>
-    </div>
-    <template is="dom-repeat" items="{{_files}}" as="file">
+    <header>
+      <div>Files</div>
+      <div class="rightControls">
+        <gr-button link on-tap="_expandAllDiffs">Show diffs</gr-button>
+        /
+        <gr-button link on-tap="_collapseAllDiffs">Hide diffs</gr-button>
+        /
+        <label>
+          Diff against
+          <select on-change="_handlePatchChange">
+            <option value="PARENT">Base</option>
+            <template is="dom-repeat" items="[[_computePatchSets(revisions, patchRange.*)]]" as="patchNum">
+              <option
+                  value$="[[patchNum]]"
+                  selected$="[[_computePatchSetSelected(patchNum, patchRange.basePatchNum)]]"
+                  disabled$="[[_computePatchSetDisabled(patchNum, patchRange.patchNum)]]">[[patchNum]]</option>
+            </template>
+          </select>
+        </label>
+      </div>
+    </header>
+    <template is="dom-repeat" items="[[_files]]" as="file">
       <div class="row" selected$="[[_computeFileSelected(index, selectedIndex)]]">
-        <div class="positionIndicator">&#x25b6;</div>
         <div class="reviewed" hidden$="[[!_loggedIn]]" hidden>
           <input type="checkbox" checked$="[[_computeReviewed(file, _reviewed)]]"
               data-path$="[[file.__path]]" on-change="_handleReviewedChange">
@@ -132,18 +150,31 @@
         <div class$="[[_computeClass('status', file.__path)]]">
           [[_computeFileStatus(file.status)]]
         </div>
-        <a class="path" href$="[[_computeDiffURL(changeNum, patchNum, file.__path)]]">
-          [[_computeFileDisplayName(file.__path)]]
+        <a class="path" href$="[[_computeDiffURL(changeNum, patchRange, file.__path)]]">
+          <div title$="[[_computeFileDisplayName(file.__path)]]">
+            [[_computeFileDisplayName(file.__path)]]
+          </div>
+          <div class="oldPath" hidden$="[[!file.old_path]]" hidden
+              title$="[[file.old_path]]">
+            [[file.old_path]]
+          </div>
         </a>
         <div class="comments">
-          <span class="drafts">[[_computeDraftsString(drafts, patchNum, file.__path)]]</span>
-          [[_computeCommentsString(comments, patchNum, file.__path)]]
+          <span class="drafts">[[_computeDraftsString(drafts, patchRange.patchNum, file.__path)]]</span>
+          [[_computeCommentsString(comments, patchRange.patchNum, file.__path)]]
         </div>
         <div class$="[[_computeClass('stats', file.__path)]]">
           <span class="added">+[[file.lines_inserted]]</span>
           <span class="removed">-[[file.lines_deleted]]</span>
         </div>
       </div>
+      <gr-diff hidden
+          change-num="[[changeNum]]"
+          patch-range="[[patchRange]]"
+          path="[[file.__path]]"
+          prefs="[[_diffPrefs]]"
+          project-config="[[projectConfig]]"
+          view-mode="[[_userPrefs.diff_view]]"></gr-diff>
     </template>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index 39e50a9..3a6930e 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -20,10 +20,14 @@
     is: 'gr-file-list',
 
     properties: {
+      patchRange: Object,
       patchNum: String,
       changeNum: String,
       comments: Object,
       drafts: Object,
+      revisions: Object,
+      projectConfig: Object,
+      topMargin: Number,
       selectedIndex: {
         type: Number,
         notify: true,
@@ -42,6 +46,9 @@
         type: Array,
         value: function() { return []; },
       },
+      _diffPrefs: Object,
+      _userPrefs: Object,
+      _showInlineDiffs: Boolean,
     },
 
     behaviors: [
@@ -49,10 +56,12 @@
     ],
 
     reload: function() {
-      if (!this.changeNum || !this.patchNum) {
+      if (!this.changeNum || !this.patchRange.patchNum) {
         return Promise.resolve();
       }
 
+      this._collapseAllDiffs();
+
       var promises = [];
       var _this = this;
 
@@ -69,7 +78,65 @@
         });
       }));
 
-      return Promise.all(promises);
+      promises.push(this._getDiffPreferences().then(function(prefs) {
+        this._diffPrefs = prefs;
+      }.bind(this)));
+
+      promises.push(this._getPreferences().then(function(prefs) {
+        this._userPrefs = prefs;
+      }.bind(this)));
+    },
+
+    _getDiffPreferences: function() {
+      return this.$.restAPI.getDiffPreferences();
+    },
+
+    _getPreferences: function() {
+      return this.$.restAPI.getPreferences();
+    },
+
+    _computePatchSets: function(revisions) {
+      var patchNums = [];
+      for (var commit in revisions) {
+        patchNums.push(revisions[commit]._number);
+      }
+      return patchNums.sort(function(a, b) { return a - b; });
+    },
+
+    _computePatchSetDisabled: function(patchNum, currentPatchNum) {
+      return parseInt(patchNum, 10) >= parseInt(currentPatchNum, 10);
+    },
+
+    _computePatchSetSelected: function(patchNum, basePatchNum) {
+      return parseInt(patchNum, 10) === parseInt(basePatchNum, 10);
+    },
+
+    _handlePatchChange: function(e) {
+      this.set('patchRange.basePatchNum', Polymer.dom(e).rootTarget.value);
+      page.show('/c/' + encodeURIComponent(this.changeNum) + '/' +
+          encodeURIComponent(this._patchRangeStr(this.patchRange)));
+    },
+
+    _forEachDiff: function(fn) {
+      var diffs = Polymer.dom(this.root).querySelectorAll('gr-diff');
+      for (var i = 0; i < diffs.length; i++) {
+        fn(diffs[i]);
+      }
+    },
+
+    _expandAllDiffs: function() {
+      this._showInlineDiffs = true;
+      this._forEachDiff(function(diff) {
+        diff.hidden = false;
+        diff.reload();
+      });
+    },
+
+    _collapseAllDiffs: function() {
+      this._showInlineDiffs = false;
+      this._forEachDiff(function(diff) {
+        diff.hidden = true;
+      });
     },
 
     _computeCommentsString: function(comments, patchNum, path) {
@@ -113,8 +180,8 @@
     },
 
     _saveReviewedState: function(path, reviewed) {
-      return this.$.restAPI.saveFileReviewed(this.changeNum, this.patchNum,
-          path, reviewed);
+      return this.$.restAPI.saveFileReviewed(this.changeNum,
+          this.patchRange.patchNum, path, reviewed);
     },
 
     _getLoggedIn: function() {
@@ -122,39 +189,34 @@
     },
 
     _getReviewedFiles: function() {
-      return this.$.restAPI.getReviewedFiles(this.changeNum, this.patchNum);
+      return this.$.restAPI.getReviewedFiles(this.changeNum,
+          this.patchRange.patchNum);
     },
 
     _getFiles: function() {
-      return this.$.restAPI.getChangeFiles(this.changeNum, this.patchNum).then(
-          this._normalizeFilesResponse.bind(this));
-    },
-
-    _normalizeFilesResponse: function(response) {
-      var paths = Object.keys(response).sort();
-      var files = [];
-      for (var i = 0; i < paths.length; i++) {
-        var info = response[paths[i]];
-        info.__path = paths[i];
-        info.lines_inserted = info.lines_inserted || 0;
-        info.lines_deleted = info.lines_deleted || 0;
-        files.push(info);
-      }
-      return files;
+      return this.$.restAPI.getChangeFilesAsSpeciallySortedArray(
+          this.changeNum, this.patchRange);
     },
 
     _handleKey: function(e) {
       if (this.shouldSupressKeyboardShortcut(e)) { return; }
 
       switch (e.keyCode) {
+        case 73:  // 'i'
+          if (!e.shiftKey) { return; }
+          e.preventDefault();
+          this._toggleInlineDiffs();
+          break;
         case 74:  // 'j'
           e.preventDefault();
           this.selectedIndex =
               Math.min(this._files.length - 1, this.selectedIndex + 1);
+          this._scrollToSelectedFile();
           break;
         case 75:  // 'k'
           e.preventDefault();
           this.selectedIndex = Math.max(0, this.selectedIndex - 1);
+          this._scrollToSelectedFile();
           break;
         case 219:  // '['
           e.preventDefault();
@@ -172,14 +234,35 @@
       }
     },
 
+    _toggleInlineDiffs: function() {
+      if (this._showInlineDiffs) {
+        this._collapseAllDiffs();
+      } else {
+        this._expandAllDiffs();
+      }
+    },
+
     _openSelectedFile: function(opt_index) {
       if (opt_index != null) {
         this.selectedIndex = opt_index;
       }
-      page.show(this._computeDiffURL(this.changeNum, this.patchNum,
+      page.show(this._computeDiffURL(this.changeNum, this.patchRange,
           this._files[this.selectedIndex].__path));
     },
 
+    _scrollToSelectedFile: function() {
+      var el = this.$$('.row[selected]');
+      var top = 0;
+      for (var node = el; node; node = node.offsetParent) {
+        top += node.offsetTop;
+      }
+      if (this._showInlineDiffs) {
+        window.scrollTo(0, top - this.topMargin);
+      } else {
+        window.scrollTo(0, top - document.body.clientHeight / 2);
+      }
+    },
+
     _computeFileSelected: function(index, selectedIndex) {
       return index === selectedIndex;
     },
@@ -188,8 +271,19 @@
       return status || 'M';
     },
 
-    _computeDiffURL: function(changeNum, patchNum, path) {
-      return '/c/' + changeNum + '/' + patchNum + '/' + path;
+    _computeDiffURL: function(changeNum, patchRange, path) {
+      return '/c/' +
+          encodeURIComponent(changeNum) +
+          '/' +
+          encodeURIComponent(this._patchRangeStr(patchRange)) +
+          '/' +
+          path;
+    },
+
+    _patchRangeStr: function(patchRange) {
+      return patchRange.basePatchNum !== 'PARENT' ?
+          patchRange.basePatchNum + '..' + patchRange.patchNum :
+          patchRange.patchNum + '';
     },
 
     _computeFileDisplayName: function(path) {
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index 20add83..055f961 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -35,17 +35,12 @@
 <script>
   suite('gr-file-list tests', function() {
     var element;
-    var getLoggedInStub;
 
     setup(function() {
-      element = fixture('basic');
-      getLoggedInStub = sinon.stub(element, '_getLoggedIn', function() {
-        return Promise.resolve(true);
+      stub('gr-rest-api-interface', {
+        getLoggedIn: function() { return Promise.resolve(true); },
       });
-    });
-
-    teardown(function() {
-      getLoggedInStub.restore();
+      element = fixture('basic');
     });
 
     test('get file list', function(done) {
@@ -77,18 +72,27 @@
           __path: 'tags.html',
         });
 
+        getChangeFilesStub.restore();
         done();
       });
     });
 
     test('keyboard shortcuts', function() {
+      var toggleInlineDiffsStub = sinon.stub(element, '_toggleInlineDiffs');
+      MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift');  // 'I'
+      assert.isTrue(toggleInlineDiffsStub.calledOnce);
+      toggleInlineDiffsStub.restore();
+
       element._files = [
         {__path: '/COMMIT_MSG'},
         {__path: 'file_added_in_rev2.txt'},
         {__path: 'myfile.txt'},
       ];
-      element.changeNum = '42',
-      element.patchNum = '2';
+      element.changeNum = '42';
+      element.patchRange = {
+        basePatchNum: 'PARENT',
+        patchNum: '2',
+      };
       element.selectedIndex = 0;
 
       flushAsynchronousOperations();
@@ -180,7 +184,10 @@
       ];
       element._reviewed = ['/COMMIT_MSG', 'myfile.txt'];
       element.changeNum = '42';
-      element.patchNum = '2';
+      element.patchRange = {
+        basePatchNum: 'PARENT',
+        patchNum: '2',
+      };
       element.selectedIndex = 0;
 
       flushAsynchronousOperations();
@@ -204,5 +211,52 @@
 
       saveStub.restore();
     });
+
+    test('patch set from revisions', function() {
+      var patchNums = element._computePatchSets({
+        rev3: {_number: 3},
+        rev1: {_number: 1},
+        rev4: {_number: 4},
+        rev2: {_number: 2},
+      });
+      assert.deepEqual(patchNums, [1, 2, 3, 4]);
+    });
+
+    test('patch range string', function() {
+      assert.equal(
+          element._patchRangeStr({basePatchNum: 'PARENT', patchNum: '1'}),
+          '1');
+      assert.equal(
+          element._patchRangeStr({basePatchNum: '1', patchNum: '3'}),
+          '1..3');
+    });
+
+    test('diff against dropdown', function(done) {
+      var showStub = sinon.stub(page, 'show');
+      element.changeNum = '42';
+      element.patchRange = {
+        basePatchNum: 'PARENT',
+        patchNum: '3',
+      };
+      element.revisions = {
+        rev1: {_number: 1},
+        rev2: {_number: 2},
+        rev3: {_number: 3},
+      };
+      flush(function() {
+        var selectEl = element.$$('select');
+        assert.equal(selectEl.value, 'PARENT');
+        assert.isTrue(element.$$('option[value="3"]').hasAttribute('disabled'));
+        selectEl.addEventListener('change', function() {
+          assert.equal(selectEl.value, '2');
+          assert(showStub.lastCall.calledWithExactly('/c/42/2..3'),
+              'Should navigate to /c/42/2..3');
+          showStub.restore();
+          done();
+        });
+        selectEl.value = '2';
+        element.fire('change', {}, {node: selectEl});
+      });
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html
index e93d008..daa33cd 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html
@@ -16,7 +16,7 @@
 
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/rest-client-behavior.html">
-<link rel="import" href="../../shared/gr-ajax/gr-ajax.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
 <dom-module id="gr-related-changes-list">
   <template>
@@ -33,6 +33,16 @@
       a {
         display: block;
       }
+      .changeContainer,
+      a {
+        max-width: 100%;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+      }
+      .changeContainer {
+        display: flex;
+      }
       .relatedChanges a {
         display: inline-block;
       }
@@ -43,6 +53,7 @@
       .status {
         color: #666;
         font-weight: bold;
+        margin-left: .25em;
       }
       .notCurrent {
         color: #e65100;
@@ -57,30 +68,11 @@
         display: none;
       }
     </style>
-    <gr-ajax id="relatedXHR"
-        url="[[_computeRelatedURL(change._number, patchNum)]]"
-        last-response="{{_relatedResponse}}"></gr-ajax>
-    <gr-ajax id="submittedTogetherXHR"
-        url="[[_computeSubmittedTogetherURL(change._number)]]"
-        last-response="{{_submittedTogether}}"></gr-ajax>
-    <gr-ajax id="conflictsXHR"
-        url="/changes/"
-        params="[[_computeConflictsQueryParams(change._number)]]"
-        last-response="{{_conflicts}}"></gr-ajax>
-    <gr-ajax id="cherryPicksXHR"
-        url="/changes/"
-        params="[[_computeCherryPicksQueryParams(change.project, change.change_id, change._number)]]"
-        last-response="{{_cherryPicks}}"></gr-ajax>
-    <gr-ajax id="sameTopicXHR"
-        url="/changes/"
-        params="[[_computeSameTopicQueryParams(change.topic)]]"
-        last-response="{{_sameTopic}}"></gr-ajax>
-
     <div hidden$="[[!_loading]]">Loading...</div>
     <section class="relatedChanges" hidden$="[[!_relatedResponse.changes.length]]" hidden>
-      <h4>Relation Chain</h4>
+      <h4>Relation chain</h4>
       <template is="dom-repeat" items="[[_relatedResponse.changes]]" as="change">
-        <div>
+        <div class="changeContainer">
           <a href$="[[_computeChangeURL(change._change_number, change._revision_number)]]"
               class$="[[_computeLinkClass(change)]]">
             [[change.commit.subject]]
@@ -127,6 +119,7 @@
         </a>
       </template>
     </section>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-related-changes-list.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
index f3a298e..287765b 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
@@ -20,10 +20,6 @@
     properties: {
       change: Object,
       patchNum: String,
-      serverConfig: {
-        type: Object,
-        observer: '_serverConfigChanged',
-      },
       hidden: {
         type: Boolean,
         value: false,
@@ -31,15 +27,6 @@
       },
 
       _loading: Boolean,
-      _resolveServerConfigReady: Function,
-      _serverConfigReady: {
-        type: Object,
-        value: function() {
-          return new Promise(function(resolve) {
-            this._resolveServerConfigReady = resolve;
-          }.bind(this));
-        }
-      },
       _connectedRevisions: {
         type: Array,
         computed: '_computeConnectedRevisions(change, patchNum, ' +
@@ -67,72 +54,58 @@
       }
       this._loading = true;
       var promises = [
-        this.$.relatedXHR.generateRequest().completes,
-        this.$.submittedTogetherXHR.generateRequest().completes,
-        this.$.conflictsXHR.generateRequest().completes,
-        this.$.cherryPicksXHR.generateRequest().completes,
+        this._getRelatedChanges().then(function(response) {
+          this._relatedResponse = response;
+        }.bind(this)),
+        this._getSubmittedTogether().then(function(response) {
+          this._submittedTogether = response;
+        }.bind(this)),
+        this._getConflicts().then(function(response) {
+          this._conflicts = response;
+        }.bind(this)),
+        this._getCherryPicks().then(function(response) {
+          this._cherryPicks = response;
+        }.bind(this)),
       ];
 
-      return this._serverConfigReady.then(function() {
-        if (this.change.topic &&
-            !this.serverConfig.change.submit_whole_topic) {
-          return this.$.sameTopicXHR.generateRequest().completes;
+      return this._getServerConfig().then(function(config) {
+        if (this.change.topic && !config.change.submit_whole_topic) {
+          return this._getChangesWithSameTopic().then(function(response) {
+            this._sameTopic = response;
+          }.bind(this));
         } else {
-          this._sameTopic = [];
+         this._sameTopic = [];
         }
-        return Promise.resolve();
+        return this._sameTopic;
       }.bind(this)).then(Promise.all(promises)).then(function() {
         this._loading = false;
       }.bind(this));
     },
 
-    _computeRelatedURL: function(changeNum, patchNum) {
-      return this.changeBaseURL(changeNum, patchNum) + '/related';
+    _getRelatedChanges: function() {
+      return this.$.restAPI.getRelatedChanges(this.change._number,
+          this.patchNum);
     },
 
-    _computeSubmittedTogetherURL: function(changeNum) {
-      return this.changeBaseURL(changeNum) + '/submitted_together';
+    _getSubmittedTogether: function() {
+      return this.$.restAPI.getChangesSubmittedTogether(this.change._number);
     },
 
-    _computeConflictsQueryParams: function(changeNum) {
-      var options = this.listChangesOptionsToHex(
-          this.ListChangesOption.CURRENT_REVISION,
-          this.ListChangesOption.CURRENT_COMMIT
-      );
-      return {
-        O: options,
-        q: 'status:open is:mergeable conflicts:' + changeNum,
-      };
+    _getServerConfig: function() {
+      return this.$.restAPI.getConfig();
     },
 
-    _computeCherryPicksQueryParams: function(project, changeID, changeNum) {
-      var options = this.listChangesOptionsToHex(
-          this.ListChangesOption.CURRENT_REVISION,
-          this.ListChangesOption.CURRENT_COMMIT
-      );
-      var query = [
-        'project:' + project,
-        'change:' + changeID,
-        '-change:' + changeNum,
-        '-is:abandoned',
-      ].join(' ');
-      return {
-        O: options,
-        q: query
-      }
+    _getConflicts: function() {
+      return this.$.restAPI.getChangeConflicts(this.change._number);
     },
 
-    _computeSameTopicQueryParams: function(topic) {
-      var options = this.listChangesOptionsToHex(
-          this.ListChangesOption.LABELS,
-          this.ListChangesOption.CURRENT_REVISION,
-          this.ListChangesOption.CURRENT_COMMIT,
-          this.ListChangesOption.DETAILED_LABELS
-      );
-      return {
-        O: options,
-        q: 'status:open topic:' + topic,
-      };
+    _getCherryPicks: function() {
+      return this.$.restAPI.getChangeCherryPicks(this.change.project,
+          this.change.change_id, this.change._number);
+    },
+
+    _getChangesWithSameTopic: function() {
+      return this.$.restAPI.getChangesWithSameTopic(this.change.topic);
     },
 
     _computeChangeURL: function(changeNum, patchNum) {
@@ -179,11 +152,7 @@
       } else if (change.submittable) {
         return 'Submittable';
       }
-      return ''
-    },
-
-    _serverConfigChanged: function(config) {
-      this._resolveServerConfigReady(config);
+      return '';
     },
 
     _resultsChanged: function(related, submittedTogether, conflicts,
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
index ad2b925..7fb998b 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
@@ -19,7 +19,6 @@
 <link rel="import" href="../../../bower_components/iron-selector/iron-selector.html">
 <link rel="import" href="../../../behaviors/rest-client-behavior.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-request/gr-request.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
 <dom-module id="gr-reply-dialog">
@@ -60,6 +59,9 @@
         border: none;
         width: 100%;
       }
+      .labelsNotShown {
+        color: #666;
+      }
       .labelContainer:not(:first-of-type) {
         margin-top: .5em;
       }
@@ -113,19 +115,28 @@
             bind-value="{{draft}}"></iron-autogrow-textarea>
       </section>
       <section class="labelsContainer">
-        <template is="dom-repeat"
-            items="[[_computeLabelArray(permittedLabels)]]" as="label">
-          <div class="labelContainer">
-            <span class="labelName">[[label]]</span>
-            <iron-selector data-label$="[[label]]"
-                selected="[[_computeIndexOfLabelValue(labels, permittedLabels, label, _account)]]">
-              <template is="dom-repeat"
-                  items="[[_computePermittedLabelValues(permittedLabels, label)]]"
-                  as="value">
-                <gr-button data-value$="[[value]]">[[value]]</gr-button>
-              </template>
-            </iron-selector>
-          </div>
+        <template is="dom-if" if="[[_computeShowLabels(patchNum, revisions)]]">
+          <template is="dom-repeat"
+              items="[[_computeLabelArray(permittedLabels)]]" as="label">
+            <div class="labelContainer">
+              <span class="labelName">[[label]]</span>
+              <iron-selector data-label$="[[label]]"
+                  selected="[[_computeIndexOfLabelValue(labels, permittedLabels, label, _account)]]">
+                <template is="dom-repeat"
+                    items="[[_computePermittedLabelValues(permittedLabels, label)]]"
+                    as="value">
+                  <gr-button has-tooltip data-value$="[[value]]"
+                      title$="[[_computeLabelValueTitle(labels, label, value)]]">[[value]]</gr-button>
+                </template>
+              </iron-selector>
+            </div>
+          </template>
+        </template>
+        <template is="dom-if" if="[[!_computeShowLabels(patchNum, revisions)]]">
+          <span class="labelsNotShown">
+            Labels are not shown because this is not the most recent patch set.
+            <a href$="/c/[[changeNum]]">Go to the latest patch set.</a>
+          </span>
         </template>
       </section>
       <section class="draftsContainer" hidden$="[[_computeHideDraftList(diffDrafts)]]">
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index 1cd0ce2..ae57be3 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -32,6 +32,7 @@
     properties: {
       changeNum: String,
       patchNum: String,
+      revisions: Object,
       disabled: {
         type: Boolean,
         value: false,
@@ -46,7 +47,6 @@
       permittedLabels: Object,
 
       _account: Object,
-      _xhrPromise: Object,  // Used for testing.
     },
 
     behaviors: [
@@ -65,6 +65,16 @@
       }.bind(this));
     },
 
+    _computeShowLabels: function(patchNum, revisions) {
+      var num = parseInt(patchNum, 10);
+      for (var rev in revisions) {
+        if (revisions[rev]._number > num) {
+          return false;
+        }
+      }
+      return true;
+    },
+
     _computeHideDraftList: function(drafts) {
       return Object.keys(drafts || {}).length == 0;
     },
@@ -79,6 +89,10 @@
       if (total > 1) { return total + ' Drafts'; }
     },
 
+    _computeLabelValueTitle: function(labels, label, value) {
+      return labels[label] && labels[label].values[value];
+    },
+
     _computeLabelArray: function(labelsObj) {
       return Object.keys(labelsObj).sort();
     },
@@ -140,26 +154,21 @@
         obj.message = this.draft;
       }
       this.disabled = true;
-      this._send(obj).then(function(req) {
-        this.fire('send', null, {bubbles: false});
-        this.draft = '';
+      this._saveReview(obj).then(function(response) {
         this.disabled = false;
+        if (!response.ok) { return response; }
+
+        this.draft = '';
+        this.fire('send', null, {bubbles: false});
       }.bind(this)).catch(function(err) {
-        alert('Oops. Something went wrong. Check the console and bug the ' +
-            'PolyGerrit team for assistance.');
+        this.disabled = false;
         throw err;
       }.bind(this));
     },
 
-    _send: function(payload) {
-      var xhr = document.createElement('gr-request');
-      this._xhrPromise = xhr.send({
-        method: 'POST',
-        url: this.changeBaseURL(this.changeNum, this.patchNum) + '/review',
-        body: payload,
-      });
-
-      return this._xhrPromise;
+    _saveReview: function(review) {
+      return this.$.restAPI.saveChangeReview(this.changeNum, this.patchNum,
+          review);
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
index fb2de6a..cbef78d 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
@@ -34,11 +34,10 @@
 <script>
   suite('gr-reply-dialog tests', function() {
     var element;
-    var server;
 
     setup(function() {
-      stub('gr-reply-dialog', {
-        _getAccount: function() { return Promise.resolve({}); },
+      stub('gr-rest-api-interface', {
+        getAccount: function() { return Promise.resolve({}); },
       });
       element = fixture('basic');
       element.changeNum = 42;
@@ -76,37 +75,30 @@
         ]
       };
 
-      server = sinon.fakeServer.create();
-      server.respondWith(
-        'POST',
-        '/changes/42/revisions/1/review',
-        [
-          200,
-          {'Content-Type': 'application/json'},
-          ')]}\'\n' +
-          '{' +
-            '"labels": {' +
-              '"Code-Review": -1,' +
-              '"Verified": -1' +
-            '}' +
-          '}'
-        ]
-      );
-
       // Allow the elements created by dom-repeat to be stamped.
       flushAsynchronousOperations();
     });
 
-    teardown(function() {
-      server.restore();
-    });
-
     test('cancel event', function(done) {
       element.addEventListener('cancel', function() { done(); });
       MockInteractions.tap(element.$$('.cancel'));
     });
 
+    test('show/hide labels', function() {
+      var revisions = {
+        rev1: {_number: 1},
+        rev2: {_number: 2},
+      };
+      assert.isFalse(element._computeShowLabels('1', revisions));
+      assert.isTrue(element._computeShowLabels('2', revisions));
+    });
+
     test('label picker', function(done) {
+      var showLabelsStub = sinon.stub(element, '_computeShowLabels',
+          function() { return true; });
+      element.revisions = {};
+      element.patchNum = '';
+
       // Async tick is needed because iron-selector content is distributed and
       // distributed content requires an observer to be set up.
       flush(function() {
@@ -122,32 +114,33 @@
             'iron-selector[data-label="Verified"] > ' +
             'gr-button[data-value="-1"]'));
 
+        var saveReviewStub = sinon.stub(element, '_saveReview',
+            function(review) {
+          assert.deepEqual(review, {
+            drafts: 'PUBLISH_ALL_REVISIONS',
+            labels: {
+              'Code-Review': -1,
+              'Verified': -1
+            },
+            message: 'I wholeheartedly disapprove'
+          });
+          return Promise.resolve({ok: true});
+        });
+
+        element.addEventListener('send', function() {
+          assert.isFalse(element.disabled,
+              'Element should be enabled when done sending reply.');
+          assert.equal(element.draft.length, 0);
+          saveReviewStub.restore();
+          showLabelsStub.restore();
+          done();
+        });
+
         // This is needed on non-Blink engines most likely due to the ways in
         // which the dom-repeat elements are stamped.
         flush(function() {
           MockInteractions.tap(element.$$('.send'));
           assert.isTrue(element.disabled);
-
-          server.respond();
-
-          element._xhrPromise.then(function(req) {
-            assert.isFalse(element.disabled,
-                'Element should be enabled when done sending reply.');
-            assert.equal(req.status, 200);
-            assert.equal(req.url, '/changes/42/revisions/1/review');
-            var reqObj = JSON.parse(req.xhr.requestBody);
-            assert.deepEqual(reqObj, {
-              drafts: 'PUBLISH_ALL_REVISIONS',
-              labels: {
-                'Code-Review': -1,
-                'Verified': -1
-              },
-              message: 'I wholeheartedly disapprove'
-            });
-            assert.equal(req.response.labels['Code-Review'], -1);
-            assert.equal(req.response.labels.Verified, -1);
-            done();
-          });
         });
       });
     });
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
index 6e081ea..3fee424 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
@@ -18,9 +18,8 @@
 <link rel="import" href="../../../bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html">
 <link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html">
-<link rel="import" href="../../shared/gr-ajax/gr-ajax.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-request/gr-request.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
 <dom-module id="gr-reviewer-list">
   <template>
@@ -80,10 +79,6 @@
         }
       }
     </style>
-    <gr-ajax id="autocompleteXHR"
-        url="[[_computeAutocompleteURL(change)]]"
-        params="[[_computeAutocompleteParams(_inputVal)]]"
-        on-response="_handleResponse"></gr-ajax>
     <template is="dom-repeat" items="[[_reviewers]]" as="reviewer">
       <gr-account-chip class="reviewer" account="[[reviewer]]"
           on-remove="_handleRemove"
@@ -119,6 +114,7 @@
       <gr-button link id="addReviewer" class="addReviewer" on-tap="_handleAddTap"
           hidden$="[[_showInput]]">Add reviewer</gr-button>
     </div>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-reviewer-list.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
index 275ab6f..de99039 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
@@ -121,35 +121,10 @@
       return false;
     },
 
-    _computeAutocompleteURL: function(change) {
-      return '/changes/' + change._number + '/suggest_reviewers';
-    },
-
-    _computeAutocompleteParams: function(inputVal) {
-      return {
-        n: 10,  // Return max 10 results
-        q: inputVal,
-      };
-    },
-
     _computeSelected: function(index, selectedIndex) {
       return index == selectedIndex;
     },
 
-    _handleResponse: function(e) {
-      this._autocompleteData = e.detail.response.filter(function(reviewer) {
-        var account = reviewer.account;
-        if (!account) { return true; }
-        for (var i = 0; i < this._reviewers.length; i++) {
-          if (account._account_id == this.change.owner._account_id ||
-              account._account_id == this._reviewers[i]._account_id) {
-            return false;
-          }
-        }
-        return true;
-      }, this);
-    },
-
     _handleBodyClick: function(e) {
       var eventPath = Polymer.dom(e).path;
       for (var i = 0; i < eventPath.length; i++) {
@@ -165,7 +140,12 @@
       e.preventDefault();
       var target = Polymer.dom(e).rootTarget;
       var accountID = parseInt(target.getAttribute('data-account-id'), 10);
-      this._send('DELETE', this._restEndpoint(accountID)).then(function(req) {
+      this.disabled = true;
+      this._xhrPromise =
+          this._removeReviewer(accountID).then(function(response) {
+        this.disabled = false;
+        if (!response.ok) { return response; }
+
         var reviewers = this.change.reviewers;
         ['REVIEWER', 'CC'].forEach(function(type) {
           reviewers[type] = reviewers[type] || [];
@@ -177,8 +157,7 @@
           }
         }, this);
       }.bind(this)).catch(function(err) {
-        alert('Oops. Something went wrong. Check the console and bug the ' +
-            'PolyGerrit team for assistance.');
+        this.disabled = false;
         throw err;
       }.bind(this));
     },
@@ -241,7 +220,8 @@
           return;
         }
         this._lastAutocompleteRequest =
-            this.$.autocompleteXHR.generateRequest();
+            this._getSuggestedReviewers(this.change._number, val).then(
+                this._handleReviewersResponse.bind(this));
       }.bind(this);
 
       this._clearInputRequestHandle();
@@ -253,6 +233,24 @@
       }
     },
 
+    _handleReviewersResponse: function(response) {
+      this._autocompleteData = response.filter(function(reviewer) {
+        var account = reviewer.account;
+        if (!account) { return true; }
+        for (var i = 0; i < this._reviewers.length; i++) {
+          if (account._account_id == this.change.owner._account_id ||
+              account._account_id == this._reviewers[i]._account_id) {
+            return false;
+          }
+        }
+        return true;
+      }, this);
+    },
+
+    _getSuggestedReviewers: function(changeNum, inputVal) {
+      return this.$.restAPI.getChangeSuggestedReviewers(changeNum, inputVal);
+    },
+
     _handleKey: function(e) {
       if (this._hideAutocomplete) {
         if (e.keyCode == 27) {  // 'esc'
@@ -302,43 +300,32 @@
         reviewerID = reviewer.group.id;
       }
       this._autocompleteData = [];
-      this._send('POST', this._restEndpoint(), reviewerID).then(function(req) {
-        this.change.reviewers.CC = this.change.reviewers.CC || [];
-        req.response.reviewers.forEach(function(r) {
-          this.push('change.removable_reviewers', r);
-          this.push('change.reviewers.CC', r);
-        }, this);
-        this._inputVal = '';
-        this.$.input.focus();
+      this.disabled = true;
+      this._xhrPromise = this._addReviewer(reviewerID).then(function(response) {
+        this.change.reviewers['CC'] = this.change.reviewers['CC'] || [];
+        this.disabled = false;
+        if (!response.ok) { return response; }
+
+        return this.$.restAPI.getResponseObject(response).then(function(obj) {
+          obj.reviewers.forEach(function(r) {
+            this.push('change.removable_reviewers', r);
+            this.push('change.reviewers.CC', r);
+          }, this);
+          this._inputVal = '';
+          this.$.input.focus();
+        }.bind(this));
       }.bind(this)).catch(function(err) {
-        // TODO(andybons): Use the message returned by the server.
-        alert('Unable to add ' + reviewerID + ' as a reviewer.');
+        this.disabled = false;
         throw err;
       }.bind(this));
     },
 
-    _send: function(method, url, reviewerID) {
-      this.disabled = true;
-      var request = document.createElement('gr-request');
-      var opts = {
-        method: method,
-        url: url,
-      };
-      if (reviewerID) {
-        opts.body = {reviewer: reviewerID};
-      }
-      this._xhrPromise = request.send(opts);
-      var enableEl = function() { this.disabled = false; }.bind(this);
-      this._xhrPromise.then(enableEl).catch(enableEl);
-      return this._xhrPromise;
+    _addReviewer: function(id) {
+      return this.$.restAPI.addChangeReviewer(this.change._number, id);
     },
 
-    _restEndpoint: function(id) {
-      var path = '/changes/' + this.change._number + '/reviewers';
-      if (id) {
-        path += '/' + id;
-      }
-      return path;
+    _removeReviewer: function(id) {
+      return this.$.restAPI.removeChangeReviewer(this.change._number, id);
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
index eccc72e..9311e91 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
@@ -34,20 +34,12 @@
 <script>
   suite('gr-reviewer-list tests', function() {
     var element;
-    var server;
 
     setup(function() {
       element = fixture('basic');
-
-      server = sinon.fakeServer.create();
-      server.respondWith(
-        'GET',
-        /\/changes\/42\/suggest_reviewers\?n=10&q=andy(.*)/,
-        [
-          200,
-          {'Content-Type': 'application/json'},
-          ')]}\'\n' +
-          JSON.stringify([
+      stub('gr-rest-api-interface', {
+        getChangeSuggestedReviewers: function() {
+          return Promise.resolve([
             {
               account: {
                 _account_id: 1021482,
@@ -68,41 +60,32 @@
                 name: 'andy',
               }
             }
-          ]),
-        ]
-      );
-      server.respondWith(
-        'POST',
-        '/changes/42/reviewers',
-        [
-          200,
-          {'Content-Type': 'application/json'},
-          ')]}\'\n' +
-          JSON.stringify({
-            reviewers: [{
-              _account_id: 1021482,
-              approvals: {
-                'Code-Review': ' 0'
-              },
-              email: 'andybons@chromium.org',
-              name: 'Andrew Bonventre',
-            }]
-          }),
-        ]
-      );
-      server.respondWith(
-        'DELETE',
-        '/changes/42/reviewers/1021482',
-        [
-          204,
-          {'Content-Type': 'application/json'},
-          ')]}\'\n{}',
-        ]
-      );
-    });
-
-    teardown(function() {
-      server.restore();
+          ]);
+        },
+        addChangeReviewer: function() {
+          return Promise.resolve({
+            ok: true,
+            text: function() {
+              return Promise.resolve(
+                ')]}\'\n' +
+                JSON.stringify({
+                  reviewers: [{
+                    _account_id: 1021482,
+                    approvals: {
+                      'Code-Review': ' 0'
+                    },
+                    email: 'andybons@chromium.org',
+                    name: 'Andrew Bonventre',
+                  }]
+                })
+              );
+            },
+          });
+        },
+        removeChangeReviewer: function() {
+          return Promise.resolve({ok: true});
+        },
+      });
     });
 
     test('controls hidden on immutable element', function() {
@@ -176,7 +159,7 @@
       };
       flushAsynchronousOperations();
       var chips =
-        Polymer.dom(element.root).querySelectorAll('gr-account-chip');
+          Polymer.dom(element.root).querySelectorAll('gr-account-chip');
       assert.equal(chips.length, 3);
       Array.from(chips).forEach(function(el) {
         var accountID = parseInt(el.getAttribute('data-account-id'), 10);
@@ -195,17 +178,15 @@
     test('autocomplete starts at >= 3 chars', function() {
       element._inputRequestTimeout = 0;
       element._mutable = true;
-      var genRequestStub = sinon.stub(
-        element.$.autocompleteXHR,
-        'generateRequest',
+      var requestStub = sinon.stub(element, '_getSuggestedReviewers',
         function() {
-          assert(false, 'generateRequest should not be called for input ' +
-              'lengths of less than 3 chars');
+          assert(false, '_getSuggestedReviewers should not be called for ' +
+              'input lengths of less than 3 chars');
         }
       );
       element._inputVal = 'fo';
       flushAsynchronousOperations();
-      genRequestStub.restore();
+      requestStub.restore();
     });
 
     test('add/remove reviewer flow', function(done) {
@@ -220,9 +201,8 @@
       MockInteractions.tap(element.$$('.addReviewer'));
       flushAsynchronousOperations();
       element._inputVal = 'andy';
-      server.respond();
 
-      element._lastAutocompleteRequest.completes.then(function() {
+      element._lastAutocompleteRequest.then(function() {
         flushAsynchronousOperations();
         assert.isFalse(element.$$('.dropdown').hasAttribute('hidden'));
         var itemEls = Polymer.dom(element.root).querySelectorAll('.reviewer');
@@ -242,8 +222,7 @@
         assert.isTrue(element.$$('.dropdown').hasAttribute('hidden'));
 
         element._inputVal = 'andyb';
-        server.respond();
-        element._lastAutocompleteRequest.completes.then(function() {
+        element._lastAutocompleteRequest.then(function() {
           assert.isFalse(element.$$('.dropdown').hasAttribute('hidden'));
           var itemEls = Polymer.dom(element.root).querySelectorAll('.reviewer');
           assert.equal(itemEls.length, 3);
@@ -251,7 +230,6 @@
           assert.isFalse(itemEls[1].hasAttribute('selected'));
           MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter'
           assert.isTrue(element.disabled);
-          server.respond();
 
           element._xhrPromise.then(function() {
             assert.isFalse(element.disabled);
@@ -262,13 +240,12 @@
             MockInteractions.tap(element.$$('.reviewer').$$('gr-button'));
             flushAsynchronousOperations();
             assert.isTrue(element.disabled);
-            server.respond();
 
             element._xhrPromise.then(function() {
               flushAsynchronousOperations();
               assert.isFalse(element.disabled);
               var reviewerEls =
-                Polymer.dom(element.root).querySelectorAll('.reviewer');
+                  Polymer.dom(element.root).querySelectorAll('.reviewer');
               assert.equal(reviewerEls.length, 0);
               done();
             });
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html
index 2c7550f..6880a39 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html
@@ -82,8 +82,8 @@
               <div>[[account.email]]</div>
             </div>
           </li>
-          <li><a href="/switch-account">Switch account</a></li>
-          <li><a href="/logout">Sign out</a></li>
+          <li><a href$="[[_computeRelativeURL('/switch-account')]]">Switch account</a></li>
+          <li><a href$="[[_computeRelativeURL('/logout')]]">Sign out</a></li>
         </ul>
       </div>
     </iron-dropdown>
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
index 62212a3..31edb1a 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
@@ -31,5 +31,9 @@
     _showDropdownTapHandler: function(e) {
       this.$.dropdown.open();
     },
+
+    _computeRelativeURL: function(path) {
+      return '//' + window.location.host + path;
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/shared/gr-request/gr-request.html b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.html
similarity index 64%
copy from polygerrit-ui/app/elements/shared/gr-request/gr-request.html
copy to polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.html
index df9eddc..80f293d 100644
--- a/polygerrit-ui/app/elements/shared/gr-request/gr-request.html
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.html
@@ -1,5 +1,5 @@
 <!--
-Copyright (C) 2015 The Android Open Source Project
+Copyright (C) 2016 The Android Open Source Project
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -15,11 +15,13 @@
 -->
 
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-ajax/iron-request.html">
+<link rel="import" href="../../shared/gr-alert/gr-alert.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
-<dom-module id="gr-request">
+<dom-module id="gr-error-manager">
   <template>
-    <iron-request id="xhr"></iron-request>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
-  <script src="gr-request.js"></script>
+  <script src="gr-error-manager.js"></script>
 </dom-module>
+
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
new file mode 100644
index 0000000..757d79f
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
@@ -0,0 +1,109 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-error-manager',
+
+    properties: {
+      _alertElement: Element,
+      _hideAlertHandle: Number,
+      _hideAlertTimeout: {
+        type: Number,
+        value: 5000,
+      },
+    },
+
+    attached: function() {
+      this.listen(document, 'server-error', '_handleServerError');
+      this.listen(document, 'network-error', '_handleNetworkError');
+    },
+
+    detached: function() {
+      this._clearHideAlertHandle();
+      this.unlisten(document, 'server-error', '_handleServerError');
+      this.unlisten(document, 'network-error', '_handleNetworkError');
+    },
+
+    _handleServerError: function(e) {
+      if (e.detail.response.status === 403) {
+        this._getLoggedIn().then(function(loggedIn) {
+          if (loggedIn) {
+            // The app was logged at one point and is now getting auth errors.
+            // This indicates the auth token is no longer valid.
+            this._showAuthErrorAlert();
+          }
+        }.bind(this));
+      } else {
+        e.detail.response.text().then(function(text) {
+          this._showAlert('Server error: ' + text);
+        }.bind(this));
+      }
+    },
+
+    _handleNetworkError: function(e) {
+      this._showAlert('Server unavailable');
+      console.error(e.detail.error.message);
+    },
+
+    _getLoggedIn: function() {
+      return this.$.restAPI.getLoggedIn();
+    },
+
+    _showAlert: function(text) {
+      if (this._alertElement) { return; }
+
+      this._clearHideAlertHandle();
+      this._hideAlertHandle =
+            this.async(this._hideAlert.bind(this), this._hideAlertTimeout);
+      var el = this._createToastAlert();
+      el.show(text);
+      this._alertElement = el;
+    },
+
+    _hideAlert: function() {
+      if (!this._alertElement) { return; }
+
+      this._alertElement.hide();
+      this._alertElement = null;
+    },
+
+    _clearHideAlertHandle: function() {
+      if (this._hideAlertHandle != null) {
+        this.cancelAsync(this._hideAlertHandle);
+        this._hideAlertHandle = null;
+      }
+    },
+
+    _showAuthErrorAlert: function() {
+      if (this._alertElement) { return; }
+
+      var el = this._createToastAlert();
+      el.addEventListener('action', this._refreshPage.bind(this));
+      el.show('Auth error', 'Refresh page');
+      this._alertElement = el;
+    },
+
+    _createToastAlert: function() {
+      var el = document.createElement('gr-alert');
+      el.toast = true;
+      return el;
+    },
+
+    _refreshPage: function() {
+      window.location.reload();
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
new file mode 100644
index 0000000..98f79b0
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-error-manager</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-error-manager.html">
+
+<test-fixture id="basic">
+  <template>
+    <gr-error-manager></gr-error-manager>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-error-manager tests', function() {
+    var element;
+
+    setup(function() {
+      stub('gr-rest-api-interface', {
+        getLoggedIn: function() { return Promise.resolve(true); },
+      });
+      element = fixture('basic');
+    });
+
+    test('show auth error', function(done) {
+      var showAuthErrorStub = sinon.stub(element, '_showAuthErrorAlert');
+      element.fire('server-error', {response: {status: 403}});
+      flush(function() {
+        assert.isTrue(showAuthErrorStub.calledOnce);
+        showAuthErrorStub.restore();
+        done();
+      });
+    });
+
+    test('show normal server error', function(done) {
+      var showAlertStub = sinon.stub(element, '_showAlert');
+      element.fire('server-error', {response: {
+        status: 500,
+        text: function() { return Promise.resolve('ZOMG'); },
+      }});
+      flush(function() {
+        assert.isTrue(showAlertStub.calledOnce);
+        assert.isTrue(showAlertStub.lastCall.calledWithExactly(
+            'Server error: ZOMG'));
+        showAlertStub.restore();
+        done();
+      });
+    });
+
+    test('show network error', function(done) {
+      var consoleErrorStub = sinon.stub(console, 'error');
+      var showAlertStub = sinon.stub(element, '_showAlert');
+      element.fire('network-error', {error: new Error('ZOMG')});
+      flush(function() {
+        assert.isTrue(showAlertStub.calledOnce);
+        assert.isTrue(showAlertStub.lastCall.calledWithExactly(
+            'Server unavailable'));
+        assert.isTrue(consoleErrorStub.calledOnce);
+        assert.isTrue(consoleErrorStub.lastCall.calledWithExactly('ZOMG'));
+        showAlertStub.restore();
+        consoleErrorStub.restore();
+        done();
+      });
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
index 9a75fa9..32de08a 100644
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
@@ -98,6 +98,13 @@
             <td><span class="key">u</span></td>
             <td>Up to change list</td>
           </tr>
+          <tr>
+            <td>
+              <span class="key modifier">Shift</span>
+              <span class="key">i</span>
+            </td>
+            <td>Show/hide inline diffs</td>
+          </tr>
         </tbody>
         <!-- Diff View -->
         <tbody hidden$="[[!_computeInView(view, 'gr-diff-view')]]" hidden>
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
index 20405b9..c6d8dbc 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
@@ -130,7 +130,7 @@
       }
     </style>
     <nav>
-      <a href="/" class="bigTitle">PolyGerrit</a>
+      <a href$="[[_computeRelativeURL('/')]]" class="bigTitle">PolyGerrit</a>
       <ul class="links">
         <template is="dom-repeat" items="[[_links]]" as="linkGroup">
           <li>
@@ -139,7 +139,7 @@
             </span>
             <ul>
               <template is="dom-repeat" items="[[linkGroup.links]]" as="link">
-                <li><a href="[[link.url]]">[[link.name]]</a></li>
+                <li><a href$="[[link.url]]">[[link.name]]</a></li>
               </template>
             </ul>
           </li>
@@ -148,7 +148,7 @@
       <div class="rightItems">
         <gr-search-bar value="{{searchQuery}}" role="search"></gr-search-bar>
         <div class="accountContainer" id="accountContainer">
-          <a class="loginButton" href="/login" on-tap="_loginTapHandler">Sign in</a>
+          <a class="loginButton" href$="[[_computeRelativeURL('/login')]]" on-tap="_loginTapHandler">Sign in</a>
           <gr-account-dropdown account="[[_account]]"></gr-account-dropdown>
         </div>
       </div>
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
index 173b88e..186932a 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
@@ -70,6 +70,10 @@
       this._loadAccount();
     },
 
+    _computeRelativeURL: function(path) {
+      return '//' + window.location.host + path;
+    },
+
     _computeLinks: function(defaultLinks, userLinks) {
       var links = defaultLinks.slice();
       if (userLinks && userLinks.length > 0) {
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.js b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
index 12fd31a..9145ba1 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -29,7 +29,7 @@
     function loadUser(ctx, next) {
       restAPI.getLoggedIn().then(function() {
         next();
-      })
+      });
     }
 
     // Routes.
@@ -68,15 +68,36 @@
     page('/q/:query', queryHandler);
 
     page(/^\/(\d+)\/?/, function(ctx) {
-      page.redirect('/c/' + ctx.params[0]);
+      page.redirect('/c/' + encodeURIComponent(ctx.params[0]));
     });
 
-    page('/c/:changeNum/:patchNum?', function(data) {
-      data.params.view = 'gr-change-view';
-      app.params = data.params;
+    // Matches /c/<changeNum>/[<basePatchNum>..][<patchNum>].
+    page(/^\/c\/(\d+)\/?(((\d+)(\.\.(\d+))?))?$/, function(ctx) {
+      // Parameter order is based on the regex group number matched.
+      var params = {
+        changeNum: ctx.params[0],
+        basePatchNum: ctx.params[3],
+        patchNum: ctx.params[5],
+        view: 'gr-change-view',
+      };
+
+      // Don't allow diffing the same patch number against itself.
+      if (params.basePatchNum != null &&
+          params.basePatchNum === params.patchNum) {
+        page.redirect('/c/' +
+            encodeURIComponent(params.changeNum) +
+            '/' +
+            encodeURIComponent(params.patchNum) +
+            '/');
+        return;
+      }
+      normalizePatchRangeParams(params);
+      app.params = params;
     });
 
+    // Matches /c/<changeNum>/[<basePatchNum>..]<patchNum>/<path>.
     page(/^\/c\/(\d+)\/((\d+)(\.\.(\d+))?)\/(.+)/, function(ctx) {
+      // Parameter order is based on the regex group number matched.
       var params = {
         changeNum: ctx.params[0],
         basePatchNum: ctx.params[2],
@@ -84,19 +105,27 @@
         path: ctx.params[5],
         view: 'gr-diff-view',
       };
-      // Don't allow diffing the same patch number against itself because WHY?
-      if (params.basePatchNum == params.patchNum) {
-        page.redirect('/c/' + params.changeNum + '/' + params.patchNum + '/' +
-            params.path);
+      // Don't allow diffing the same patch number against itself.
+      if (params.basePatchNum === params.patchNum) {
+        page.redirect('/c/' +
+            encodeURIComponent(params.changeNum) +
+            '/' +
+            encodeURIComponent(params.patchNum) +
+            '/' +
+            encodeURIComponent(params.path));
         return;
       }
-      if (!params.patchNum) {
-        params.patchNum = params.basePatchNum;
-        delete(params.basePatchNum);
-      }
+      normalizePatchRangeParams(params);
       app.params = params;
     });
 
+    function normalizePatchRangeParams(params) {
+      if (params.basePatchNum && !params.patchNum) {
+        params.patchNum = params.basePatchNum;
+        params.basePatchNum = null;
+      }
+    }
+
     page.start();
   });
 })();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html
index d293060..25237b5 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html
@@ -22,18 +22,11 @@
   <template>
     <style>
       :host {
+        border: 1px solid #ddd;
+        border-right: none;
         display: block;
         white-space: normal;
       }
-      gr-diff-comment {
-        border-left: 1px solid #ddd;
-      }
-      gr-diff-comment:first-of-type {
-        border-top: 1px solid #ddd;
-      }
-      gr-diff-comment:last-of-type {
-        border-bottom: 1px solid #ddd;
-      }
     </style>
     <div id="container">
       <template id="commentList" is="dom-repeat" items="[[_orderedComments]]" as="comment">
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
index 82b0faf..9462408 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
@@ -124,11 +124,24 @@
 
   suite('comment action tests', function() {
     var element;
-    var server;
 
     setup(function() {
       stub('gr-rest-api-interface', {
         getLoggedIn: function() { return Promise.resolve(false); },
+        saveDiffDraft: function() { return Promise.resolve({
+          ok: true,
+          text: function() { return Promise.resolve(')]}\'\n' +
+              JSON.stringify({
+                id: '7afa4931_de3d65bd',
+                path: '/path/to/file.txt',
+                line: 5,
+                in_reply_to: 'baf0414d_60047215',
+                updated: '2015-12-21 02:01:10.850000000',
+                message: 'Done'
+              }));
+          },
+        })},
+        deleteDiffDraft: function() { return Promise.resolve({ok: true}); },
       });
       element = fixture('withComment');
       element.comments = [{
@@ -142,35 +155,6 @@
         updated: '2015-12-08 19:48:33.843000000',
       }];
       flushAsynchronousOperations();
-
-      server = sinon.fakeServer.create();
-      // Eat any requests made by elements in this suite.
-      server.respondWith(
-        'PUT',
-        '/changes/41/1/drafts',
-        [
-          201,
-          {'Content-Type': 'application/json'},
-          ')]}\'\n' + JSON.stringify({
-            id: '7afa4931_de3d65bd',
-            path: '/path/to/file.txt',
-            line: 5,
-            in_reply_to: 'baf0414d_60047215',
-            updated: '2015-12-21 02:01:10.850000000',
-            message: 'Done'
-          }),
-        ]
-      );
-
-      server.respondWith(
-        'DELETE',
-        '/changes/41/1/drafts/baf0414d_60047215',
-        [
-          204,
-          {},
-          '',
-        ]
-      );
     });
 
     test('reply', function(done) {
@@ -210,7 +194,6 @@
       var commentEl = element.$$('gr-diff-comment');
       assert.ok(commentEl);
       commentEl.addEventListener('done', function() {
-        server.respond();
         var drafts = element._orderedComments.filter(function(c) {
           return c.__draft == true;
         });
@@ -236,7 +219,6 @@
           Polymer.dom(element.root).querySelectorAll('gr-diff-comment')[1];
       assert.ok(draftEl);
       draftEl.addEventListener('comment-discard', function() {
-        server.respond();
         var drafts = element.comments.filter(function(c) {
           return c.__draft == true;
         });
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
index a6a8c4d..fd35ad7 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
@@ -19,7 +19,7 @@
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
 <link rel="import" href="../../shared/gr-linked-text/gr-linked-text.html">
-<link rel="import" href="../../shared/gr-request/gr-request.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
 <dom-module id="gr-diff-comment">
   <template>
@@ -147,6 +147,7 @@
         </div>
       </div>
     </div>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-diff-comment.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
index 81449b8..ded5108 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
@@ -72,22 +72,26 @@
     save: function() {
       this.comment.message = this._editDraft;
       this.disabled = true;
-      var endpoint = this._restEndpoint(this.comment.id);
-      this._send('PUT', endpoint).then(function(req) {
+      this._xhrPromise = this._saveDraft(this.comment).then(function(response) {
         this.disabled = false;
-        var comment = req.response;
-        comment.__draft = true;
-        // Maintain the ephemeral draft ID for identification by other
-        // elements.
-        if (this.comment.__draftID) {
-          comment.__draftID = this.comment.__draftID;
-        }
-        this.comment = comment;
-        this.editing = false;
+        if (!response.ok) { return response; }
+
+        return this.$.restAPI.getResponseObject(response).then(function(obj) {
+          var comment = obj;
+          comment.__draft = true;
+          // Maintain the ephemeral draft ID for identification by other
+          // elements.
+          if (this.comment.__draftID) {
+            comment.__draftID = this.comment.__draftID;
+          }
+          this.comment = comment;
+          this.editing = false;
+
+          return obj;
+        }.bind(this));
       }.bind(this)).catch(function(err) {
-        alert('Your draft couldn’t be saved. Check the console and contact ' +
-            'the PolyGerrit team for assistance.');
         this.disabled = false;
+        throw err;
       }.bind(this));
     },
 
@@ -179,18 +183,21 @@
         throw Error('Cannot discard a non-draft comment.');
       }
       this.disabled = true;
-      var commentID = this.comment.id;
-      if (!commentID) {
+      if (!this.comment.id) {
         this.fire('comment-discard');
         return;
       }
-      this._send('DELETE', this._restEndpoint(commentID)).then(function(req) {
+
+      this._xhrPromise =
+          this._deleteDraft(this.comment).then(function(response) {
+        this.disabled = false;
+        if (!response.ok) { return response; }
+
         this.fire('comment-discard');
       }.bind(this)).catch(function(err) {
-        alert('Your draft couldn’t be deleted. Check the console and ' +
-            'contact the PolyGerrit team for assistance.');
         this.disabled = false;
-      }.bind(this));
+        throw err;
+      }.bind(this));;
     },
 
     _preventDefaultAndBlur: function(e) {
@@ -198,26 +205,13 @@
       Polymer.dom(e).rootTarget.blur();
     },
 
-    _send: function(method, url) {
-      var xhr = document.createElement('gr-request');
-      var opts = {
-        method: method,
-        url: url,
-      };
-      if (method == 'PUT' || method == 'POST') {
-        opts.body = this.comment;
-      }
-      this._xhrPromise = xhr.send(opts);
-      return this._xhrPromise;
+    _saveDraft: function(draft) {
+      return this.$.restAPI.saveDiffDraft(this.changeNum, this.patchNum, draft);
     },
 
-    _restEndpoint: function(id) {
-      var path = '/changes/' + this.changeNum + '/revisions/' +
-          this.patchNum + '/drafts';
-      if (id) {
-        path += '/' + id;
-      }
-      return path;
+    _deleteDraft: function(draft) {
+      return this.$.restAPI.deleteDiffDraft(this.changeNum, this.patchNum,
+          draft);
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
index c95a9a4..a333e14 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
@@ -42,6 +42,9 @@
   suite('gr-diff-comment tests', function() {
     var element;
     setup(function() {
+      stub('gr-rest-api-interface', {
+        getAccount: function() { return Promise.resolve(null); },
+      });
       element = fixture('basic');
       element.comment = {
         author: {
@@ -93,9 +96,30 @@
 
   suite('gr-diff-comment draft tests', function() {
     var element;
-    var server;
 
     setup(function() {
+      stub('gr-rest-api-interface', {
+        getAccount: function() { return Promise.resolve(null); },
+        saveDiffDraft: function() {
+          return Promise.resolve({
+            ok: true,
+            text: function() {
+              return Promise.resolve(
+                ')]}\'\n{' +
+                  '"id": "baf0414d_40572e03",' +
+                  '"path": "/path/to/file",' +
+                  '"line": 5,' +
+                  '"updated": "2015-12-08 21:52:36.177000000",' +
+                  '"message": "saved!"' +
+                '}'
+              );
+            },
+          });
+        },
+        removeChangeReviewer: function() {
+          return Promise.resolve({ok: true});
+        },
+      });
       element = fixture('draft');
       element.changeNum = 42;
       element.patchNum = 1;
@@ -106,43 +130,6 @@
         path: '/path/to/file',
         line: 5,
       };
-
-      server = sinon.fakeServer.create();
-      server.respondWith(
-        'PUT',
-        '/changes/42/revisions/1/drafts',
-        [
-          201,
-          {'Content-Type': 'application/json'},
-          ')]}\'\n{' +
-            '"id": "baf0414d_40572e03",' +
-            '"path": "/path/to/file",' +
-            '"line": 5,' +
-            '"updated": "2015-12-08 21:52:36.177000000",' +
-            '"message": "created!"' +
-          '}'
-        ]
-      );
-
-      server.respondWith(
-        'PUT',
-        /\/changes\/42\/revisions\/1\/drafts\/.+/,
-        [
-          200,
-          {'Content-Type': 'application/json'},
-          ')]}\'\n{' +
-            '"id": "baf0414d_40572e03",' +
-            '"path": "/path/to/file",' +
-            '"line": 5,' +
-            '"updated": "2015-12-08 21:52:36.177000000",' +
-            '"message": "saved!"' +
-          '}'
-        ]
-      );
-    });
-
-    teardown(function() {
-      server.restore();
     });
 
     function isVisible(el) {
@@ -224,14 +211,10 @@
       assert.isTrue(element.disabled,
           'Element should be disabled when creating draft.');
 
-      server.respond();
-
-      element._xhrPromise.then(function(req) {
+      element._xhrPromise.then(function(draft) {
         assert.isFalse(element.disabled,
             'Element should be enabled when done creating draft.');
-        assert.equal(req.status, 201);
-        assert.equal(req.url, '/changes/42/revisions/1/drafts');
-        assert.equal(req.response.message, 'created!');
+        assert.equal(draft.message, 'saved!');
         assert.isFalse(element.editing);
       }).then(function() {
         MockInteractions.tap(element.$$('.edit'));
@@ -240,15 +223,11 @@
         MockInteractions.tap(element.$$('.save'));
         assert.isTrue(element.disabled,
             'Element should be disabled when updating draft.');
-        server.respond();
 
-        element._xhrPromise.then(function(req) {
+        element._xhrPromise.then(function(draft) {
           assert.isFalse(element.disabled,
               'Element should be enabled when done updating draft.');
-          assert.equal(req.status, 200);
-          assert.equal(req.url,
-              '/changes/42/revisions/1/drafts/baf0414d_40572e03');
-          assert.equal(req.response.message, 'saved!');
+          assert.equal(draft.message, 'saved!');
           assert.isFalse(element.editing);
           done();
         });
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
index 137162f..c52289f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
@@ -18,8 +18,11 @@
 <link rel="import" href="../../../bower_components/iron-dropdown/iron-dropdown.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../gr-diff/gr-diff.html">
+<link rel="import" href="../gr-diff-preferences/gr-diff-preferences.html">
+<link rel="import" href="../gr-patch-range-select/gr-patch-range-select.html">
 
 <dom-module id="gr-diff-view">
   <template>
@@ -29,7 +32,6 @@
         display: block;
       }
       h3 {
-        margin-top: 1em;
         padding: .75em var(--default-horizontal-margin);
       }
       .reviewed {
@@ -89,6 +91,21 @@
         padding: .3em 0;
         text-decoration: none;
       }
+      .loading {
+        padding: 0 var(--default-horizontal-margin) 1em;
+        color: #666;
+      }
+      .header {
+        display: flex;
+        justify-content: space-between;
+        margin: 0 var(--default-horizontal-margin) .75em;
+      }
+      .prefsButton {
+        text-align: right;
+      }
+      #modeSelect {
+        margin-left: .5em;
+      }
       @media screen and (max-width: 50em) {
         .dash {
           display: none;
@@ -106,7 +123,7 @@
       }
     </style>
     <h3>
-      <a href$="[[_computeChangePath(_changeNum, _patchRange.patchNum, _change.revisions)]]">
+      <a href$="[[_computeChangePath(_changeNum, _patchRange.*, _change.revisions)]]">
         [[_changeNum]]</a><span>:</span>
       <span>[[_change.subject]]</span>
       <span class="dash">—</span>
@@ -123,7 +140,7 @@
         <iron-dropdown id="dropdown" vertical-align="top" vertical-offset="25">
           <div class="dropdown-content">
             <template is="dom-repeat" items="[[_fileList]]" as="path">
-              <a href$="[[_computeDiffURL(_changeNum, _patchRange, path)]]"
+              <a href$="[[_computeDiffURL(_changeNum, _patchRange.*, path)]]"
                  selected$="[[_computeFileSelected(path, _path)]]"
                  data-key-nav$="[[_computeKeyNav(path, _path, _fileList)]]"
                  on-tap="_handleFileTap">
@@ -145,14 +162,44 @@
         </select>
       </div>
     </h3>
-    <gr-diff id="diff"
-        change-num="[[_changeNum]]"
-        patch-range="[[_patchRange]]"
-        path="[[_path]]"
-        project-config="[[_projectConfig]]"
-        available-patches="[[_computeAvailablePatches(_change.revisions)]]"
-        on-render="_handleDiffRender">
-    </gr-diff>
+    <div class="loading" hidden$="[[!_loading]]">Loading...</div>
+    <div hidden$="[[_loading]]" hidden>
+      <div class="header">
+        <gr-patch-range-select
+            path="[[_path]]"
+            change-num="[[_changeNum]]"
+            patch-range="[[_patchRange]]"
+            available-patches="[[_computeAvailablePatches(_change.revisions)]]">
+        </gr-patch-range-select>
+        <div>
+          <select id="modeSelect" on-change="_handleModeChange">
+            <option value="SIDE_BY_SIDE">Side By Side</option>
+            <option value="UNIFIED_DIFF">Unified</option>
+          </select>
+          <span hidden$="[[_computePrefsButtonHidden(_prefs, _loggedIn)]]">
+            /
+            <gr-button link
+                class="prefsButton"
+                on-tap="_handlePrefsTap">Preferences</gr-button>
+          </span>
+        </div>
+      </div>
+      <gr-overlay id="prefsOverlay" with-backdrop>
+        <gr-diff-preferences
+            prefs="{{_prefs}}"
+            on-save="_handlePrefsSave"
+            on-cancel="_handlePrefsCancel"></gr-diff-preferences>
+      </gr-overlay>
+      <gr-diff id="diff"
+          change-num="[[_changeNum]]"
+          patch-range="[[_patchRange]]"
+          path="[[_path]]"
+          prefs="[[_prefs]]"
+          project-config="[[_projectConfig]]"
+          view-mode="[[_diffMode]]"
+          on-render="_handleDiffRender">
+      </gr-diff>
+    </div>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-diff-view.js"></script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index c4f2a83..26c1030 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -16,6 +16,11 @@
 
   var COMMIT_MESSAGE_PATH = '/COMMIT_MSG';
 
+  var DiffViewMode = {
+    SIDE_BY_SIDE: 'SIDE_BY_SIDE',
+    UNIFIED: 'UNIFIED_DIFF',
+  };
+
   Polymer({
     is: 'gr-diff-view',
 
@@ -59,6 +64,16 @@
         type: Boolean,
         value: false,
       },
+      _loading: {
+        type: Boolean,
+        value: true,
+      },
+      _prefs: Object,
+      _userPrefs: Object,
+      _diffMode: {
+        type: String,
+        computed: '_getDiffViewMode(changeViewState.diffMode, _userPrefs)'
+      },
     },
 
     behaviors: [
@@ -68,7 +83,8 @@
     observers: [
       '_getChangeDetail(_changeNum)',
       '_getProjectConfig(_change.project)',
-      '_getFiles(_changeNum, _patchRange.patchNum)',
+      '_getFiles(_changeNum, _patchRange.*)',
+      '_updateModeSelect(_diffMode)',
     ],
 
     attached: function() {
@@ -83,11 +99,11 @@
         this.fire('title-change',
             {title: this._computeFileDisplayName(this._path)});
       }
-      window.addEventListener('resize', this._boundWindowResizeHandler);
     },
 
     detached: function() {
-      window.removeEventListener('resize', this._boundWindowResizeHandler);
+      // Reset the diff mode to null so that it reverts to the user preference.
+      this.changeViewState.diffMode = null;
     },
 
     _getLoggedIn: function() {
@@ -108,13 +124,22 @@
           }.bind(this));
     },
 
-    _getFiles: function(changeNum, patchNum) {
-      return this.$.restAPI.getChangeFiles(changeNum, patchNum).then(
-          function(files) {
-            this._fileList = Object.keys(files).sort();
+    _getFiles: function(changeNum, patchRangeRecord) {
+      var patchRange = patchRangeRecord.base;
+      return this.$.restAPI.getChangeFilePathsAsSpeciallySortedArray(
+          changeNum, patchRange).then(function(files) {
+            this._fileList = files;
           }.bind(this));
     },
 
+    _getDiffPreferences: function() {
+      return this.$.restAPI.getDiffPreferences();
+    },
+
+    _getPreferences: function() {
+      return this.$.restAPI.getPreferences();
+    },
+
     _handleReviewedChange: function(e) {
       this._setReviewed(Polymer.dom(e).rootTarget.checked);
     },
@@ -169,15 +194,15 @@
         case 85:  // 'u'
           if (this._changeNum && this._patchRange.patchNum) {
             e.preventDefault();
-            page.show(this._computeChangePath(
+            page.show(this._getChangePath(
                 this._changeNum,
-                this._patchRange.patchNum,
+                this._patchRange,
                 this._change && this._change.revisions));
           }
           break;
         case 188:  // ','
           e.preventDefault();
-          this.$.diff.showDiffPreferences();
+          this.$.prefsOverlay.open();
           break;
       }
     },
@@ -194,20 +219,22 @@
 
       var idx = fileList.indexOf(this._path) + direction;
       if (idx < 0 || idx > fileList.length - 1) {
-        page.show(this._computeChangePath(
+        page.show(this._getChangePath(
             this._changeNum,
-            this._patchRange.patchNum,
+            this._patchRange,
             this._change && this._change.revisions));
         return;
       }
-      page.show(this._computeDiffURL(this._changeNum,
-                                     this._patchRange,
-                                     fileList[idx]));
+      page.show(this._getDiffURL(this._changeNum,
+                                 this._patchRange,
+                                 fileList[idx]));
     },
 
     _paramsChanged: function(value) {
       if (value.view != this.tagName.toLowerCase()) { return; }
 
+      this._loading = true;
+
       this._changeNum = value.changeNum;
       this._patchRange = {
         patchNum: value.patchNum,
@@ -225,7 +252,21 @@
         return;
       }
 
-      this.$.diff.reload();
+      var promises = [];
+
+      promises.push(this._getDiffPreferences().then(function(prefs) {
+        this._prefs = prefs;
+      }.bind(this)));
+
+      promises.push(this._getPreferences().then(function(prefs) {
+        this._userPrefs = prefs;
+      }.bind(this)));
+
+      promises.push(this.$.diff.reload());
+
+      Promise.all(promises).then(function() {
+        this._loading = false;
+      }.bind(this));
     },
 
     _pathChanged: function(path) {
@@ -239,13 +280,22 @@
       }
     },
 
-    _computeDiffURL: function(changeNum, patchRange, path) {
+    _getDiffURL: function(changeNum, patchRange, path) {
+      return '/c/' + changeNum + '/' + this._patchRangeStr(patchRange) + '/' +
+          path;
+    },
+
+    _computeDiffURL: function(changeNum, patchRangeRecord, path) {
+      return this._getDiffURL(changeNum, patchRangeRecord.base, path);
+    },
+
+    _patchRangeStr: function(patchRange) {
       var patchStr = patchRange.patchNum;
       if (patchRange.basePatchNum != null &&
           patchRange.basePatchNum != 'PARENT') {
         patchStr = patchRange.basePatchNum + '..' + patchRange.patchNum;
       }
-      return '/c/' + changeNum + '/' + patchStr + '/' + path;
+      return patchStr;
     },
 
     _computeAvailablePatches: function(revisions) {
@@ -256,25 +306,30 @@
       return patchNums.sort(function(a, b) { return a - b; });
     },
 
-    _computeChangePath: function(changeNum, patchNum, revisions) {
+    _getChangePath: function(changeNum, patchRange, revisions) {
       var base = '/c/' + changeNum + '/';
 
       // The change may not have loaded yet, making revisions unavailable.
       if (!revisions) {
-        return base + patchNum;
+        return base + this._patchRangeStr(patchRange);
       }
 
       var latestPatchNum = -1;
       for (var rev in revisions) {
         latestPatchNum = Math.max(latestPatchNum, revisions[rev]._number);
       }
-      if (parseInt(patchNum, 10) != latestPatchNum) {
-        return base + patchNum;
+      if (patchRange.basePatchNum !== 'PARENT' ||
+          parseInt(patchRange.patchNum, 10) !== latestPatchNum) {
+        return base + this._patchRangeStr(patchRange);
       }
 
       return base;
     },
 
+    _computeChangePath: function(changeNum, patchRangeRecord, revisions) {
+      return this._getChangePath(changeNum, patchRangeRecord.base, revisions);
+    },
+
     _computeFileDisplayName: function(path) {
       return path == COMMIT_MESSAGE_PATH ? 'Commit message' : path;
     },
@@ -283,6 +338,10 @@
       return path == currentPath;
     },
 
+    _computePrefsButtonHidden: function(prefs, loggedIn) {
+      return !loggedIn || !prefs;
+    },
+
     _computeKeyNav: function(path, selectedPath, fileList) {
       var selectedIndex = fileList.indexOf(selectedPath);
       if (fileList.indexOf(path) == selectedIndex - 1) {
@@ -300,12 +359,77 @@
 
     _handleMobileSelectChange: function(e) {
       var path = Polymer.dom(e).rootTarget.value;
-      page.show(
-          this._computeDiffURL(this._changeNum, this._patchRange, path));
+      page.show(this._getDiffURL(this._changeNum, this._patchRange, path));
     },
 
     _showDropdownTapHandler: function(e) {
       this.$.dropdown.open();
     },
+
+    _handlePrefsTap: function(e) {
+      e.preventDefault();
+      this.$.prefsOverlay.open();
+    },
+
+    _handlePrefsSave: function(e) {
+      e.stopPropagation();
+      var el = Polymer.dom(e).rootTarget;
+      el.disabled = true;
+      this._saveDiffPreferences().then(function(response) {
+        el.disabled = false;
+        if (!response.ok) { return response; }
+
+        this.$.prefsOverlay.close();
+      }.bind(this)).catch(function(err) {
+        el.disabled = false;
+      }.bind(this));
+    },
+
+    _saveDiffPreferences: function() {
+      return this.$.restAPI.saveDiffPreferences(this._prefs);
+    },
+
+    _handlePrefsCancel: function(e) {
+      e.stopPropagation();
+      this.$.prefsOverlay.close();
+    },
+
+    _handleModeChange: function(e) {
+      this.set('changeViewState.diffMode', this.$.modeSelect.value);
+    },
+
+    /**
+     * _getDiffViewMode: Get the diff view (side-by-side or unified) based on
+     * the current state.
+     *
+     * The expected behavior is to use the mode specified in the user's
+     * preferences unless they have manually chosen the alternative view. If the
+     * user navigates up to the change view, it should clear this choice and
+     * revert to the preference the next time a diff is viewed.
+     *
+     * Use side-by-side if the user is not logged in.
+     *
+     * @return {String}
+     */
+    _getDiffViewMode: function() {
+      if (this.changeViewState.diffMode) {
+        return this.changeViewState.diffMode;
+      } else if (this._userPrefs && this._userPrefs.diff_view) {
+        return this.changeViewState.diffMode = this._userPrefs.diff_view;
+      }
+
+      return DiffViewMode.SIDE_BY_SIDE;
+    },
+
+    /**
+     * Synchronize the mode select if it deviates from the selected mode state.
+     * This is mainly to keep it accurate when the page loads.
+     */
+    _updateModeSelect: function() {
+      var mode = this._getDiffViewMode();
+      if (this.$.modeSelect.value !== mode) {
+        this.$.modeSelect.value = mode;
+      }
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
index aaeced2..ce7cdd6 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
@@ -50,11 +50,12 @@
     test('keyboard shortcuts', function() {
       element._changeNum = '42';
       element._patchRange = {
+        basePatchNum: 'PARENT',
         patchNum: '10',
       };
       element._change = {
         revisions: {
-          a: { _number: 10, },
+          a: {_number: 10},
         },
       };
       element._fileList = ['chell.go', 'glados.txt', 'wheatley.md'];
@@ -89,7 +90,7 @@
           'Should navigate to /c/42/');
       assert.equal(element.changeViewState.selectedFileIndex, 0);
 
-      var showPrefsStub = sinon.stub(element.$.diff, 'showDiffPreferences');
+      var showPrefsStub = sinon.stub(element.$.prefsOverlay, 'open');
       MockInteractions.pressAndReleaseKeyOn(element, 188);  // ','
       assert(showPrefsStub.calledOnce);
 
@@ -125,7 +126,7 @@
       };
       element._change = {
         revisions: {
-          a: { _number: 10, },
+          a: {_number: 10},
         },
       };
       element._fileList = ['chell.go', 'glados.txt', 'wheatley.md'];
@@ -143,12 +144,12 @@
       MockInteractions.pressAndReleaseKeyOn(element, 65);  // 'a'
       assert.isTrue(element.changeViewState.showReplyDialog);
 
-      assert(showStub.lastCall.calledWithExactly('/c/42/'),
-          'Should navigate to /c/42/');
+      assert(showStub.lastCall.calledWithExactly('/c/42/5..10'),
+          'Should navigate to /c/42/5..10');
 
       MockInteractions.pressAndReleaseKeyOn(element, 85);  // 'u'
-      assert(showStub.lastCall.calledWithExactly('/c/42/'),
-          'Should navigate to /c/42/');
+      assert(showStub.lastCall.calledWithExactly('/c/42/5..10'),
+          'Should navigate to /c/42/5..10');
 
       MockInteractions.pressAndReleaseKeyOn(element, 221);  // ']'
       assert(showStub.lastCall.calledWithExactly('/c/42/5..10/wheatley.md'),
@@ -166,8 +167,8 @@
       element._path = 'chell.go';
 
       MockInteractions.pressAndReleaseKeyOn(element, 219);  // '['
-      assert(showStub.lastCall.calledWithExactly('/c/42/'),
-          'Should navigate to /c/42/');
+      assert(showStub.lastCall.calledWithExactly('/c/42/5..10'),
+          'Should navigate to /c/42/5..10');
 
       showStub.restore();
     });
@@ -175,12 +176,13 @@
     test('keyboard shortcuts with old patch number', function() {
       element._changeNum = '42';
       element._patchRange = {
+        basePatchNum: 'PARENT',
         patchNum: '1',
       };
       element._change = {
         revisions: {
-          a: { _number: 1, },
-          b: { _number: 2, },
+          a: {_number: 1},
+          b: {_number: 2},
         },
       };
       element._fileList = ['chell.go', 'glados.txt', 'wheatley.md'];
@@ -230,6 +232,7 @@
     test('go up to change via kb without change loaded', function() {
       element._changeNum = '42';
       element._patchRange = {
+        basePatchNum: 'PARENT',
         patchNum: '1',
       };
 
@@ -280,6 +283,7 @@
     test('jump to file dropdown', function() {
       element._changeNum = '42';
       element._patchRange = {
+        basePatchNum: 'PARENT',
         patchNum: '10',
       };
       element._fileList = ['chell.go', 'glados.txt', 'wheatley.md'];
@@ -356,5 +360,37 @@
         done();
       });
     });
+
+    test('diff mode selector correctly toggles the diff', function() {
+      var select = element.$.modeSelect;
+      var diffDisplay = element.$.diff;
+
+      element._userPrefs = {diff_view: 'SIDE_BY_SIDE'};
+
+      // The mode selected in the view state reflects the selected option.
+      assert.equal(element._getDiffViewMode(), select.value);
+
+      // The mode selected in the view state reflects the view rednered in the
+      // diff.
+      assert.equal(select.value, diffDisplay.viewMode);
+
+      // We will simulate a user change of the selected mode.
+      var newMode = 'UNIFIED_DIFF';
+
+      // Listen to the change handler.
+      var eventStub = sinon.spy(element, '_handleModeChange');
+
+      // Set the actual value of the select, and simulate the change event.
+      select.value = newMode;
+      element.fire('change', {}, { node: select });
+
+      // Make sure the handler was called and the state is still coherent.
+      assert.isTrue(eventStub.called);
+      assert.equal(element._getDiffViewMode(), newMode);
+      assert.equal(element._getDiffViewMode(), select.value);
+      assert.equal(element._getDiffViewMode(), diffDisplay.viewMode);
+
+      eventStub.restore();
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-side-by-side.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-side-by-side.js
index 77c790c..061ed4f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-side-by-side.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-side-by-side.js
@@ -30,7 +30,7 @@
           pairs[i].right));
     }
     this._outputEl.insertBefore(sectionEl, opt_beforeSection);
-  },
+  };
 
   GrDiffBuilderSideBySide.prototype._createRow = function(section, leftLine,
       rightLine) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder.js
index f5e7c4c..ce4515c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder.js
@@ -58,7 +58,7 @@
 
   GrDiffBuilder.prototype.emitGroup = function(group, opt_beforeSection) {
     throw Error('Subclasses must implement emitGroup');
-  },
+  };
 
   GrDiffBuilder.prototype._processContent = function(content, groups, context) {
     this._appendFileComments(groups);
@@ -339,8 +339,8 @@
     function byLineNum(lineNum) {
       return function(c) {
         return (c.line === lineNum) ||
-               (c.line === undefined && lineNum === GrDiffLine.FILE)
-      }
+               (c.line === undefined && lineNum === GrDiffLine.FILE);
+      };
     }
     var leftComments =
         comments[GrDiffBuilder.Side.LEFT].filter(byLineNum(line.beforeNumber));
@@ -373,7 +373,7 @@
     threadEl.side = side;
     threadEl.projectConfig = projectConfig;
     return threadEl;
-  },
+  };
 
   GrDiffBuilder.prototype._commentThreadForLine = function(line, opt_side) {
     var comments = this._getCommentsForLine(this._comments, line, opt_side);
@@ -431,14 +431,15 @@
       html = this._addIntralineHighlights(text, html, line.highlights);
     }
 
-    if (text.length > this._prefs.line_length) {
+    if (this._textLength(text, this._prefs.tab_size) >
+        this._prefs.line_length) {
       html = this._addNewlines(text, html);
     }
     html = this._addTabWrappers(html);
 
     // If the html is equivalent to the text then it didn't get highlighted
     // or escaped. Use textContent which is faster than innerHTML.
-    if (html == text) {
+    if (html === text) {
       td.textContent = text;
     } else {
       td.innerHTML = html;
@@ -446,6 +447,19 @@
     return td;
   };
 
+  GrDiffBuilder.prototype._textLength = function(text, tabSize) {
+    // TODO(andybons): Unicode support.
+    var numChars = 0;
+    for (var i = 0; i < text.length; i++) {
+      if (text[i] === '\t') {
+        numChars += tabSize;
+      } else {
+        numChars++;
+      }
+    }
+    return numChars;
+  };
+
   // Advance `index` by the appropriate number of characters that would
   // represent one source code character and return that index. For
   // example, for source code '<span>' the escaped html string is
@@ -553,10 +567,10 @@
     if (showTabs) {
       str += 'withIndicator';
     }
-    str += '" ';
+    str += '" style="';
     // TODO(andybons): CSS tab-size is not supported in IE.
-    str += 'style="tab-size:' + tabSize + ';';
-    str += 'style="-moz-tab-size:' + tabSize + ';';
+    str += 'tab-size:' + tabSize + ';';
+    str += '-moz-tab-size:' + tabSize + ';';
     str += '">\t</span>';
     return str;
   };
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder_test.html
index 2e30999..22b9072 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder_test.html
@@ -259,6 +259,11 @@
           '6789');
     });
 
+    test('text length with tabs', function() {
+      assert.equal(builder._textLength('12345', 4), 5);
+      assert.equal(builder._textLength('\t\t12', 4), 10);
+    });
+
     test('tab wrapper insertion', function() {
       var html = 'abc\tdef';
       var wrapper = builder._getTabWrapper(
@@ -278,7 +283,7 @@
       line.beforeNumber = 3;
       line.afterNumber = 5;
 
-      var comments = {left: [], right:[]};
+      var comments = {left: [], right: []};
       assert.deepEqual(builder._getCommentsForLine(comments, line), []);
       assert.deepEqual(builder._getCommentsForLine(comments, line,
           GrDiffBuilder.Side.LEFT), []);
@@ -323,7 +328,7 @@
         ],
       };
 
-      function checkThreadProps(patchNum, side, comments) {
+      function checkThreadProps(threadEl, patchNum, side, comments) {
         assert.equal(threadEl.changeNum, '42');
         assert.equal(threadEl.patchNum, patchNum);
         assert.equal(threadEl.path, '/path/to/foo');
@@ -335,27 +340,27 @@
       var line = new GrDiffLine(GrDiffLine.Type.BOTH);
       line.beforeNumber = 5;
       line.afterNumber = 5;
-      threadEl = builder._commentThreadForLine(line);
-      checkThreadProps('3', 'REVISION',
+      var threadEl = builder._commentThreadForLine(line);
+      checkThreadProps(threadEl, '3', 'REVISION',
           [{id: 'l5', line: 5}, {id: 'r5', line: 5}]);
 
       threadEl = builder._commentThreadForLine(line, GrDiffBuilder.Side.RIGHT);
-      checkThreadProps('3', 'REVISION', [{id: 'r5', line: 5}]);
+      checkThreadProps(threadEl, '3', 'REVISION', [{id: 'r5', line: 5}]);
 
       threadEl = builder._commentThreadForLine(line, GrDiffBuilder.Side.LEFT);
-      checkThreadProps('3', 'PARENT', [{id: 'l5', line: 5}]);
+      checkThreadProps(threadEl, '3', 'PARENT', [{id: 'l5', line: 5}]);
 
       builder._comments.meta.patchRange.basePatchNum = '1';
 
       threadEl = builder._commentThreadForLine(line);
-      checkThreadProps('3', 'REVISION',
+      checkThreadProps(threadEl, '3', 'REVISION',
           [{id: 'l5', line: 5}, {id: 'r5', line: 5}]);
 
       threadEl = builder._commentThreadForLine(line, GrDiffBuilder.Side.LEFT);
-      checkThreadProps('1', 'REVISION', [{id: 'l5', line: 5}]);
+      checkThreadProps(threadEl, '1', 'REVISION', [{id: 'l5', line: 5}]);
 
       threadEl = builder._commentThreadForLine(line, GrDiffBuilder.Side.RIGHT);
-      checkThreadProps('3', 'REVISION', [{id: 'r5', line: 5}]);
+      checkThreadProps(threadEl, '3', 'REVISION', [{id: 'r5', line: 5}]);
 
       builder._comments.meta.patchRange.basePatchNum = 'PARENT';
 
@@ -363,14 +368,14 @@
       line.beforeNumber = 5;
       line.afterNumber = 5;
       threadEl = builder._commentThreadForLine(line);
-      checkThreadProps('3', 'PARENT',
+      checkThreadProps(threadEl, '3', 'PARENT',
           [{id: 'l5', line: 5}, {id: 'r5', line: 5}]);
 
       line = new GrDiffLine(GrDiffLine.Type.ADD);
       line.beforeNumber = 3;
       line.afterNumber = 5;
       threadEl = builder._commentThreadForLine(line);
-      checkThreadProps('3', 'REVISION',
+      checkThreadProps(threadEl, '3', 'REVISION',
           [{id: 'l3', line: 3}, {id: 'r5', line: 5}]);
     });
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
index 1e7b0e4..7b1ce00 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -16,13 +16,8 @@
 
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../shared/gr-request/gr-request.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-
 <link rel="import" href="../gr-diff-comment-thread/gr-diff-comment-thread.html">
-<link rel="import" href="../gr-diff-preferences/gr-diff-preferences.html">
-<link rel="import" href="../gr-patch-range-select/gr-patch-range-select.html">
 
 <dom-module id="gr-diff">
   <template>
@@ -33,18 +28,6 @@
         --light-add-highlight-color: #efe;
         --dark-add-highlight-color: #d4ffd4;
       }
-      .loading {
-        padding: 0 var(--default-horizontal-margin) 1em;
-        color: #666;
-      }
-      .header {
-        display: flex;
-        justify-content: space-between;
-        margin: 0 var(--default-horizontal-margin) .75em;
-      }
-      .prefsButton {
-        text-align: right;
-      }
       .diffContainer {
         border-bottom: 1px solid #eee;
         border-top: 1px solid #eee;
@@ -148,33 +131,11 @@
         content: '\00BB';
       }
     </style>
-    <div class="loading" hidden$="[[!_loading]]">Loading...</div>
-    <div hidden$="[[_loading]]" hidden>
-      <div class="header">
-        <gr-patch-range-select
-            path="[[path]]"
-            change-num="[[changeNum]]"
-            patch-range="[[patchRange]]"
-            available-patches="[[availablePatches]]"></gr-patch-range-select>
-        <gr-button link
-           class="prefsButton"
-           on-tap="_handlePrefsTap"
-           hidden$="[[_computePrefsButtonHidden(_prefs, _loggedIn)]]"
-           hidden>Diff View Preferences</gr-button>
-      </div>
-      <gr-overlay id="prefsOverlay" with-backdrop>
-        <gr-diff-preferences
-            prefs="{{_prefs}}"
-            on-save="_handlePrefsSave"
-            on-cancel="_handlePrefsCancel"></gr-diff-preferences>
-      </gr-overlay>
-
-      <div class$="[[_computeContainerClass(_loggedIn, _viewMode)]]"
-          on-tap="_handleTap"
-          on-mousedown="_handleMouseDown"
-          on-copy="_handleCopy">
-        <table id="diffTable"></table>
-      </div>
+    <div class$="[[_computeContainerClass(_loggedIn, viewMode)]]"
+        on-tap="_handleTap"
+        on-mousedown="_handleMouseDown"
+        on-copy="_handleCopy">
+      <table id="diffTable"></table>
     </div>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
index 7e89f83..447d307 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -34,11 +34,10 @@
      */
 
     properties: {
-      availablePatches: Array,
       changeNum: String,
       patchRange: Object,
       path: String,
-
+      prefs: Object,
       projectConfig: {
         type: Object,
         observer: '_projectConfigChanged',
@@ -48,17 +47,12 @@
         type: Boolean,
         value: false,
       },
-      _loading: {
-        type: Boolean,
-        value: true,
-      },
-      _viewMode: {
+      viewMode: {
         type: String,
         value: DiffViewMode.SIDE_BY_SIDE,
       },
       _diff: Object,
       _diffBuilder: Object,
-      _prefs: Object,
       _selectionSide: {
         type: String,
         observer: '_selectionSideChanged',
@@ -75,7 +69,7 @@
     },
 
     observers: [
-      '_prefsChanged(_prefs.*)',
+      '_prefsChanged(prefs.*, viewMode)',
     ],
 
     attached: function() {
@@ -91,32 +85,24 @@
 
     reload: function() {
       this._clearDiffContent();
-      this._loading = true;
 
       var promises = [];
 
       promises.push(this._getDiff().then(function(diff) {
         this._diff = diff;
-        this._loading = false;
       }.bind(this)));
 
       promises.push(this._getDiffCommentsAndDrafts().then(function(comments) {
         this._comments = comments;
       }.bind(this)));
 
-      promises.push(this._getDiffPreferences().then(function(prefs) {
-        this._prefs = prefs;
-      }.bind(this)));
-
       return Promise.all(promises).then(function() {
-        this._render();
+        if (this.prefs) {
+          this._render();
+        }
       }.bind(this));
     },
 
-    showDiffPreferences: function() {
-      this.$.prefsOverlay.open();
-    },
-
     scrollToLine: function(lineNum) {
       if (isNaN(lineNum) || lineNum < 1) { return; }
 
@@ -192,7 +178,7 @@
           break;
         case DiffViewMode.SIDE_BY_SIDE:
           classes.push('sideBySide');
-          break
+          break;
         default:
           throw Error('Invalid view mode: ', viewMode);
       }
@@ -202,41 +188,6 @@
       return classes.join(' ');
     },
 
-    _computePrefsButtonHidden: function(prefs, loggedIn) {
-      return !loggedIn || !prefs;
-    },
-
-    _handlePrefsTap: function(e) {
-      e.preventDefault();
-      this.$.prefsOverlay.open();
-    },
-
-    _handlePrefsSave: function(e) {
-      e.stopPropagation();
-      var el = Polymer.dom(e).rootTarget;
-      el.disabled = true;
-      this._saveDiffPreferences().then(function(response) {
-        el.disabled = false;
-        if (!response.ok) {
-          alert('Oops. Something went wrong. Check the console and bug the ' +
-              'PolyGerrit team for assistance.');
-          return response.text().then(function(text) {
-            console.error(text);
-          });
-        }
-        this.$.prefsOverlay.close();
-      }.bind(this));
-    },
-
-    _saveDiffPreferences: function() {
-      return this.$.restAPI.saveDiffPreferences(this._prefs);
-    },
-
-    _handlePrefsCancel: function(e) {
-      e.stopPropagation();
-      this.$.prefsOverlay.close();
-    },
-
     _handleTap: function(e) {
       var el = Polymer.dom(e).rootTarget;
 
@@ -355,6 +306,9 @@
     },
 
     _handleCopy: function(e) {
+      if (!e.target.classList.contains('content')) {
+        return;
+      }
       var text = this._getSelectedText(this._selectionSide);
       e.clipboardData.setData('Text', text);
       e.preventDefault();
@@ -399,7 +353,7 @@
     _render: function() {
       this._clearDiffContent();
       this._builder = this._getDiffBuilder(this._diff, this._comments,
-          this._prefs);
+          this.prefs);
       this._builder.emitDiff(this._diff.content);
 
       this.async(function() {
@@ -457,32 +411,6 @@
       }).then(this._normalizeDiffCommentsAndDrafts.bind(this));
     },
 
-    _getDiffPreferences: function() {
-      return this._getLoggedIn().then(function(loggedIn) {
-        if (!loggedIn) {
-          // These defaults should match the defaults in
-          // gerrit-extension-api/src/main/jcg/gerrit/extensions/client/DiffPreferencesInfo.java
-          // NOTE: There are some settings that don't apply to PolyGerrit
-          // (Render mode being at least one of them).
-          return Promise.resolve({
-            auto_hide_diff_table_header: true,
-            context: 10,
-            cursor_blink_rate: 0,
-            ignore_whitespace: 'IGNORE_NONE',
-            intraline_difference: true,
-            line_length: 100,
-            show_line_endings: true,
-            show_tabs: true,
-            show_whitespace_errors: true,
-            syntax_highlighting: true,
-            tab_size: 8,
-            theme: 'DEFAULT',
-          });
-        }
-        return this.$.restAPI.getDiffPreferences();
-      }.bind(this));
-    },
-
     _normalizeDiffCommentsAndDrafts: function(results) {
       function markAsDraft(d) {
         d.__draft = true;
@@ -507,14 +435,14 @@
     },
 
     _getDiffBuilder: function(diff, comments, prefs) {
-      if (this._viewMode === DiffViewMode.SIDE_BY_SIDE) {
+      if (this.viewMode === DiffViewMode.SIDE_BY_SIDE) {
         return new GrDiffBuilderSideBySide(diff, comments, prefs,
             this.$.diffTable);
-      } else if (this._viewMode === DiffViewMode.UNIFIED) {
+      } else if (this.viewMode === DiffViewMode.UNIFIED) {
         return new GrDiffBuilderUnified(diff, comments, prefs,
             this.$.diffTable);
       }
-      throw Error('Unsupported diff view mode: ' + this._viewMode);
+      throw Error('Unsupported diff view mode: ' + this.viewMode);
     },
 
     _projectConfigChanged: function(projectConfig) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
index 579957e..f02b861 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
@@ -37,7 +37,7 @@
     setup(function() {
       stub('gr-rest-api-interface', {
         getLoggedIn: function() { return Promise.resolve(false); },
-      })
+      });
       element = fixture('basic');
     });
 
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index 723766d..0d6eaa8 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -18,6 +18,7 @@
 <link rel="import" href="../behaviors/keyboard-shortcut-behavior.html">
 <link rel="import" href="../styles/app-theme.html">
 
+<link rel="import" href="./core/gr-error-manager/gr-error-manager.html">
 <link rel="import" href="./core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html">
 <link rel="import" href="./core/gr-main-header/gr-main-header.html">
 <link rel="import" href="./core/gr-router/gr-router.html">
@@ -36,17 +37,21 @@
   <template>
     <style>
       :host {
-        background-color: var(--secondary-color);
         display: flex;
         min-height: 100vh;
         flex-direction: column;
       }
       gr-main-header,
       footer {
-        background-color: var(--primary-color);
         color: var(--primary-text-color);
         padding: .5rem var(--default-horizontal-margin);
       }
+      gr-main-header {
+        background-color: var(--header-background-color, #eee);
+      }
+      footer {
+        background-color: var(--footer-background-color, #eee);
+      }
       main {
         flex: 1;
         position: relative;
@@ -85,7 +90,6 @@
         <gr-change-list-view
             params="[[params]]"
             view-state="{{_viewState.changeListView}}"
-            changes-per-page="[[_preferences.changes_per_page]]"
             logged-in="[[_computeLoggedIn(_account)]]"></gr-change-list-view>
       </template>
       <template is="dom-if" if="[[_showDashboardView]]" restamp="true">
@@ -133,6 +137,7 @@
           view="[[params.view]]"
           on-close="_handleKeyboardShortcutDialogClose"></gr-keyboard-shortcuts-dialog>
     </gr-overlay>
+    <gr-error-manager></gr-error-manager>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-app.js"></script>
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js
index 3495218..684120e 100644
--- a/polygerrit-ui/app/elements/gr-app.js
+++ b/polygerrit-ui/app/elements/gr-app.js
@@ -30,7 +30,6 @@
       },
       _serverConfig: Object,
       _version: String,
-      _preferences: Object,
       _showChangeListView: Boolean,
       _showDashboardView: Boolean,
       _showChangeView: Boolean,
@@ -46,16 +45,13 @@
 
     observers: [
       '_viewChanged(params.view)',
+      '_loadPlugins(_serverConfig.plugin.js_resource_paths)',
     ],
 
     behaviors: [
       Gerrit.KeyboardShortcutBehavior,
     ],
 
-    get loggedIn() {
-      return !!(this._account && Object.keys(this._account).length > 0);
-    },
-
     attached: function() {
       this.$.restAPI.getAccount().then(function(account) {
         this._account = account;
@@ -72,9 +68,10 @@
       this._viewState = {
         changeView: {
           changeNum: null,
-          patchNum: null,
+          patchRange: null,
           selectedFileIndex: 0,
           showReplyDialog: false,
+          diffMode: null,
         },
         changeListView: {
           query: null,
@@ -88,17 +85,9 @@
     },
 
     _accountChanged: function(account) {
-      if (this.loggedIn) {
-        this.$.restAPI.getPreferences().then(function(preferences) {
-          this._preferences = preferences;
-        }.bind(this));
-        // Diff preferences are cached; warm it before a diff is rendered.
-        this.$.restAPI.getDiffPreferences();
-      } else {
-        this._preferences = {
-          changes_per_page: 25,
-        };
-      }
+      // Preferences are cached when a user is logged in; warm them.
+      this.$.restAPI.getPreferences();
+      this.$.restAPI.getDiffPreferences();
     },
 
     _viewChanged: function(view) {
@@ -109,6 +98,15 @@
       this.set('_showDiffView', view === 'gr-diff-view');
     },
 
+    _loadPlugins: function(plugins) {
+      for (var i = 0; i < plugins.length; i++) {
+        var scriptEl = document.createElement('script');
+        scriptEl.defer = true;
+        scriptEl.src = '/' + plugins[i];
+        document.body.appendChild(scriptEl);
+      }
+    },
+
     _loginTapHandler: function(e) {
       e.preventDefault();
       page.show('/login/' + encodeURIComponent(
@@ -117,7 +115,7 @@
 
     // Argument used for binding update only.
     _computeLoggedIn: function(account) {
-      return this.loggedIn;
+      return !!(account && Object.keys(account).length > 0);
     },
 
     _handlePageError: function(e) {
diff --git a/polygerrit-ui/app/elements/shared/gr-ajax/gr-ajax.html b/polygerrit-ui/app/elements/shared/gr-ajax/gr-ajax.html
deleted file mode 100644
index 9a93426..0000000
--- a/polygerrit-ui/app/elements/shared/gr-ajax/gr-ajax.html
+++ /dev/null
@@ -1,35 +0,0 @@
-<!--
-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.
--->
-
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-ajax/iron-ajax.html">
-
-<dom-module id="gr-ajax">
-  <template>
-    <iron-ajax id="xhr"
-        auto="[[auto]]"
-        url="[[url]]"
-        params="[[params]]"
-        json-prefix=")]}'"
-        last-error="{{lastError}}"
-        last-response="{{lastResponse}}"
-        loading="{{loading}}"
-        on-response="_handleResponse"
-        on-error="_handleError"
-        debounce-duration="300"></iron-ajax>
-  </template>
-  <script src="gr-ajax.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-ajax/gr-ajax.js b/polygerrit-ui/app/elements/shared/gr-ajax/gr-ajax.js
deleted file mode 100644
index 7fec507..0000000
--- a/polygerrit-ui/app/elements/shared/gr-ajax/gr-ajax.js
+++ /dev/null
@@ -1,81 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-(function() {
-  'use strict';
-
-  Polymer({
-    is: 'gr-ajax',
-
-    /**
-     * Fired when a response is received.
-     *
-     * @event response
-     */
-
-    /**
-     * Fired when an error is received.
-     *
-     * @event error
-     */
-
-    hostAttributes: {
-      hidden: true
-    },
-
-    properties: {
-      auto: {
-        type: Boolean,
-        value: false,
-      },
-      url: String,
-      params: {
-        type: Object,
-        value: function() {
-          return {};
-        },
-      },
-      lastError: {
-        type: Object,
-        notify: true,
-      },
-      lastResponse: {
-        type: Object,
-        notify: true,
-      },
-      loading: {
-        type: Boolean,
-        notify: true,
-      },
-    },
-
-    ready: function() {
-      // Used for debugging which element a request came from.
-      var headers = this.$.xhr.headers;
-      headers['x-requesting-element-id'] = this.id || 'gr-ajax (no id)';
-      this.$.xhr.headers = headers;
-    },
-
-    generateRequest: function() {
-      return this.$.xhr.generateRequest();
-    },
-
-    _handleResponse: function(e, req) {
-      this.fire('response', req, {bubbles: false});
-    },
-
-    _handleError: function(e, req) {
-      this.fire('error', req, {bubbles: false});
-    },
-  });
-})();
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html
new file mode 100644
index 0000000..140fbaa
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html
@@ -0,0 +1,66 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../gr-button/gr-button.html">
+
+<dom-module id="gr-alert">
+  <template>
+    <style>
+      /**
+       * ALERT: DO NOT ADD TRANSITION PROPERTIES WITHOUT PROPERLY UNDERSTANDING
+       * HOW THEY ARE USED IN THE CODE.
+       */
+      :host([toast]) {
+        background-color: #333;
+        bottom: 1.25rem;
+        border-radius: 3px;
+        box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
+        color: #fff;
+        left: 1.25rem;
+        padding: 1em 1.5em;
+        position: fixed;
+        transform: translateY(5rem);
+        transition: transform var(--gr-alert-transition-duration, 80ms) ease-out;
+      }
+      :host([shown]) {
+        transform: translateY(0);
+      }
+      .text {
+        display: inline-block;
+        max-width: 25em;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        vertical-align: bottom;
+        white-space: nowrap;
+      }
+      .action {
+        color: #a1c2fa;
+        font-weight: bold;
+        margin-left: 1em;
+        text-decoration: none;
+      }
+    </style>
+    <span class="text">[[text]]</span>
+    <gr-button
+        link
+        class="action"
+        hidden$="[[_hideActionButton]]"
+        on-tap="_handleActionTap">[[actionText]]</gr-button>
+  </template>
+  <script src="gr-alert.js"></script>
+</dom-module>
+
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
new file mode 100644
index 0000000..a3e933f
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
@@ -0,0 +1,90 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-alert',
+
+    /**
+     * Fired when the action button is pressed.
+     *
+     * @event action
+     */
+
+    properties: {
+      text: String,
+      actionText: String,
+      shown: {
+        type: Boolean,
+        value: true,
+        readOnly: true,
+        reflectToAttribute: true,
+      },
+      toast: {
+        type: Boolean,
+        value: true,
+        reflectToAttribute: true,
+      },
+
+      _hideActionButton: Boolean,
+      _boundTransitionEndHandler: {
+        type: Function,
+        value: function() { return this._handleTransitionEnd.bind(this); },
+      },
+    },
+
+    attached: function() {
+      this.addEventListener('transitionend', this._boundTransitionEndHandler);
+    },
+
+    detached: function() {
+      this.removeEventListener('transitionend',
+          this._boundTransitionEndHandler);
+    },
+
+    show: function(text, opt_actionText) {
+      this.text = text;
+      this.actionText = opt_actionText;
+      this._hideActionButton = !opt_actionText
+      document.body.appendChild(this);
+      this._setShown(true);
+    },
+
+    hide: function() {
+      this._setShown(false);
+      if (this._hasZeroTransitionDuration()) {
+        document.body.removeChild(this);
+      }
+    },
+
+    _hasZeroTransitionDuration: function() {
+      var style = window.getComputedStyle(this);
+      // transitionDuration is always given in seconds.
+      var duration = Math.round(parseFloat(style.transitionDuration) * 100);
+      return duration === 0;
+    },
+
+    _handleTransitionEnd: function(e) {
+      if (this.shown) { return; }
+
+      document.body.removeChild(this);
+    },
+
+    _handleActionTap: function(e) {
+      e.preventDefault();
+      this.fire('action', null, {bubbles: false});
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.html b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.html
new file mode 100644
index 0000000..067ac5b
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-alert</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-alert.html">
+
+<script>
+  suite('gr-alert tests', function() {
+    var element;
+
+    setup(function() {
+      element = document.createElement('gr-alert');
+    });
+
+    teardown(function() {
+      if (element.parentNode) {
+        element.parentNode.removeChild(element);
+      }
+    });
+
+    test('show/hide', function() {
+      assert.isNull(element.parentNode);
+      element.show();
+      assert.equal(element.parentNode, document.body);
+      element.customStyle['--gr-alert-transition-duration'] = '0ms';
+      element.updateStyles();
+      element.hide();
+      assert.isNull(element.parentNode);
+    });
+
+    test('action event', function(done) {
+      element.show();
+      element.addEventListener('action', function() {
+        done();
+      });
+      MockInteractions.tap(element.$$('.action'));
+    });
+
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
index 3655975..17a0ed9 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
@@ -57,6 +57,12 @@
 
     _buildAvatarURL: function(account) {
       if (!account) { return ''; }
+      var avatars = account.avatars || [];
+      for (var i = 0; i < avatars.length; i++) {
+        if (avatars[i].height === this.imageSize) {
+          return avatars[i].url;
+        }
+      }
       return '/accounts/' + account._account_id + '/avatar?s=' + this.imageSize;
     },
   });
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
index b308a38..b55a1c3 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
@@ -46,18 +46,21 @@
     });
 
     test('dom for existing account', function() {
-      assert.isTrue(element.hasAttribute('hidden'), 'element not hidden initially');
+      assert.isTrue(element.hasAttribute('hidden'),
+          'element not hidden initially');
       element.hidden = false;
       element.imageSize = 64;
       element.account = {
         _account_id: 123
       };
       assert.isFalse(element.hasAttribute('hidden'), 'element hidden');
-      assert.isTrue(element.style.backgroundImage.indexOf('/accounts/123/avatar?s=64') > -1);
+      assert.isTrue(element.style.backgroundImage.indexOf(
+          '/accounts/123/avatar?s=64') > -1);
     });
 
     test('dom for non available account', function() {
-      assert.isTrue(element.hasAttribute('hidden'), 'element not hidden initially');
+      assert.isTrue(element.hasAttribute('hidden'),
+          'element not hidden initially');
       element.account = undefined;
       assert.isTrue(element.hasAttribute('hidden'), 'element not hidden');
     });
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
index 1df98fd..c815ffd 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
@@ -15,6 +15,7 @@
 -->
 
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html">
 
 <dom-module id="gr-button">
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
index 772fccc..e109896 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
@@ -31,6 +31,7 @@
 
     behaviors: [
       Gerrit.KeyboardShortcutBehavior,
+      Gerrit.TooltipBehavior,
     ],
 
     hostAttributes: {
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.html b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.html
index 62a9d2d..05fe10b 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.html
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.html
@@ -15,7 +15,7 @@
 -->
 
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../gr-request/gr-request.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
 <dom-module id="gr-change-star">
   <template>
@@ -46,6 +46,7 @@
       <!-- Public Domain image from the Noun Project: https://thenounproject.com/search/?q=star&i=25969 -->
       <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"><path d="M26.439,95.601c-5.608,2.949-9.286,0.276-8.216-5.968l4.5-26.237L3.662,44.816c-4.537-4.423-3.132-8.746,3.137-9.657  l26.343-3.829L44.923,7.46c2.804-5.682,7.35-5.682,10.154,0l11.78,23.87l26.343,3.829c6.27,0.911,7.674,5.234,3.138,9.657  L77.277,63.397l4.501,26.237c1.07,6.244-2.608,8.916-8.216,5.968L50,83.215L26.439,95.601z"></path></svg>
     </button>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-change-star.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
index 26680b6..23c56b4 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
@@ -35,27 +35,10 @@
     },
 
     _handleStarTap: function() {
-      var method = this.change.starred ? 'DELETE' : 'PUT';
-      this.set('change.starred', !this.change.starred);
-      this._send(method, this._restEndpoint()).catch(function(err) {
-        this.set('change.starred', !this.change.starred);
-        alert('Change couldn’t be starred. Check the console and contact ' +
-            'the PolyGerrit team for assistance.');
-        throw err;
-      }.bind(this));
-    },
-
-    _send: function(method, url) {
-      var xhr = document.createElement('gr-request');
-      this._xhrPromise = xhr.send({
-        method: method,
-        url: url,
-      });
-      return this._xhrPromise;
-    },
-
-    _restEndpoint: function() {
-      return '/accounts/self/starred.changes/' + this.change._number;
+      var newVal = !this.change.starred;
+      this.set('change.starred', newVal);
+      this._xhrPromise = this.$.restAPI.saveChangeStarred(this.change._number,
+          newVal);
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html
index 8b13a86..969f7dd 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html
@@ -20,8 +20,6 @@
 
 <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
 <script src="../../../bower_components/web-component-tester/browser.js"></script>
-<script src="../../../bower_components/page/page.js"></script>
-<script src="../../../scripts/util.js"></script>
 
 <link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
 <link rel="import" href="gr-change-star.html">
@@ -35,39 +33,16 @@
 <script>
   suite('gr-change-star tests', function() {
     var element;
-    var server;
 
     setup(function() {
+      stub('gr-rest-api-interface', {
+        saveChangeStarred: function() { return Promise.resolve({ok: true}); },
+      });
       element = fixture('basic');
       element.change = {
         _number: 2,
         starred: true,
       };
-
-      server = sinon.fakeServer.create();
-      server.respondWith(
-        'PUT',
-        '/accounts/self/starred.changes/2',
-        [
-          204,
-          {'Content-Type': 'application/json'},
-          ''
-        ]
-      );
-
-      server.respondWith(
-        'DELETE',
-        '/accounts/self/starred.changes/2',
-        [
-          204,
-          {'Content-Type': 'application/json'},
-          ''
-        ]
-      );
-    });
-
-    teardown(function() {
-      server.restore();
     });
 
     test('star visibility states', function() {
@@ -86,11 +61,7 @@
       element.set('change.starred', false);
       MockInteractions.tap(element.$$('button'));
 
-      server.respond();
-
       element._xhrPromise.then(function(req) {
-        assert.equal(req.status, 204);
-        assert.equal(req.url, '/accounts/self/starred.changes/2');
         assert.equal(element.change.starred, true);
         done();
       });
@@ -100,11 +71,7 @@
       element.set('change.starred', true);
       MockInteractions.tap(element.$$('button'));
 
-      server.respond();
-
       element._xhrPromise.then(function(req) {
-        assert.equal(req.status, 204);
-        assert.equal(req.url, '/accounts/self/starred.changes/2');
         assert.equal(element.change.starred, false);
         done();
       });
diff --git a/polygerrit-ui/app/elements/shared/gr-request/gr-request.html b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.html
similarity index 69%
rename from polygerrit-ui/app/elements/shared/gr-request/gr-request.html
rename to polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.html
index df9eddc..f213312 100644
--- a/polygerrit-ui/app/elements/shared/gr-request/gr-request.html
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.html
@@ -1,5 +1,5 @@
 <!--
-Copyright (C) 2015 The Android Open Source Project
+Copyright (C) 2016 The Android Open Source Project
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -15,11 +15,8 @@
 -->
 
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-ajax/iron-request.html">
 
-<dom-module id="gr-request">
-  <template>
-    <iron-request id="xhr"></iron-request>
-  </template>
-  <script src="gr-request.js"></script>
+<dom-module id="gr-cursor-manager">
+  <template></template>
+  <script src="gr-cursor-manager.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
new file mode 100644
index 0000000..fe1605c
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
@@ -0,0 +1,198 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-cursor-manager',
+
+    properties: {
+      stops: {
+        type: Array,
+        value: function() {
+          return [];
+        },
+        observer: '_updateIndex',
+      },
+      target: {
+        type: Object,
+        notify: true,
+        observer: '_scrollToTarget',
+      },
+
+      /**
+       * The index of the current target (if any). -1 otherwise.
+       */
+      index: {
+        type: Number,
+        value: -1,
+      },
+
+      /**
+       * The class to apply to the current target. Use null for no class.
+       */
+      cursorTargetClass: {
+        type: String,
+        value: null,
+      },
+      scroll: {
+        type: Boolean,
+        value: false,
+      },
+    },
+
+    detached: function() {
+      this.unsetCursor();
+    },
+
+    next: function(opt_condition) {
+      this._moveCursor(1, opt_condition);
+    },
+
+    previous: function(opt_condition) {
+      this._moveCursor(-1, opt_condition);
+    },
+
+    /**
+     * Set the cursor to an arbitrary element.
+     * @param {DOMElement}
+     */
+    setCursor: function(element) {
+      this.unsetCursor();
+      this.target = element;
+      this._updateIndex();
+      this._decorateTarget();
+    },
+
+    unsetCursor: function() {
+      this._unDecorateTarget();
+      this.index = -1;
+      this.target = null;
+    },
+
+    isAtStart: function() {
+      return this.index === 0;
+    },
+
+    isAtEnd: function() {
+      return this.index === this.stops.length - 1;
+    },
+
+    moveToStart: function() {
+      if (this.stops.length) {
+        this.setCursor(this.stops[0]);
+      }
+    },
+
+    /**
+     * Move the cursor forward or backward by delta. Noop if moving past either
+     * end of the stop list.
+     * @param {Number} delta: either -1 or 1.
+     * @param {Function} opt_condition Optional stop condition. If a condition
+     *    is passed the cursor will continue to move in the specified direction
+     *    until the condition is met.
+     * @private
+     */
+    _moveCursor: function(delta, opt_condition) {
+      if (!this.stops.length) {
+        this.unsetCursor();
+        return;
+      }
+
+      this._unDecorateTarget();
+
+      var newIndex = this._getNextindex(delta, opt_condition);
+
+      var newTarget = null;
+      if (newIndex != -1) {
+        newTarget = this.stops[newIndex];
+      }
+
+      this.index = newIndex;
+      this.target = newTarget;
+
+      this._decorateTarget();
+    },
+
+    _decorateTarget: function() {
+      if (this.target && this.cursorTargetClass) {
+        this.target.classList.add(this.cursorTargetClass);
+      }
+    },
+
+    _unDecorateTarget: function() {
+      if (this.target && this.cursorTargetClass) {
+        this.target.classList.remove(this.cursorTargetClass);
+      }
+    },
+
+    /**
+     * Get the next stop index indicated by the delta direction.
+     * @param {Number} delta: either -1 or 1.
+     * @param {Function} opt_condition Optional stop condition.
+     * @return {Number} the new index.
+     * @private
+     */
+    _getNextindex: function(delta, opt_condition) {
+      if (!this.stops.length || this.index === -1) {
+        return -1;
+      }
+
+      var newIndex = this.index;
+      do {
+        newIndex = newIndex + delta;
+      } while(newIndex !== 0 &&
+              newIndex !== this.stops.length - 1 &&
+              opt_condition &&
+              !opt_condition(this.stops[newIndex]));
+
+      newIndex = Math.max(0, Math.min(this.stops.length - 1, newIndex));
+
+      return newIndex;
+    },
+
+    _updateIndex: function() {
+      if (!this.target) {
+        this.index = -1;
+        return;
+      }
+
+      var newIndex = Array.prototype.indexOf.call(this.stops, this.target);
+      if (newIndex === -1) {
+        this.unsetCursor();
+      } else {
+        this.index = newIndex;
+      }
+    },
+
+    _scrollToTarget: function() {
+      if (!this.target || !this.scroll) { return; }
+
+      // Calculate where the element is relative to the window.
+      var top = this.target.offsetTop;
+      for (var offsetParent = this.target.offsetParent;
+           offsetParent;
+           offsetParent = offsetParent.offsetParent) {
+        top += offsetParent.offsetTop;
+      }
+
+      // Scroll the element to the middle of the window. Dividing by a third
+      // instead of half the inner height feels a bit better otherwise the
+      // element appears to be below the center of the window even when it
+      // isn't.
+      window.scrollTo(0, top - (window.innerHeight / 3) +
+          (this.target.offsetHeight / 2));
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html
new file mode 100644
index 0000000..1ad014d
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html
@@ -0,0 +1,124 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-cursor-manager</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-cursor-manager.html">
+
+<test-fixture id="basic">
+  <template>
+    <gr-cursor-manager
+        cursor-stop-selector="li"
+        cursor-target-class="targeted"></gr-cursor-manager>
+    <ul>
+      <li>A</li>
+      <li>B</li>
+      <li>C</li>
+      <li>D</li>
+    </ul>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-cursor-manager tests', function() {
+    var element;
+    var list;
+
+    setup(function() {
+      var fixtureElements = fixture('basic');
+      element = fixtureElements[0];
+      list = fixtureElements[1];
+    });
+
+    test('core cursor functionality', function() {
+      // The element is initialized into the proper state.
+      assert.isArray(element.stops);
+      assert.equal(element.stops.length, 0);
+      assert.equal(element.index, -1);
+      assert.isNotOk(element.target);
+
+      // Initialize the cursor with its stops.
+      element.stops = list.querySelectorAll('li');
+
+      // It should have the stops but it should not be targeting any of them.
+      assert.isNotNull(element.stops);
+      assert.equal(element.stops.length, 4);
+      assert.equal(element.index, -1);
+      assert.isNotOk(element.target);
+
+      // Select the third stop.
+      element.setCursor(list.children[2]);
+
+      // It should update its internal state and update the element's class.
+      assert.equal(element.index, 2);
+      assert.equal(element.target, list.children[2]);
+      assert.isTrue(list.children[2].classList.contains('targeted'));
+      assert.isFalse(element.isAtStart());
+      assert.isFalse(element.isAtEnd());
+
+      // Progress the cursor.
+      element.next();
+
+      // Confirm that the next stop is selected and that the previous stop is
+      // unselected.
+      assert.equal(element.index, 3);
+      assert.equal(element.target, list.children[3]);
+      assert.isTrue(element.isAtEnd());
+      assert.isFalse(list.children[2].classList.contains('targeted'));
+      assert.isTrue(list.children[3].classList.contains('targeted'));
+
+      // Progress the cursor.
+      element.next();
+
+      // We should still be at the end.
+      assert.equal(element.index, 3);
+      assert.equal(element.target, list.children[3]);
+      assert.isTrue(element.isAtEnd());
+
+      // Wind the cursor all the way back to the first stop.
+      element.previous();
+      element.previous();
+      element.previous();
+
+      // The element state should reflect the end of the list.
+      assert.equal(element.index, 0);
+      assert.equal(element.target, list.children[0]);
+      assert.isTrue(element.isAtStart());
+      assert.isTrue(list.children[0].classList.contains('targeted'));
+
+      var newLi = document.createElement('li');
+      newLi.textContent = 'Z';
+      list.insertBefore(newLi, list.children[0]);
+      element.stops = list.querySelectorAll('li');
+
+      assert.equal(element.index, 1);
+
+      // De-select all targets.
+      element.unsetCursor();
+
+      // There should now be no cursor target.
+      assert.isFalse(list.children[1].classList.contains('targeted'));
+      assert.isNotOk(element.target);
+      assert.equal(element.index, -1);
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
index a77d4e7..d1886e7 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
@@ -69,11 +69,12 @@
     }
 
     suite('24 hours time format preference', function() {
-      setup(function() {
+      setup(function(done) {
         return stubRestAPI(
           {time_format: 'HHMM_24', relative_date_in_change_table: false}
         ).then(function() {
           element = fixture('basic');
+          element._loadPreferences().then(function() { done(); });
         });
       });
 
@@ -108,12 +109,13 @@
     });
 
     suite('12 hours time format preference', function() {
-      setup(function() {
+      setup(function(done) {
         // relative_date_in_change_table is not set when false.
         return stubRestAPI(
           {time_format: 'HHMM_12'}
         ).then(function() {
           element = fixture('basic');
+          element._loadPreferences().then(function() { done(); });
         });
       });
 
@@ -130,7 +132,7 @@
           {time_format: 'HHMM_12', relative_date_in_change_table: true}
         ).then(function() {
           element = fixture('basic');
-          done();
+          element._loadPreferences().then(function() { done(); });
         });
       });
 
@@ -153,16 +155,13 @@
           {time_format: 'HHMM_12', relative_date_in_change_table: true}
         ).then(function() {
           element = fixture('basic');
-          done();
+          element._loadPreferences().then(function() { done(); });
         });
       });
 
-      test('Preferences are respected', function(done) {
-        element._loadPreferences().then(function() {
-          assert.equal(element._timeFormat, 'h:mm A');
-          assert.isTrue(element._relative);
-          done();
-        });
+      test('Preferences are respected', function() {
+        assert.equal(element._timeFormat, 'h:mm A');
+        assert.isTrue(element._relative);
       });
     });
 
@@ -170,16 +169,13 @@
       setup(function(done) {
         return stubRestAPI(null).then(function() {
           element = fixture('basic');
-          done();
+          element._loadPreferences().then(function() { done(); });
         });
       });
 
-      test('Default preferences are respected', function(done) {
-        element._loadPreferences().then(function() {
-          assert.equal(element._timeFormat, 'H:mm');
-          assert.isFalse(element._relative);
-          done();
-        });
+      test('Default preferences are respected', function() {
+        assert.equal(element._timeFormat, 'H:mm');
+        assert.isFalse(element._relative);
       });
     });
   });
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
index d87d572..147ef9f 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
@@ -82,7 +82,7 @@
     _handleShowChange: function(detail) {
       this._getEventCallbacks(EventType.SHOW_CHANGE).forEach(function(cb) {
         var change = detail.change;
-        var patchNum = detail.patchNum
+        var patchNum = detail.patchNum;
         var revision;
         for (var rev in change.revisions) {
           if (change.revisions[rev]._number == patchNum) {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
index 2fede7c..c10c73c 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
@@ -35,7 +35,8 @@
 
     setup(function() {
       element = fixture('basic');
-      Gerrit.install(function(p) { plugin = p; });
+      Gerrit.install(function(p) { plugin = p; },
+          'http://test.com/plugins/testplugin/static/test.js');
     });
 
     teardown(function() {
@@ -43,6 +44,12 @@
       plugin = null;
     });
 
+    test('url', function() {
+      assert.equal(plugin.url(), 'http://test.com/plugins/testplugin/');
+      assert.equal(plugin.url('/static/test.js'),
+          'http://test.com/plugins/testplugin/static/test.js');
+    });
+
     test('history event', function(done) {
       plugin.on(element.EventType.HISTORY, function(path) {
         assert.equal(path, '/path/to/awesomesauce');
@@ -56,8 +63,8 @@
       var testChange = {
         _number: 42,
         revisions: {
-          def: { _number: 2 },
-          abc: { _number: 1 },
+          def: {_number: 2},
+          abc: {_number: 1},
         },
       };
       plugin.on(element.EventType.SHOW_CHANGE, function(change, revision) {
@@ -70,7 +77,7 @@
     });
 
     test('comment event', function(done) {
-      var testCommentNode = { foo: 'bar' };
+      var testCommentNode = {foo: 'bar'};
       plugin.on(element.EventType.COMMENT, function(commentNode) {
         assert.deepEqual(commentNode, testCommentNode);
         done();
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
index b5c84d3..be79d1ed 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
@@ -14,15 +14,42 @@
 (function(window) {
   'use strict';
 
-  function Plugin() {}
+  // GWT JSNI uses $wnd to refer to window.
+  // http://www.gwtproject.org/doc/latest/DevGuideCodingBasicsJSNI.html
+  window.$wnd = window;
+
+  function Plugin(opt_url) {
+    this._url = new URL(opt_url);
+    if (this._url.pathname.indexOf('/plugins') !== 0) {
+      console.warn('Plugin not being loaded from /plugins base path:',
+          this._url.href, '— Unable to determine name.');
+      return;
+    }
+    this._name = this._url.pathname.split('/')[2];
+  }
+
+  Plugin.prototype._name = '';
+
+  Plugin.prototype.getPluginName = function() {
+    return this._name;
+  };
 
   Plugin.prototype.on = function(eventName, callback) {
     document.createElement('gr-js-api-interface').addEventCallback(eventName,
         callback);
   };
 
+  Plugin.prototype.url = function(opt_path) {
+    return this._url.origin + '/plugins/' + this._name + (opt_path || '/');
+  };
+
   var Gerrit = window.Gerrit || {};
 
+  Gerrit.getPluginName = function() {
+    console.warn('Gerrit.getPluginName is not supported in PolyGerrit.',
+        'Please use self.getPluginName() instead.');
+  };
+
   Gerrit.css = function(rulesStr) {
     if (!Gerrit._customStyleSheet) {
       var styleEl = document.createElement('style');
@@ -33,10 +60,16 @@
     var name = '__pg_js_api_class_' + Gerrit._customStyleSheet.cssRules.length;
     Gerrit._customStyleSheet.insertRule('.' + name + '{' + rulesStr + '}', 0);
     return name;
-  },
+  };
 
-  Gerrit.install = function(callback) {
-    callback(new Plugin());
+  Gerrit.install = function(callback, opt_src) {
+    // TODO(andybons): Polyfill currentScript for IE10/11 (edge supports it).
+    var src = opt_src || (document.currentScript && document.currentScript.src);
+    callback(new Plugin(src));
+  };
+
+  Gerrit.installGwt = function() {
+    // NOOP since PolyGerrit doesn’t support GWT plugins.
   };
 
   window.Gerrit = Gerrit;
diff --git a/polygerrit-ui/app/elements/shared/gr-request/gr-request.js b/polygerrit-ui/app/elements/shared/gr-request/gr-request.js
deleted file mode 100644
index be24344..0000000
--- a/polygerrit-ui/app/elements/shared/gr-request/gr-request.js
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-(function() {
-  'use strict';
-
-  Polymer({
-    is: 'gr-request',
-
-    hostAttributes: {
-      hidden: true
-    },
-
-    send: function(options) {
-      options.headers = options.headers || {};
-      if (options.body != null) {
-        options.headers['content-type'] =
-            options.headers['content-type'] || 'application/json';
-      }
-      options.headers['x-gerrit-auth'] = options.headers['x-gerrit-auth'] ||
-          util.getCookie('XSRF_TOKEN');
-      options.jsonPrefix = options.jsonPrefix || ')]}\'';
-      return this.$.xhr.send(options);
-    },
-  });
-})();
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index c84c3e8..3795aa8 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -68,6 +68,18 @@
   Polymer({
     is: 'gr-rest-api-interface',
 
+    /**
+     * Fired when an server error occurs.
+     *
+     * @event server-error
+     */
+
+    /**
+     * Fired when a network error occurs.
+     *
+     * @event network-error
+     */
+
     properties: {
       _cache: {
         type: Object,
@@ -84,49 +96,56 @@
       opt_opts = opt_opts || {};
 
       var fetchOptions = {
-        credentials: (opt_opts.noCredentials ? undefined : 'same-origin'),
+        credentials: 'same-origin',
         headers: opt_opts.headers,
       };
 
-      var urlWithParams = url;
-      if (opt_params) {
-        var params = [];
-        for (var p in opt_params) {
-          if (opt_params[p] == null) {
-            params.push(encodeURIComponent(p));
-            continue;
-          }
-          params.push(
-            encodeURIComponent(p) + '=' +
-            encodeURIComponent(opt_params[p]));
-        }
-        // Sorting the params leaves the order deterministic which is easier
-        // to test.
-        urlWithParams += '?' + params.sort().join('&');
-      }
-
+      var urlWithParams = this._urlWithParams(url, opt_params);
       return fetch(urlWithParams, fetchOptions).then(function(response) {
         if (opt_cancelCondition && opt_cancelCondition()) {
           response.body.cancel();
           return;
         }
 
-        if (!response.ok && opt_errFn) {
-          opt_errFn.call(null, response);
-          return undefined;
+        if (!response.ok) {
+          if (opt_errFn) {
+            opt_errFn.call(null, response);
+            return undefined;
+          }
+          this.fire('server-error', {response: response});
         }
+
         return this.getResponseObject(response);
       }.bind(this)).catch(function(err) {
-        if (opt_opts.noCredentials) {
-          throw err;
+        if (opt_errFn) {
+          opt_errFn.call(null, null, err);
         } else {
-          // This could be because of a 302 auth redirect. Retry the request.
-          return this.fetchJSON(url, opt_errFn, opt_cancelCondition, opt_params,
-              Object.assign(opt_opts, {noCredentials: true}));
+          this.fire('network-error', {error: err});
+          throw err;
         }
+        throw err;
       }.bind(this));
     },
 
+    _urlWithParams: function(url, opt_params) {
+      if (!opt_params) { return url; }
+
+      var params = [];
+      for (var p in opt_params) {
+        if (opt_params[p] == null) {
+          params.push(encodeURIComponent(p));
+          continue;
+        }
+        var values = [].concat(opt_params[p]);
+        for (var i = 0; i < values.length; i++) {
+          params.push(
+            encodeURIComponent(p) + '=' +
+            encodeURIComponent(values[i]));
+        }
+      }
+      return url + '?' + params.join('&');
+    },
+
     getResponseObject: function(response) {
       return response.text().then(function(text) {
         var result;
@@ -153,7 +172,29 @@
     },
 
     getDiffPreferences: function() {
-      return this._fetchSharedCacheURL('/accounts/self/preferences.diff');
+      return this.getLoggedIn().then(function(loggedIn) {
+        if (loggedIn) {
+          return this._fetchSharedCacheURL('/accounts/self/preferences.diff');
+        }
+        // These defaults should match the defaults in
+        // gerrit-extension-api/src/main/jcg/gerrit/extensions/client/DiffPreferencesInfo.java
+        // NOTE: There are some settings that don't apply to PolyGerrit
+        // (Render mode being at least one of them).
+        return Promise.resolve({
+          auto_hide_diff_table_header: true,
+          context: 10,
+          cursor_blink_rate: 0,
+          ignore_whitespace: 'IGNORE_NONE',
+          intraline_difference: true,
+          line_length: 100,
+          show_line_endings: true,
+          show_tabs: true,
+          show_whitespace_errors: true,
+          syntax_highlighting: true,
+          tab_size: 8,
+          theme: 'DEFAULT',
+        });
+      }.bind(this));
     },
 
     saveDiffPreferences: function(prefs, opt_errFn, opt_ctx) {
@@ -172,7 +213,16 @@
     },
 
     getPreferences: function() {
-      return this._fetchSharedCacheURL('/accounts/self/preferences');
+      return this.getLoggedIn().then(function(loggedIn) {
+        if (loggedIn) {
+          return this._fetchSharedCacheURL('/accounts/self/preferences');
+        }
+
+        return Promise.resolve({
+          changes_per_page: 25,
+          diff_view: 'SIDE_BY_SIDE',
+        });
+      }.bind(this));
     },
 
     _fetchSharedCacheURL: function(url) {
@@ -197,6 +247,39 @@
       return this._sharedFetchPromises[url];
     },
 
+    getChanges: function(changesPerPage, opt_query, opt_offset) {
+      var options = this._listChangesOptionsToHex(
+          ListChangesOption.LABELS,
+          ListChangesOption.DETAILED_ACCOUNTS
+      );
+      var params = {
+        n: changesPerPage,
+        O: options,
+        S: opt_offset || 0,
+      };
+      if (opt_query && opt_query.length > 0) {
+        params.q = opt_query;
+      }
+      return this.fetchJSON('/changes/', null, null, params);
+    },
+
+    getDashboardChanges: function() {
+      var options = this._listChangesOptionsToHex(
+          ListChangesOption.LABELS,
+          ListChangesOption.DETAILED_ACCOUNTS,
+          ListChangesOption.REVIEWED
+      );
+      var params = {
+        O: options,
+        q: [
+          'is:open owner:self',
+          'is:open reviewer:self -owner:self',
+          'is:closed (owner:self OR reviewer:self) -age:4w limit:10',
+        ],
+      };
+      return this.fetchJSON('/changes/', null, null, params);
+    },
+
     getChangeActionURL: function(changeNum, opt_patchNum, endpoint) {
       return this._changeBaseURL(changeNum, opt_patchNum) + endpoint;
     },
@@ -233,14 +316,174 @@
           this.getChangeActionURL(changeNum, patchNum, '/commit?links'));
     },
 
-    getChangeFiles: function(changeNum, patchNum) {
+    getChangeFiles: function(changeNum, patchRange) {
+      var endpoint = '/files';
+      if (patchRange.basePatchNum !== 'PARENT') {
+        endpoint += '?base=' + encodeURIComponent(patchRange.basePatchNum);
+      }
       return this.fetchJSON(
-          this.getChangeActionURL(changeNum, patchNum, '/files'));
+          this.getChangeActionURL(changeNum, patchRange.patchNum, endpoint));
+    },
+
+    getChangeFilesAsSpeciallySortedArray: function(changeNum, patchRange) {
+      return this.getChangeFiles(changeNum, patchRange).then(
+          this._normalizeChangeFilesResponse.bind(this));
+    },
+
+    getChangeFilePathsAsSpeciallySortedArray: function(changeNum, patchRange) {
+      return this.getChangeFiles(changeNum, patchRange).then(function(files) {
+        return Object.keys(files).sort(this._specialFilePathCompare.bind(this));
+      }.bind(this));
+    },
+
+    _normalizeChangeFilesResponse: function(response) {
+      var paths = Object.keys(response).sort(
+          this._specialFilePathCompare.bind(this));
+      var files = [];
+      for (var i = 0; i < paths.length; i++) {
+        var info = response[paths[i]];
+        info.__path = paths[i];
+        info.lines_inserted = info.lines_inserted || 0;
+        info.lines_deleted = info.lines_deleted || 0;
+        files.push(info);
+      }
+      return files;
+    },
+
+    _specialFilePathCompare: function(a, b) {
+      var COMMIT_MESSAGE_PATH = '/COMMIT_MSG';
+      // The commit message always goes first.
+      if (a === COMMIT_MESSAGE_PATH) {
+        return -1;
+      }
+      if (b === COMMIT_MESSAGE_PATH) {
+        return 1;
+      }
+
+      var aLastDotIndex = a.lastIndexOf('.');
+      var aExt = a.substr(aLastDotIndex + 1);
+      var aFile = a.substr(0, aLastDotIndex);
+
+      var bLastDotIndex = b.lastIndexOf('.');
+      var bExt = b.substr(bLastDotIndex + 1);
+      var bFile = a.substr(0, bLastDotIndex);
+
+      // Sort header files above others with the same base name.
+      var headerExts = ['h', 'hxx', 'hpp'];
+      if (aFile.length > 0 && aFile === bFile) {
+        if (headerExts.indexOf(aExt) !== -1 &&
+            headerExts.indexOf(bExt) !== -1) {
+          return a.localeCompare(b);
+        }
+        if (headerExts.indexOf(aExt) !== -1) {
+          return -1;
+        }
+        if (headerExts.indexOf(bExt) !== -1) {
+          return 1;
+        }
+      }
+
+      return a.localeCompare(b);
     },
 
     getChangeRevisionActions: function(changeNum, patchNum) {
       return this.fetchJSON(
-          this.getChangeActionURL(changeNum, patchNum, '/actions'));
+          this.getChangeActionURL(changeNum, patchNum, '/actions')).then(
+              function(revisionActions) {
+                // The rebase button on change screen is always enabled.
+                if (revisionActions.rebase) {
+                  revisionActions.rebase.enabled = true;
+                }
+                return revisionActions;
+              });
+    },
+
+    getChangeSuggestedReviewers: function(changeNum, inputVal, opt_errFn,
+        opt_ctx) {
+      var url = this.getChangeActionURL(changeNum, null, '/suggest_reviewers');
+      return this.fetchJSON(url, opt_errFn, opt_ctx, {
+        n: 10,  // Return max 10 results
+        q: inputVal,
+      });
+    },
+
+    addChangeReviewer: function(changeNum, reviewerID) {
+      return this._sendChangeReviewerRequest('POST', changeNum, reviewerID);
+    },
+
+    removeChangeReviewer: function(changeNum, reviewerID) {
+      return this._sendChangeReviewerRequest('DELETE', changeNum, reviewerID);
+    },
+
+    _sendChangeReviewerRequest: function(method, changeNum, reviewerID) {
+      var url = this.getChangeActionURL(changeNum, null, '/reviewers');
+      var body;
+      switch(method) {
+        case 'POST':
+          body = {reviewer: reviewerID};
+          break;
+        case 'DELETE':
+          url += '/' + reviewerID;
+          break;
+        default:
+          throw Error('Unsupported HTTP method: ' + method);
+      }
+
+      return this.send(method, url, body);
+    },
+
+    getRelatedChanges: function(changeNum, patchNum) {
+      return this.fetchJSON(
+          this.getChangeActionURL(changeNum, patchNum, '/related'));
+    },
+
+    getChangesSubmittedTogether: function(changeNum) {
+      return this.fetchJSON(
+          this.getChangeActionURL(changeNum, null, '/submitted_together'));
+    },
+
+    getChangeConflicts: function(changeNum) {
+      var options = this._listChangesOptionsToHex(
+          ListChangesOption.CURRENT_REVISION,
+          ListChangesOption.CURRENT_COMMIT
+      );
+      var params = {
+        O: options,
+        q: 'status:open is:mergeable conflicts:' + changeNum,
+      };
+      return this.fetchJSON('/changes/', null, null, params);
+    },
+
+    getChangeCherryPicks: function(project, changeID, changeNum) {
+      var options = this._listChangesOptionsToHex(
+          ListChangesOption.CURRENT_REVISION,
+          ListChangesOption.CURRENT_COMMIT
+      );
+      var query = [
+        'project:' + project,
+        'change:' + changeID,
+        '-change:' + changeNum,
+        '-is:abandoned',
+      ].join(' ');
+      var params = {
+        O: options,
+        q: query
+      };
+      return this.fetchJSON('/changes/', null, null, params);
+    },
+
+    getChangesWithSameTopic: function(topic) {
+      var options = this._listChangesOptionsToHex(
+          ListChangesOption.LABELS,
+          ListChangesOption.CURRENT_REVISION,
+          ListChangesOption.CURRENT_COMMIT,
+          ListChangesOption.DETAILED_LABELS
+      );
+      var params = {
+        O: options,
+        q: 'status:open topic:' + topic,
+      };
+      return this.fetchJSON('/changes/', null, null, params);
     },
 
     getReviewedFiles: function(changeNum, patchNum) {
@@ -257,6 +500,18 @@
       return this.send(method, url, null, opt_errFn, opt_ctx);
     },
 
+    saveChangeReview: function(changeNum, patchNum, review, opt_errFn,
+        opt_ctx) {
+      var url = this.getChangeActionURL(changeNum, patchNum, '/review');
+      return this.send('POST', url, review, opt_errFn, opt_ctx);
+    },
+
+    saveChangeStarred: function(changeNum, starred) {
+      var url = '/accounts/self/starred.changes/' + changeNum;
+      var method = starred ? 'PUT' : 'DELETE';
+      return this.send(method, url);
+    },
+
     send: function(method, url, opt_body, opt_errFn, opt_ctx) {
       var headers = new Headers({
         'X-Gerrit-Auth': this._getCookie('XSRF_TOKEN'),
@@ -273,13 +528,24 @@
         }
         options.body = opt_body;
       }
-      return fetch(url, options).catch(function(err) {
+      return fetch(url, options).then(function(response) {
+        if (!response.ok) {
+          if (opt_errFn) {
+            opt_errFn.call(null, response);
+            return undefined;
+          }
+          this.fire('server-error', {response: response});
+        }
+
+        return response;
+      }.bind(this)).catch(function(err) {
+        this.fire('network-error', {error: err});
         if (opt_errFn) {
-          opt_errFn.call(opt_ctx || this);
+          opt_errFn.call(opt_ctx, null, err);
         } else {
           throw err;
         }
-      });
+      }.bind(this));
     },
 
     getDiff: function(changeNum, basePatchNum, patchNum, path,
@@ -357,6 +623,27 @@
       return this._changeBaseURL(changeNum, opt_patchNum) + endpoint;
     },
 
+    saveDiffDraft: function(changeNum, patchNum, draft) {
+      return this._sendDiffDraftRequest('PUT', changeNum, patchNum, draft);
+    },
+
+    deleteDiffDraft: function(changeNum, patchNum, draft) {
+      return this._sendDiffDraftRequest('DELETE', changeNum, patchNum, draft);
+    },
+
+    _sendDiffDraftRequest: function(method, changeNum, patchNum, draft) {
+      var url = this.getChangeActionURL(changeNum, patchNum, '/drafts');
+      if (draft.id) {
+        url += '/' + draft.id;
+      }
+      var body;
+      if (method === 'PUT') {
+        body = draft;
+      }
+
+      return this.send(method, url, body);
+    },
+
     _changeBaseURL: function(changeNum, opt_patchNum) {
       var v = '/changes/' + changeNum;
       if (opt_patchNum) {
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
index 4e2c5ad..e7f7a21 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
@@ -83,19 +83,24 @@
     });
 
     test('params are properly encoded', function() {
-      var fetchStub = sinon.stub(window, 'fetch', function() {
-        return Promise.resolve({text: function() {
-          return Promise.resolve(')]}\'\n{}');
-        }});
-      });
-      var params = {
+      var url = element._urlWithParams('/path/', {
         sp: 'hola',
         gr: 'guten tag',
         noval: null,
-      };
-      element.fetchJSON('/path/', null, null, params);
-      assert.equal(fetchStub.args[0][0], '/path/?gr=guten%20tag&noval&sp=hola');
-      fetchStub.restore();
+      });
+      assert.equal(url, '/path/?sp=hola&gr=guten%20tag&noval');
+
+      url = element._urlWithParams('/path/', {
+        sp: 'hola',
+        en: ['hey', 'hi'],
+      });
+      assert.equal(url, '/path/?sp=hola&en=hey&en=hi');
+
+      // Order must be maintained with array params.
+      url = element._urlWithParams('/path/', {
+        l: ['c', 'b', 'a'],
+      });
+      assert.equal(url, '/path/?l=c&l=b&l=a');
     });
 
     test('request callbacks can be canceled', function(done) {
@@ -196,5 +201,46 @@
         });
     });
 
+    test('special file path sorting', function() {
+      assert.deepEqual(
+          ['.b', '/COMMIT_MSG', '.a', 'file'].sort(
+              element._specialFilePathCompare),
+          ['/COMMIT_MSG', '.a', '.b', 'file']);
+
+      assert.deepEqual(
+          ['.b', '/COMMIT_MSG', 'foo/bar/baz.cc', 'foo/bar/baz.h'].sort(
+              element._specialFilePathCompare),
+          ['/COMMIT_MSG', '.b', 'foo/bar/baz.h', 'foo/bar/baz.cc']);
+
+      assert.deepEqual(
+          ['.b', '/COMMIT_MSG', 'foo/bar/baz.cc', 'foo/bar/baz.hpp'].sort(
+              element._specialFilePathCompare),
+          ['/COMMIT_MSG', '.b', 'foo/bar/baz.hpp', 'foo/bar/baz.cc']);
+
+      assert.deepEqual(
+          ['.b', '/COMMIT_MSG', 'foo/bar/baz.cc', 'foo/bar/baz.hxx'].sort(
+              element._specialFilePathCompare),
+          ['/COMMIT_MSG', '.b', 'foo/bar/baz.hxx', 'foo/bar/baz.cc']);
+
+      assert.deepEqual(
+          ['foo/bar.h', 'foo/bar.hxx', 'foo/bar.hpp'].sort(
+              element._specialFilePathCompare),
+          ['foo/bar.h', 'foo/bar.hpp', 'foo/bar.hxx']);
+    });
+
+    test('rebase always enabled', function(done) {
+      var resolveFetchJSON;
+      var fetchJSONStub = sinon.stub(element, 'fetchJSON').returns(
+          new Promise(function(resolve) {
+            resolveFetchJSON = resolve;
+          }));
+      element.getChangeRevisionActions('42', '1337').then(
+          function(response) {
+            assert.isTrue(response.rebase.enabled);
+            fetchJSONStub.restore();
+            done();
+          });
+      resolveFetchJSON({rebase: {}});
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.html b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.html
new file mode 100644
index 0000000..64ef9b2
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.html
@@ -0,0 +1,49 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+
+<dom-module id="gr-tooltip">
+  <template>
+    <style>
+      :host {
+        --gr-tooltip-arrow-size: .6em;
+
+        background-color: #333;
+        box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
+        color: #fff;
+        font-size: .75rem;
+        padding: .5em .85em;
+        position: absolute;
+        z-index: 1000;
+      }
+      .arrow {
+        border-left: var(--gr-tooltip-arrow-size) solid transparent;
+        border-right: var(--gr-tooltip-arrow-size) solid transparent;
+        border-top: var(--gr-tooltip-arrow-size) solid #333;
+        bottom: -var(--gr-tooltip-arrow-size);
+        height: 0;
+        position: absolute;
+        left: calc(50% - var(--gr-tooltip-arrow-size));
+        width: 0;
+      }
+    </style>
+    [[text]]
+    <i class="arrow"></i>
+  </template>
+  <script src="gr-tooltip.js"></script>
+</dom-module>
+
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorContainer.java b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js
similarity index 71%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorContainer.java
rename to polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js
index b1381ca..76372ba 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorContainer.java
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 The Android Open Source Project
+// Copyright (C) 2016 The Android Open 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,11 +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.
+(function() {
+  'use strict';
 
-package com.google.gerrit.client.patches;
+  Polymer({
+    is: 'gr-tooltip',
 
-public interface CommentEditorContainer {
-  void notifyDraftDelta(int delta);
-
-  void remove(CommentEditorPanel panel);
-}
+    properties: {
+      text: String,
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/index.html b/polygerrit-ui/app/index.html
index 4dad2b9..0e00f77 100644
--- a/polygerrit-ui/app/index.html
+++ b/polygerrit-ui/app/index.html
@@ -20,6 +20,7 @@
 <meta name="description" content="Gerrit Code Review">
 <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0">
 
+<link rel="stylesheet" href="/styles/fonts.css">
 <link rel="stylesheet" href="/styles/main.css">
 <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
 <link rel="import" href="/elements/gr-app.html">
diff --git a/polygerrit-ui/app/styles/app-theme.html b/polygerrit-ui/app/styles/app-theme.html
index 81c6ff7..cf54b5d 100644
--- a/polygerrit-ui/app/styles/app-theme.html
+++ b/polygerrit-ui/app/styles/app-theme.html
@@ -15,10 +15,8 @@
 -->
 <style is="custom-style">
 :root {
-  --primary-color: #fff;
   --primary-text-color: #000;
   --search-border-color: #ddd;
-  --secondary-color: #f1f2f3;
   --selection-background-color: #ebf5fb;
   --default-text-color: #000;
   --view-background-color: #fff;
diff --git a/polygerrit-ui/app/styles/gr-change-list-styles.html b/polygerrit-ui/app/styles/gr-change-list-styles.html
index b4cc415..6e34c2b 100644
--- a/polygerrit-ui/app/styles/gr-change-list-styles.html
+++ b/polygerrit-ui/app/styles/gr-change-list-styles.html
@@ -44,6 +44,9 @@
       .star {
         padding-top: .05em;
       }
+      .number {
+        width: 4em;
+      }
       .subject {
         flex-grow: 1;
         flex-shrink: 1;
diff --git a/polygerrit-ui/app/styles/main.css b/polygerrit-ui/app/styles/main.css
index 5c5ad96..9bf33a0 100644
--- a/polygerrit-ui/app/styles/main.css
+++ b/polygerrit-ui/app/styles/main.css
@@ -14,8 +14,6 @@
 limitations under the License.
 */
 
-@import "fonts.css";
-
 *,
 *::after,
 *::before {
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 4238f9a..da2072b 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -38,6 +38,7 @@
     '../elements/change-list/gr-change-list/gr-change-list_test.html',
     '../elements/change-list/gr-change-list-item/gr-change-list-item_test.html',
     '../elements/core/gr-account-dropdown/gr-account-dropdown_test.html',
+    '../elements/core/gr-error-manager/gr-error-manager_test.html',
     '../elements/core/gr-main-header/gr-main-header_test.html',
     '../elements/core/gr-search-bar/gr-search-bar_test.html',
     '../elements/diff/gr-diff/gr-diff-builder_test.html',
@@ -48,11 +49,14 @@
     '../elements/diff/gr-diff-preferences/gr-diff-preferences_test.html',
     '../elements/diff/gr-diff-view/gr-diff-view_test.html',
     '../elements/diff/gr-patch-range-select/gr-patch-range-select_test.html',
+    '../elements/shared/gr-alert/gr-alert_test.html',
     '../elements/shared/gr-account-label/gr-account-label_test.html',
     '../elements/shared/gr-account-link/gr-account-link_test.html',
+    '../elements/shared/gr-alert/gr-alert_test.html',
     '../elements/shared/gr-avatar/gr-avatar_test.html',
     '../elements/shared/gr-change-star/gr-change-star_test.html',
     '../elements/shared/gr-confirm-dialog/gr-confirm-dialog_test.html',
+    '../elements/shared/gr-cursor-manager/gr-cursor-manager_test.html',
     '../elements/shared/gr-date-formatter/gr-date-formatter_test.html',
     '../elements/shared/gr-js-api-interface/gr-js-api-interface_test.html',
     '../elements/shared/gr-linked-text/gr-linked-text_test.html',
diff --git a/polygerrit-ui/server.go b/polygerrit-ui/server.go
index 66f9e55..cb6d236 100644
--- a/polygerrit-ui/server.go
+++ b/polygerrit-ui/server.go
@@ -19,7 +19,6 @@
 	"compress/gzip"
 	"errors"
 	"flag"
-	"fmt"
 	"io"
 	"log"
 	"net"
@@ -33,7 +32,6 @@
 	restHost = flag.String("host", "gerrit-review.googlesource.com", "Host to proxy requests to")
 	port     = flag.String("port", ":8081", "Port to serve HTTP requests on")
 	prod     = flag.Bool("prod", false, "Serve production assets")
-	loggedIn = flag.Bool("logged_in", false, "Return user info as if the user is logged in")
 )
 
 func main() {
@@ -79,27 +77,9 @@
 }
 
 func handleAccountDetail(w http.ResponseWriter, r *http.Request) {
-	if !*loggedIn {
-		http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
-		return
-	}
-	fmt.Fprint(w, accountInfo)
+	http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
 }
 
-const accountInfo = `)]}'
-{
-  "registered_on": "2015-08-31 21:24:17.614000000",
-  "_account_id": 1021482,
-  "name": "Andrew Bonventre",
-  "email": "andybons@chromium.org",
-  "avatars": [
-    {
-      "url": "https://lh4.googleusercontent.com/-1EovlES413I/AAAAAAAAAAI/AAAAAAAAAAA/GQ5-31ULE1Q/s26-p/photo.jpg",
-      "height": 26
-    }
-  ]
-}`
-
 type gzipResponseWriter struct {
 	io.WriteCloser
 	http.ResponseWriter
diff --git a/tools/checkstyle.xml b/tools/checkstyle.xml
index 1bb40f7..cb24e8f 100644
--- a/tools/checkstyle.xml
+++ b/tools/checkstyle.xml
@@ -93,6 +93,10 @@
       <property name="tokens" value="BAND, BOR, BSR, BXOR, DIV, EQUAL, GE, GT, LAND, LE, LITERAL_INSTANCEOF, LOR, LT, MINUS, MOD, NOT_EQUAL, PLUS, QUESTION, SL, SR, STAR "/>
       <metadata name="net.sf.eclipsecs.core.lastEnabledSeverity" value="inherit"/>
     </module>
+    <module name="RedundantImport"/>
+    <module name="RedundantModifier"/>
+    <module name="ExplicitInitialization"/>
+    <module name="ArrayTrailingComma"/>
   </module>
   <module name="FileTabCharacter">
     <property name="severity" value="ignore"/>
diff --git a/tools/download_all.py b/tools/download_all.py
index bc45714..58316ca 100755
--- a/tools/download_all.py
+++ b/tools/download_all.py
@@ -19,11 +19,6 @@
 
 MAIN = ['//tools/eclipse:classpath']
 PAT = re.compile(r'"(//.*?)" -> "//tools:download_file"')
-# TODO(davido): Remove this hack when Buck bugs are fixed:
-# https://github.com/facebook/buck/issues/656
-# https://github.com/facebook/buck/issues/658
-JGIT = re.compile(r'//org.eclipse.jgit.*')
-CELL = '//lib/jgit'
 
 opts = OptionParser()
 opts.add_option('--src', action='store_true')
@@ -36,8 +31,6 @@
   m = PAT.search(line)
   if m:
     n = m.group(1)
-    if JGIT.match(n):
-      n = CELL + n[1:]
     if args.src and n.endswith('__download_bin'):
       n = n[:-13] + 'src'
     targets.add(n)
diff --git a/tools/eclipse/gerrit_gwt_debug.launch b/tools/eclipse/gerrit_gwt_debug.launch
index c3c58ff..b2ab320 100644
--- a/tools/eclipse/gerrit_gwt_debug.launch
+++ b/tools/eclipse/gerrit_gwt_debug.launch
@@ -16,7 +16,7 @@
 </listAttribute>
 <booleanAttribute key="org.eclipse.jdt.launching.DEFAULT_CLASSPATH" value="false"/>
 <stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="com.google.gerrit.gwtdebug.GerritGwtDebugLauncher"/>
-<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="-noprecompile -src ${resource_loc:/gerrit} -workDir ${resource_loc:/gerrit}/buck-out/gen/gerrit-gwtui com.google.gerrit.GerritGwtUI -src ${resource_loc:/gerrit}/gerrit-plugin-gwtui/src/main/java -- --console-log --show-stack-trace -d ${resource_loc:/gerrit}/../gerrit_testsite"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="-strict -noprecompile -src ${resource_loc:/gerrit} -workDir ${resource_loc:/gerrit}/buck-out/gen/gerrit-gwtui com.google.gerrit.GerritGwtUI -src ${resource_loc:/gerrit}/gerrit-plugin-gwtui/src/main/java -- --console-log --show-stack-trace -d ${resource_loc:/gerrit}/../gerrit_testsite"/>
 <stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="gerrit"/>
 <stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-Xmx1024M&#10;-XX:MaxPermSize=256M&#10;-Dgerrit.disable-gwtui-recompile=true"/>
 </launchConfiguration>
diff --git a/tools/eclipse/project.py b/tools/eclipse/project.py
index 26b35cf..4b35f7c 100755
--- a/tools/eclipse/project.py
+++ b/tools/eclipse/project.py
@@ -131,8 +131,7 @@
       continue
 
     m = java_library.match(p)
-    # Don't grab the cross-cell JGit libraries as source
-    if m and not m.group(1).startswith('org.eclipse.jgit'):
+    if m:
       src.add(m.group(1))
     else:
       lib.add(p)
diff --git a/tools/maven/BUCK b/tools/maven/BUCK
index fcd77c0..322b5a2 100644
--- a/tools/maven/BUCK
+++ b/tools/maven/BUCK
@@ -31,8 +31,3 @@
   },
   war = {'gerrit-war': '//:release'},
 )
-
-python_binary(
-  name = 'mvn',
-  main = 'mvn.py',
-)
diff --git a/tools/maven/api.py b/tools/maven/api.py
deleted file mode 100755
index 600de6a..0000000
--- a/tools/maven/api.py
+++ /dev/null
@@ -1,78 +0,0 @@
-#!/usr/bin/env python
-# 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.
-
-from __future__ import print_function
-from argparse import ArgumentParser
-from json import loads
-from os import environ, path, remove
-from subprocess import check_call, check_output, Popen, PIPE
-from sys import stderr
-from tempfile import mkstemp
-
-
-def locations():
-  d = Popen('buck audit dependencies api'.split(),
-            stdin=None, stdout=PIPE, stderr=PIPE)
-  t = Popen('xargs buck targets --show_output'.split(),
-            stdin=d.stdout, stdout=PIPE, stderr=PIPE)
-  out = t.communicate()[0]
-  d.wait()
-  targets = []
-  outs = []
-  for e in out.strip().split('\n'):
-    t, o = e.split()
-    targets.append(t)
-    outs.append(o)
-  return dict(zip(targets, outs))
-
-parser = ArgumentParser()
-parser.add_argument('-n', '--dryrun', action='store_true')
-parser.add_argument('-v', '--verbose', action='store_true')
-
-subparsers = parser.add_subparsers(help='action', dest='action')
-subparsers.add_parser('deploy', help='Deploy to Maven (remote)')
-subparsers.add_parser('install', help='Install to Maven (local)')
-
-args = parser.parse_args()
-
-root = path.abspath(__file__)
-while not path.exists(path.join(root, '.buckconfig')):
-  root = path.dirname(root)
-
-if not args.dryrun:
-  check_call('buck build api'.split())
-target = check_output(('buck targets --json api_%s' % args.action).split())
-
-s = loads(target)[0]['cmd']
-
-fd, tempfile = mkstemp()
-s = s.replace('$(exe //tools/maven:mvn)', path.join(root, 'tools/maven/mvn.py'))
-s = s.replace('-o $OUT', '-o %s' % tempfile)
-
-locations = locations()
-
-while '$(location' in s:
-  start = s.index('$(location')
-  end = s.index(')', start)
-  target = s[start+11:end]
-  s = s.replace(s[start:end+1], locations[target])
-
-try:
-  if args.verbose or args.dryrun or environ.get('VERBOSE'):
-    print(s, file=stderr)
-  if not args.dryrun:
-    check_call(s.split())
-finally:
-  remove(tempfile)
diff --git a/tools/maven/api.sh b/tools/maven/api.sh
new file mode 100755
index 0000000..54f6994
--- /dev/null
+++ b/tools/maven/api.sh
@@ -0,0 +1,69 @@
+#!/bin/bash
+
+# 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.
+
+if [[ "$#" == "0" ]] ; then
+  cat <<EOF
+Usage: run "$0 COMMAND" from the top of your workspace, where
+COMMAND is one of
+
+  install
+  deploy
+  war_install
+  war_deploy
+
+Set VERBOSE in the environment to get more information.
+
+EOF
+
+  exit 1
+fi
+
+set -o errexit
+set -o nounset
+
+
+case "$1" in
+install)
+    command="api_install"
+    ;;
+deploy)
+    command="api_deploy"
+    ;;
+war_install)
+    command="war_install"
+    ;;
+war_deploy)
+    command="war_deploy"
+    ;;
+*)
+    echo "unknown command $1"
+    exit 1
+    ;;
+esac
+
+if [[ "${VERBOSE:-x}" != "x" ]]; then
+  set -o xtrace
+fi
+
+buck build //tools/maven:gen_${command} || \
+  { echo "buck failed to build gen_${command}. Use VERBOSE=1 for more info" ; exit 1 ; }
+
+script="./buck-out/gen/tools/maven/gen_${command}/${command}.sh"
+
+# The PEX wrapper does some funky exit handling, so even if the script
+# does "exit(0)", the return status is '1'. So we can't tell if the
+# following invocation was successful.
+${script}
diff --git a/tools/maven/mvn.py b/tools/maven/mvn.py
index 83a33e8..4011d71 100755
--- a/tools/maven/mvn.py
+++ b/tools/maven/mvn.py
@@ -69,7 +69,12 @@
       file=stderr)
     exit(1)
 
-with open(args.o, 'w') as fd:
+
+out = stderr
+if args.o:
+  out = open(args.o, 'w')
+
+with out as fd:
   if args.repository:
     print('Repository: %s' % args.repository, file=fd)
   if args.url:
diff --git a/tools/maven/package.defs b/tools/maven/package.defs
index 8fe9a13..e43f59e 100644
--- a/tools/maven/package.defs
+++ b/tools/maven/package.defs
@@ -12,6 +12,19 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+sh_bang_template = (' && '.join([
+  "echo '#!/bin/sh -eu' > $OUT",
+  'echo "# this script should run from the root of your workspace." >> $OUT',
+  'echo "" >> $OUT',
+  "echo 'if [[ -n \"$${VERBOSE:-}\" ]]; then set -x ; fi' >> $OUT",
+  'echo "" >> $OUT',
+  'echo %s >> $OUT',
+  'echo "" >> $OUT',
+  'echo %s >> $OUT',
+  # This is supposed to be handled by executable=True, but it doesn't
+  # work. Bug?
+  'chmod +x $OUT' ]))
+
 def maven_package(
     version,
     repository = None,
@@ -20,44 +33,63 @@
     src = {},
     doc = {},
     war = {}):
-  cmd = ['$(exe //tools/maven:mvn)', '-v', version, '-o', '$OUT']
-  api_cmd = []
+
+  build_cmd = ['buck', 'build']
+
+  # This is not using python_binary() to avoid the baggage and bugs
+  # that PEX brings along.
+  mvn_cmd = ['python', 'tools/maven/mvn.py', '-v', version]
+  api_cmd = mvn_cmd[:]
+  api_targets = []
   for type,d in [('jar', jar), ('java-source', src), ('javadoc', doc)]:
-    for a,t in d.iteritems():
+    for a,t in sorted(d.iteritems()):
       api_cmd.append('-s %s:%s:$(location %s)' % (a,type,t))
+      api_targets.append(t)
 
   genrule(
-    name = 'api_install',
-    cmd = ' '.join(cmd + api_cmd + ['-a', 'install']),
-    out = 'api_install.info',
+    name = 'gen_api_install',
+    cmd = sh_bang_template % (
+      ' '.join(build_cmd + api_targets),
+      ' '.join(api_cmd + ['-a', 'install'])),
+    out = 'api_install.sh',
+    executable = True,
   )
 
   if repository and url:
     genrule(
-      name = 'api_deploy',
-      cmd = ' '.join(cmd + api_cmd + [
-        '-a', 'deploy',
-        '--repository', repository,
-        '--url', url]),
-      out = 'api_deploy.info',
+      name = 'gen_api_deploy',
+      cmd = sh_bang_template % (
+        ' '.join(build_cmd + api_targets),
+        ' '.join(api_cmd + ['-a', 'deploy',
+                            '--repository', repository,
+                            '--url', url])),
+      out = 'api_deploy.sh',
+      executable = True,
     )
 
-  war_cmd = []
-  for a,t in war.iteritems():
+  war_cmd = mvn_cmd[:]
+  war_targets = []
+  for a,t in sorted(war.iteritems()):
     war_cmd.append('-s %s:war:$(location %s)' % (a,t))
+    war_targets.append(t)
 
   genrule(
-    name = 'war_install',
-    cmd = ' '.join(cmd + war_cmd + ['-a', 'install']),
-    out = 'war_install.info',
+    name = 'gen_war_install',
+    cmd = sh_bang_template % (' '.join(build_cmd + war_targets),
+                              ' '.join(war_cmd + ['-a', 'install'])),
+    out = 'war_install.sh',
+    executable = True,
   )
 
   if repository and url:
     genrule(
-      name = 'war_deploy',
-      cmd = ' '.join(cmd + war_cmd + [
+      name = 'gen_war_deploy',
+      cmd = sh_bang_template % (
+          ' '.join(build_cmd + war_targets),
+          ' '.join(war_cmd + [
         '-a', 'deploy',
         '--repository', repository,
-        '--url', url]),
-      out = 'war_deploy.info',
+        '--url', url])),
+      out = 'war_deploy.sh',
+      executable = True,
     )
diff --git a/tools/pack_war.py b/tools/pack_war.py
index cd836a8..ca21790 100755
--- a/tools/pack_war.py
+++ b/tools/pack_war.py
@@ -28,7 +28,6 @@
 
 war = args.tmp
 jars = set()
-basenames = set()
 
 def prune(l):
   return [j for e in l for j in e.split(':')]
@@ -37,19 +36,10 @@
   makedirs(directory)
   for j in libs:
     if j not in jars:
-      # When jgit is consumed from its own cell,
-      # potential duplicates should be filtered.
-      # e.g. jsch.jar will be reached through:
-      # 1. /home/username/projects/gerrit/buck-out/gen/lib/jsch.jar
-      # 2. /home/username/projects/jgit/buck-out/gen/lib/jsch.jar
-      if (j.find('jgit/buck-out/gen/lib') > 0
-          and path.basename(j) in basenames):
-          continue
       jars.add(j)
       n = path.basename(j)
       if j.find('buck-out/gen/gerrit-') > 0:
         n = j[j.find('buck-out'):].split('/')[2] + '-' + n
-      basenames.add(n)
       symlink(j, path.join(directory, n))
 
 if args.lib:
diff --git a/tools/util.py b/tools/util.py
index 96f6047..08a803f 100644
--- a/tools/util.py
+++ b/tools/util.py
@@ -20,6 +20,7 @@
   'GERRIT_API': 'https://gerrit-api.commondatastorage.googleapis.com/release',
   'MAVEN_CENTRAL': 'http://repo1.maven.org/maven2',
   'MAVEN_LOCAL': 'file://' + path.expanduser('~/.m2/repository'),
+  'MAVEN_SNAPSHOT': 'https://oss.sonatype.org/content/repositories/snapshots',
 }