Merge "Revert "Refactor gr-rest-api-interface - extract common methods to helper""
diff --git a/.bazelproject b/.bazelproject
index b1c8bd3..e14c108 100644
--- a/.bazelproject
+++ b/.bazelproject
@@ -22,6 +22,3 @@
 
 build_flags:
   --javacopt=-g
-  # Temporarily add an option to work around an error in the Bazel IntelliJ plugin.
-  # TODO(aliceks): Remove when issue is fixed.
-  --incompatible_depset_is_not_iterable=false
diff --git a/.bazelversion b/.bazelversion
index 1b58cc1..8862dba 100644
--- a/.bazelversion
+++ b/.bazelversion
@@ -1 +1 @@
-0.27.0
+1.0.0rc2
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index b6c034f..d77a21d 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -2840,6 +2840,46 @@
 +
 Defaults to 300000 ms (5 minutes).
 
+
+[[index.name.maxMergeCount]]index.name.maxMergeCount::
++
+Determines the max number of simultaneous merges that are allowed. If a merge
+is necessary yet we already have this many threads running, the incoming thread
+(that is calling add/updateDocument) will block until a merge thread has
+completed.  Note that Lucene will only run the smallest maxThreadCount merges
+at a time. See the
+link:https://lucene.apache.org/core/5_5_0/core/org/apache/lucene/index/ConcurrentMergeScheduler.html#setDefaultMaxMergesAndThreads(boolean)[
+Lucene documentation] for further details.
++
+Defaults to -1 for (auto detection).
+
+
+[[index.name.maxThreadCount]]index.name.maxThreadCount::
++
+Determines the max number of simultaneous Lucene merge threads that should be running at
+once. This must be less than or equal to maxMergeCount. See the
+link:https://lucene.apache.org/core/5_5_0/core/org/apache/lucene/index/ConcurrentMergeScheduler.html#setDefaultMaxMergesAndThreads(boolean)[
+Lucene documentation] for further details.
++
+For further details on Lucene index configuration (auto detection) which
+affects maxThreadCount and maxMergeCount settings.
+See the
+link:https://lucene.apache.org/core/5_5_0/core/org/apache/lucene/index/ConcurrentMergeScheduler.html#AUTO_DETECT_MERGES_AND_THREADS[
+Lucene documentation]
++
+Defaults to -1 for (auto detection).
+
+[[index.name.enableAutoIOThrottle]]index.name.enableAutoIOThrottle::
++
+Allows the control of whether automatic IO throttling is enabled and used by
+default in the lucene merge queue.  Automatic dynamic IO throttling, which when
+on is used to adaptively rate limit writes bytes/sec to the minimal rate necessary
+so merges do not fall behind. See the
+link:https://lucene.apache.org/core/5_5_0/core/org/apache/lucene/index/ConcurrentMergeScheduler.html#enableAutoIOThrottle()[
+Lucene documentation] for further details.
++
+Defaults to true (throttling enabled).
+
 Sample Lucene index configuration:
 ----
 [index]
@@ -2848,10 +2888,17 @@
 [index "changes_open"]
   ramBufferSize = 60 m
   maxBufferedDocs = 3000
+  maxThreadCount = 5
+  maxMergeCount = 50
+
 
 [index "changes_closed"]
   ramBufferSize = 20 m
   maxBufferedDocs = 500
+  maxThreadCount = 10
+  maxMergeCount = 100
+  enableIOThrottle = false
+
 ----
 
 [[elasticsearch]]
diff --git a/Documentation/dev-build-plugins.txt b/Documentation/dev-build-plugins.txt
index 47ace5b..8139743 100644
--- a/Documentation/dev-build-plugins.txt
+++ b/Documentation/dev-build-plugins.txt
@@ -128,16 +128,25 @@
 ----
 
 If the plugin(s) being bundled in the release have external dependencies, include them
-in `plugins/external_plugin_deps`. You should alias `external_plugin_deps()` so it
-can be imported for multiple plugins. For example:
+in `plugins/external_plugin_deps`. Create symbolic link from plugin's own
+`external_plugin_deps()` file in plugins directory and prefix the file with
+plugin name, e.g.:
 
 ----
-load(":my-plugin/external_plugin_deps.bzl", my_plugin="external_plugin_deps")
-load(":my-other-plugin/external_plugin_deps.bzl", my_other_plugin="external_plugin_deps")
+  $ cd plugins
+  $ ln -s oauth/external_plugin_deps.bzl oauth_external_plugin_deps.bzl
+  $ ln -s uploadvalidator/external_plugin_deps.bzl uploadvalidator_external_plugin_deps.bzl
+----
+
+Now the plugin specific dependency files can be imported:
+
+----
+load(":oauth_external_plugin_deps.bzl", oauth_deps="external_plugin_deps")
+load(":uploadvalidator_external_plugin_deps.bzl", uploadvalidator_deps="external_plugin_deps")
 
 def external_plugin_deps():
-  my_plugin()
-  my_other_plugin()
+  oauth_deps()
+  uploadvalidator_deps()
 ----
 
 [NOTE]
diff --git a/Documentation/dev-crafting-changes.txt b/Documentation/dev-crafting-changes.txt
index 76baa8b..02b650a 100644
--- a/Documentation/dev-crafting-changes.txt
+++ b/Documentation/dev-crafting-changes.txt
@@ -115,7 +115,7 @@
 link:https://github.com/google/google-java-format[`google-java-format`]
 tool (version 1.7), and to format Bazel BUILD, WORKSPACE and .bzl files the
 link:https://github.com/bazelbuild/buildtools/tree/master/buildifier[`buildifier`]
-tool (version 0.26.0).
+tool (version 0.28.0).
 These tools automatically apply format according to the style guides; this
 streamlines code review by reducing the need for time-consuming, tedious,
 and contentious discussions about trivial issues like whitespace.
diff --git a/Documentation/dev-e2e-tests.txt b/Documentation/dev-e2e-tests.txt
new file mode 100644
index 0000000..7329a43
--- /dev/null
+++ b/Documentation/dev-e2e-tests.txt
@@ -0,0 +1,92 @@
+= Gerrit Code Review - End to end load tests
+
+This document provides a description of a Gerrit load test scenario implemented using the link:http://gatling.io[`Gatling`] framework.
+
+Similar scenarios have been successfully used to compare performance of different Gerrit versions or study the Gerrit response
+under different load profiles.
+
+== What is Gatling?
+
+Gatling is a load testing tool which provides out of the box support for the HTTP protocol. Documentation on how to write an
+HTTP load test can be found link:https://gatling.io/docs/current/http/http_protocol/[`here`].
+
+However, in the scenario we are proposing, we are leveraging the link:https://github.com/GerritForge/gatling-git[`Gatling Git extension`]
+to run tests at Git protocol level.
+
+Gatling is written in Scala, but the abstraction provided by the Gatling DSL makes the scenarios implementation easy even without any Scala knowledge.
+
+Examples of scenarios can be found in the `e2e-tests` directory.
+
+=== How to run the load tests
+
+==== Prerequisites
+
+* link:https://www.scala-lang.org/download/[`Scala 2.12`]
+
+==== How to build
+
+----
+sbt compile
+----
+
+==== Setup
+
+If you are running SSH commands the private keys of the users used for testing need to go in `/tmp/ssh-keys`.
+The keys need to be generated this way (JSch won't validate them [otherwise](https://stackoverflow.com/questions/53134212/invalid-privatekey-when-using-jsch):
+
+----
+ssh-keygen -m PEM -t rsa -C "test@mail.com" -f /tmp/ssh-keys/id_rsa
+----
+
+*NOTE*: Don't forget to add the public keys for the testing user(s) to your git server
+
+==== Input file
+
+The ReplayRecordsScenario is fed by the data coming from the [src/test/resources/data/requests.json](/src/test/resources/data/requests.json) file.
+Such file contains the commands and repo used during the load test.
+Below an example:
+
+----
+[
+  {
+    "url": "ssh://admin@localhost:29418/loadtest-repo.git",
+    "cmd": "clone"
+  },
+  {
+    "url": "http://localhost:8080/loadtest-repo.git",
+    "cmd": "fetch"
+  }
+]
+----
+
+Valid commands are:
+* fetch
+* pull
+* push
+* clone
+
+==== How to use the framework
+
+Run all tests:
+----
+sbt "gatling:test"
+----
+
+Run a single test:
+----
+sbt "gatling:testOnly com.google.gerrit.scenarios.ReplayRecordsFromFeederScenario"
+----
+
+Generate the last report:
+----
+sbt "gatling:lastReport"
+----
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
+
+[scala]:
diff --git a/Documentation/dev-intellij.txt b/Documentation/dev-intellij.txt
index c662e178..5077079 100644
--- a/Documentation/dev-intellij.txt
+++ b/Documentation/dev-intellij.txt
@@ -187,7 +187,8 @@
 or go to *Run -> Edit Configurations*.
 
 [[runconfigurations-daemon]]
-==== Gerrit Daemon
+=== Gerrit Daemon
+
 [WARNING]
 ====
 At the moment running this (local) configuration results in a
diff --git a/Documentation/dev-processes.txt b/Documentation/dev-processes.txt
index b3c147f..c19a579 100644
--- a/Documentation/dev-processes.txt
+++ b/Documentation/dev-processes.txt
@@ -80,6 +80,39 @@
 
 See link:dev-design-docs.html#review[here].
 
+[[versioning]]
+== Semantic versioning
+
+Gerrit follows a light link:https://semver.org/[semantic versioning scheme] MAJOR.MINOR[.PATCH[.HOTFIX]]
+format:
+
+  * MAJOR is incremented when there are substantial incompatible changes and/or
+    new features in Gerrit.
+  * MINOR is incremented when there are changes that are typically backward compatible
+    with the earlier minor version. Features can be removed following the
+    link:#deprecating-features[feature deprecation process]. Dependencies can be upgraded
+    according to the link:dev-processes.html#upgrading-libraries[libraries upgrade policy].
+  * PATCH is incremented when there are backward-compatible bug fixes in Gerrit or its
+    dependencies. When PATCH is zero, it can be omitted.
+  * HOTFIX is present only when immediately after a patch release, some urgent
+    fixes in the code or the packaging format are required but do not justify a
+    new patch release.
+
+For every MAJOR.MINOR release there is an associated stable branch that follows well defined
+link:#dev-in-stable-branches[rules of development].
+
+Within a stable branch, there are multiple MAJOR.MINOR.PATCH tags created associated to the
+bug-fix releases of that stable release.
+
+Examples:
+
+* Gerrit v3.0.0 contains breaking incompatible changes in the functionality because
+  the ReviewDb storage has been totally removed.
+* Gerrit v2.15 contains brand-new features like NoteDb, however, still supports the existing
+  ReviewDb storage for changes and thus is considered a minor release.
+* Gerrit v2.14.20 is the 20th patch-release of the stable Gerrit v2.14.* and thus does not contain
+  new features but only bug-fixes.
+
 [[dev-in-stable-branches]]
 == Development in stable branches
 
@@ -97,16 +130,29 @@
     and should only be allowed if the risk of breaking things is considered to be low.
   * Once a major release is done only bug-fixes and documentation updates should be done on the
     stable branch. These updates will be included in the next minor release.
-  * For minor releases new features are only acceptable if they are important to the Gerrit
-    community, if they are backwards compatible and the risk of breaking things is low and if there
-    are no objections from the steering committee.
-  * In cases of doubt it's the responsibility of the steering committee to evaluate the risk of new
-    features and make a decision based on these rules and opinions from the Gerrit community.
+  * For minor releases new features could be acceptable if the following conditions are met:
+    ** they are result of a new feature introduced through a merge of an earlier stable branch
+    ** they are justified for completing, extending or fixing an existing feature
+    ** does not involve API, user-interface changes or data migrations
+    ** is backward compatible with all existing features
+    ** the parts of the code in common with existing features are properly covered by end-to-end tests
+    ** is important to the Gerrit community and no Gerrit maintainers have raised objections.
+  * In cases of doubt or conflicting opinions on new features, it's the responsibility of the
+    steering committee to evaluate the risk of new features and make a decision based on these
+    rules and opinions from the Gerrit community.
   * The older a stable branch is the more stable it should be. This means old stable branches
     should only receive bug-fixes that are either important or low risk. Security fixes, including
     security updates for third party dependencies, are always considered as important and hence can
     always be done on stable branches.
 
+Examples:
+
+* Gerrit v3.0.0-rc1 and v3.0.0-rc2 may contain new features and API changes without notice,
+  even if they are both cut on the same stable-3.0 branch.
+* Gerrit v2.14.8 introduced the support for ElasticSearch as a new feature. This was an exception
+  agreed amongst the Gerrit maintainers, did not touch the Lucene indexing code-base, was supported
+  by container-based E2E tests and represents a completion of an high-level feature.
+
 [[backporting]]
 == Backporting to stable branches
 
diff --git a/Documentation/pg-plugin-admin-api.txt b/Documentation/pg-plugin-admin-api.txt
index 1c724d0..084fa2c 100644
--- a/Documentation/pg-plugin-admin-api.txt
+++ b/Documentation/pg-plugin-admin-api.txt
@@ -4,7 +4,7 @@
 and provides customization of the admin menu.
 
 == addMenuLink
-`adminApi.addMenuLink(text, url, opt_external)`
+`adminApi.addMenuLink(text, url, opt_external, opt_capabilities)`
 
 Add a new link to the end of the admin navigation menu.
 
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index f958f44..d2f4d97 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -3088,6 +3088,12 @@
 The reviewer to be added to the change must be provided in the request
 body as a link:#reviewer-input[ReviewerInput] entity.
 
+Users can be moved from reviewer to CC and vice versa. This means if a
+user is added as CC that is already a reviewer on the change, the
+reviewer state of that user is updated to CC. If a user that is already
+a CC on the change is added as reviewer, the reviewer state of that
+user is updated to reviewer.
+
 .Request
 ----
   POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/reviewers HTTP/1.0
@@ -3131,6 +3137,12 @@
 If a group with many members is added as reviewer a confirmation may be
 required.
 
+If a group is added as CC and members of this group are already
+reviewers on the change, these members stay reviewers on the change
+(they are not downgraded to CC). However if a group is added as
+reviewer, all group members become reviewer of the change, even if they
+have been added as CC before.
+
 .Request
 ----
   POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/reviewers HTTP/1.0
diff --git a/WORKSPACE b/WORKSPACE
index f1a1a79..83ee604 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -64,9 +64,26 @@
     urls = ["https://raw.githubusercontent.com/google/closure-compiler/35d2b3340ff23a69441f10fa3bc820691c2942f2/contrib/externs/polymer-1.0.js"],
 )
 
-load("@bazel_skylib//lib:versions.bzl", "versions")
+# Check Bazel version when invoked by Bazel directly
+load("//tools/bzl:bazelisk_version.bzl", "bazelisk_version")
 
-versions.check(minimum_bazel_version = "0.26.1")
+bazelisk_version(name = "bazelisk_version")
+
+load("@bazelisk_version//:check.bzl", "check_bazel_version")
+
+check_bazel_version()
+
+# Rules Python
+http_archive(
+    name = "rules_python",
+    sha256 = "b5bab4c47e863e0fbb77df4a40c45ca85f98f5a2826939181585644c9f31b97b",
+    strip_prefix = "rules_python-9d68f24659e8ce8b736590ba1e4418af06ec2552",
+    urls = ["https://github.com/bazelbuild/rules_python/archive/9d68f24659e8ce8b736590ba1e4418af06ec2552.tar.gz"],
+)
+
+load("@rules_python//python:repositories.bzl", "py_repositories")
+
+py_repositories()
 
 load("@io_bazel_rules_closure//closure:defs.bzl", "closure_repositories")
 
@@ -106,6 +123,17 @@
 
 gazelle_dependencies()
 
+# Protobuf rules support
+http_archive(
+    name = "rules_proto",
+    sha256 = "602e7161d9195e50246177e7c55b2f39950a9cf7366f74ed5f22fd45750cd208",
+    strip_prefix = "rules_proto-97d8af4dc474595af3900dd85cb3a29ad28cc313",
+    urls = [
+        "https://mirror.bazel.build/github.com/bazelbuild/rules_proto/archive/97d8af4dc474595af3900dd85cb3a29ad28cc313.tar.gz",
+        "https://github.com/bazelbuild/rules_proto/archive/97d8af4dc474595af3900dd85cb3a29ad28cc313.tar.gz",
+    ],
+)
+
 # Dependencies for PolyGerrit local dev server.
 go_repository(
     name = "com_github_howeyc_fsnotify",
@@ -723,7 +751,7 @@
     sha1 = "f7be08ec23c21485b9b5a1cf1654c2ec8c58168d",
 )
 
-GITILES_VERS = "0.3-2"
+GITILES_VERS = "0.3-4"
 
 GITILES_REPO = GERRIT
 
@@ -732,14 +760,14 @@
     artifact = "com.google.gitiles:blame-cache:" + GITILES_VERS,
     attach_source = False,
     repository = GITILES_REPO,
-    sha1 = "f19d4ccddad1e39165ff4c60a723f5e543a02f80",
+    sha1 = "d1d62c9905b0cc9e066d337b33480599f430eb87",
 )
 
 maven_jar(
     name = "gitiles-servlet",
     artifact = "com.google.gitiles:gitiles-servlet:" + GITILES_VERS,
     repository = GITILES_REPO,
-    sha1 = "1d4fd7358d6798cc4f4ecf5d6336523c566d7618",
+    sha1 = "7360cb90576a813cd1288cf853c59824fe92467e",
 )
 
 # prettify must match the version used in Gitiles
@@ -752,8 +780,8 @@
 # Keep this version of Soy synchronized with the version used in Gitiles.
 maven_jar(
     name = "soy",
-    artifact = "com.google.template:soy:2019-08-22",
-    sha1 = "d4bf390caf7aa448108a5b9ec1b51f46820438f3",
+    artifact = "com.google.template:soy:2019-09-03",
+    sha1 = "40781da0302b4b5d53006dc8bd5a432c7288d807",
 )
 
 maven_jar(
@@ -1072,18 +1100,18 @@
     sha1 = "0f5a654e4675769c716e5b387830d19b501ca191",
 )
 
-TESTCONTAINERS_VERSION = "1.12.0"
+TESTCONTAINERS_VERSION = "1.12.1"
 
 maven_jar(
     name = "testcontainers",
     artifact = "org.testcontainers:testcontainers:" + TESTCONTAINERS_VERSION,
-    sha1 = "ac89643ce1ddde504da09172086aba0c7df10bff",
+    sha1 = "1dc8666ead914c5515d087f75ffe92629414caf6",
 )
 
 maven_jar(
     name = "testcontainers-elasticsearch",
     artifact = "org.testcontainers:elasticsearch:" + TESTCONTAINERS_VERSION,
-    sha1 = "cd9020f1803396c45ef935312bf232f9b17332b0",
+    sha1 = "2491f792627a1f15d341bfcd6dd0ea7e3541d82f",
 )
 
 maven_jar(
@@ -1119,13 +1147,13 @@
 BYTE_BUDDY_VERSION = "1.9.7"
 
 maven_jar(
-    name = "byte-buddy",
+    name = "bytebuddy",
     artifact = "net.bytebuddy:byte-buddy:" + BYTE_BUDDY_VERSION,
     sha1 = "8fea78fea6449e1738b675cb155ce8422661e237",
 )
 
 maven_jar(
-    name = "byte-buddy-agent",
+    name = "bytebuddy-agent",
     artifact = "net.bytebuddy:byte-buddy-agent:" + BYTE_BUDDY_VERSION,
     sha1 = "8e7d1b599f4943851ffea125fd9780e572727fc0",
 )
diff --git a/antlr3/BUILD b/antlr3/BUILD
index 2d3050e..549946a 100644
--- a/antlr3/BUILD
+++ b/antlr3/BUILD
@@ -1,3 +1,4 @@
+load("@rules_java//java:defs.bzl", "java_library")
 load("//tools/bzl:genrule2.bzl", "genrule2")
 
 genrule2(
diff --git a/contrib/remove-notedb-refs.sh b/contrib/remove-notedb-refs.sh
new file mode 100755
index 0000000..3c13067
--- /dev/null
+++ b/contrib/remove-notedb-refs.sh
@@ -0,0 +1,56 @@
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#!/usr/bin/env bash
+
+set -e
+
+if [[ "$#" -lt "2" ]] ; then
+  cat <<EOF
+Usage: run "$0 /path/to/git/dir [project...]" or "$0 /path/to/git/dir ALL"
+
+This util script can be used in case of rollback to ReviewDB during an unsuccessful
+migration to NoteDB or simply while testing the migration process.
+
+It will remove all the refs used by NoteDB added during the migration (i.e.: change meta refs and sequence ref).
+EOF
+  exit 1
+fi
+
+GERRIT_GIT_DIR=$1
+shift
+
+ALL_PROJECTS=$@
+if [[ "$2" -eq "ALL" ]] ; then
+ ALL_PROJECTS=`find "${GERRIT_GIT_DIR}" -type d -name "*.git"`
+fi
+
+ALL_PROJECTS_ARRAY=(${ALL_PROJECTS// / })
+
+for project in "${ALL_PROJECTS_ARRAY[@]}"
+do
+    if [[ "$project" =~ /All-Users\.git$ ]]; then
+        echo "Skipping $project ..."
+    else
+        echo "Removing meta ref for $project ..."
+        cd "$project"
+        if `git show-ref meta | grep -q "/meta$"`; then
+            git show-ref meta | grep "/meta$" | cut -d' ' -f2 | xargs -L1 git update-ref -d
+        fi
+    fi
+done
+
+echo "Remove sequence ref"
+allProjectDir="$GERRIT_GIT_DIR/All-Projects.git"
+cd $allProjectDir
+git update-ref -d refs/sequences/changes
diff --git a/e2e-tests/load-tests/.gitignore b/e2e-tests/load-tests/.gitignore
new file mode 100644
index 0000000..052f424
--- /dev/null
+++ b/e2e-tests/load-tests/.gitignore
@@ -0,0 +1,16 @@
+.idea/
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+### Scala ###
+*.class
+*.log
+target
+project/target
diff --git a/e2e-tests/load-tests/build.sbt b/e2e-tests/load-tests/build.sbt
new file mode 100644
index 0000000..46a3202
--- /dev/null
+++ b/e2e-tests/load-tests/build.sbt
@@ -0,0 +1,18 @@
+import Dependencies._
+
+enablePlugins(GatlingPlugin)
+
+lazy val gatlingGitExtension = RootProject(uri("git://github.com/GerritForge/gatling-git.git"))
+lazy val root = (project in file("."))
+  .settings(
+    inThisBuild(List(
+      organization := "com.google.gerrit",
+      scalaVersion := "2.12.8",
+      version := "0.1.0-SNAPSHOT"
+    )),
+    name := "gerrit",
+    libraryDependencies ++=
+      gatling ++
+        Seq("io.gatling" % "gatling-core" % "3.1.1" ) ++
+        Seq("io.gatling" % "gatling-app" % "3.1.1" )
+  ) dependsOn(gatlingGitExtension)
diff --git a/e2e-tests/load-tests/project/Dependencies.scala b/e2e-tests/load-tests/project/Dependencies.scala
new file mode 100644
index 0000000..72d2ac2
--- /dev/null
+++ b/e2e-tests/load-tests/project/Dependencies.scala
@@ -0,0 +1,8 @@
+import sbt._
+
+object Dependencies {
+  lazy val gatling = Seq(
+    "io.gatling.highcharts" % "gatling-charts-highcharts",
+    "io.gatling" % "gatling-test-framework",
+  ).map(_ % "3.1.1" % Test)
+}
diff --git a/e2e-tests/load-tests/project/build.properties b/e2e-tests/load-tests/project/build.properties
new file mode 100644
index 0000000..0cd8b07
--- /dev/null
+++ b/e2e-tests/load-tests/project/build.properties
@@ -0,0 +1 @@
+sbt.version=1.2.3
diff --git a/e2e-tests/load-tests/project/plugins.sbt b/e2e-tests/load-tests/project/plugins.sbt
new file mode 100644
index 0000000..36cd201
--- /dev/null
+++ b/e2e-tests/load-tests/project/plugins.sbt
@@ -0,0 +1 @@
+addSbtPlugin("io.gatling" % "gatling-sbt" % "3.0.0")
diff --git a/e2e-tests/load-tests/src/test/resources/application.conf b/e2e-tests/load-tests/src/test/resources/application.conf
new file mode 100644
index 0000000..33da75d
--- /dev/null
+++ b/e2e-tests/load-tests/src/test/resources/application.conf
@@ -0,0 +1,30 @@
+http {
+  username: "default_username",
+  username: ${?GIT_HTTP_USERNAME},
+
+  password: "default_password",
+  password: ${?GIT_HTTP_PASSWORD},
+}
+
+ssh {
+  private_key_path: "/tmp/ssh-keys/id_rsa",
+  private_key_path: ${?GIT_SSH_PRIVATE_KEY_PATH},
+}
+
+tmpFiles {
+  basePath: "/tmp"
+  basePath: ${?TMP_BASE_PATH}
+}
+
+commands {
+  push {
+    numFiles: 4
+    numFiles: ${?NUM_FILES}
+    minContentLength: 100
+    minContentLength: ${?MIN_CONTENT_LEGTH}
+    maxContentLength: 10000
+    maxContentLength: ${?MAX_CONTENT_LEGTH}
+    commitPrefix: ""
+    commitPrefix: ${?COMMIT_PREFIX}
+  }
+}
diff --git a/e2e-tests/load-tests/src/test/resources/data/requests.json b/e2e-tests/load-tests/src/test/resources/data/requests.json
new file mode 100644
index 0000000..86f9bf1
--- /dev/null
+++ b/e2e-tests/load-tests/src/test/resources/data/requests.json
@@ -0,0 +1,26 @@
+[
+  {
+    "url": "ssh://admin@localhost:29418/loadtest-repo",
+    "cmd": "clone"
+  },
+  {
+    "url": "ssh://admin@localhost:29418/loadtest-repo",
+    "cmd": "pull"
+  },
+  {
+    "url": "ssh://admin@localhost:29418/loadtest-repo",
+    "cmd": "push"
+  },
+  {
+    "url": "http://localhost:8080/loadtest-repo",
+    "cmd": "clone"
+  },
+  {
+    "url": "http://localhost:8080/loadtest-repo",
+    "cmd": "pull"
+  },
+  {
+    "url": "http://localhost:8080/loadtest-repo",
+    "cmd": "push"
+  }
+]
diff --git a/e2e-tests/load-tests/src/test/resources/gatling.conf b/e2e-tests/load-tests/src/test/resources/gatling.conf
new file mode 100644
index 0000000..94c371b
--- /dev/null
+++ b/e2e-tests/load-tests/src/test/resources/gatling.conf
@@ -0,0 +1,128 @@
+#########################
+# Gatling Configuration #
+#########################
+
+# This file contains all the settings configurable for Gatling with their default values
+
+gatling {
+  core {
+    #outputDirectoryBaseName = "" # The prefix for each simulation result folder (then suffixed by the report generation timestamp)
+    #runDescription = ""          # The description for this simulation run, displayed in each report
+    #encoding = "utf-8"           # Encoding to use throughout Gatling for file and string manipulation
+    #simulationClass = ""         # The FQCN of the simulation to run (when used in conjunction with noReports, the simulation for which assertions will be validated)
+    #elFileBodiesCacheMaxCapacity = 200        # Cache size for request body EL templates, set to 0 to disable
+    #rawFileBodiesCacheMaxCapacity = 200       # Cache size for request body Raw templates, set to 0 to disable
+    #rawFileBodiesInMemoryMaxSize = 1000       # Below this limit, raw file bodies will be cached in memory
+    #pebbleFileBodiesCacheMaxCapacity = 200    # Cache size for request body Peeble templates, set to 0 to disable
+    #shutdownTimeout = 5000                    # Milliseconds to wait for the actor system to shutdown
+    extract {
+      regex {
+        #cacheMaxCapacity = 200 # Cache size for the compiled regexes, set to 0 to disable caching
+      }
+      xpath {
+        #cacheMaxCapacity = 200 # Cache size for the compiled XPath queries,  set to 0 to disable caching
+      }
+      jsonPath {
+        #cacheMaxCapacity = 200 # Cache size for the compiled jsonPath queries, set to 0 to disable caching
+        #preferJackson = false  # When set to true, prefer Jackson over Boon for JSON-related operations
+      }
+      css {
+        #cacheMaxCapacity = 200 # Cache size for the compiled CSS selectors queries,  set to 0 to disable caching
+      }
+    }
+    directory {
+      simulations = "./src/test/scala"
+      #simulations = user-files/simulations # Directory where simulation classes are located (for bundle packaging only)
+      resources = "./src/test/resources/data"     # Directory where resources, such as feeder files and request bodies are located (for bundle packaging only)
+      #reportsOnly = ""                     # If set, name of report folder to look for in order to generate its report
+      binaries = "./target/scala-2.12/classes"                        # If set, name of the folder where compiles classes are located: Defaults to GATLING_HOME/target.
+      #results = results                    # Name of the folder where all reports folder are located
+    }
+  }
+  charting {
+    #noReports = false       # When set to true, don't generate HTML reports
+    #maxPlotPerSeries = 1000 # Number of points per graph in Gatling reports
+    #useGroupDurationMetric = false  # Switch group timings from cumulated response time to group duration.
+    indicators {
+      #lowerBound = 800      # Lower bound for the requests' response time to track in the reports and the console summary
+      #higherBound = 1200    # Higher bound for the requests' response time to track in the reports and the console summary
+      #percentile1 = 50      # Value for the 1st percentile to track in the reports, the console summary and Graphite
+      #percentile2 = 75      # Value for the 2nd percentile to track in the reports, the console summary and Graphite
+      #percentile3 = 95      # Value for the 3rd percentile to track in the reports, the console summary and Graphite
+      #percentile4 = 99      # Value for the 4th percentile to track in the reports, the console summary and Graphite
+    }
+  }
+  http {
+    #fetchedCssCacheMaxCapacity = 200          # Cache size for CSS parsed content, set to 0 to disable
+    #fetchedHtmlCacheMaxCapacity = 200         # Cache size for HTML parsed content, set to 0 to disable
+    #perUserCacheMaxCapacity = 200             # Per virtual user cache size, set to 0 to disable
+    #warmUpUrl = "https://gatling.io"           # The URL to use to warm-up the HTTP stack (blank means disabled)
+    #enableGA = true                           # Very light Google Analytics, please support
+    ssl {
+      keyStore {
+        #type = ""      # Type of SSLContext's KeyManagers store
+        #file = ""      # Location of SSLContext's KeyManagers store
+        #password = ""  # Password for SSLContext's KeyManagers store
+        #algorithm = "" # Algorithm used SSLContext's KeyManagers store
+      }
+      trustStore {
+        #type = ""      # Type of SSLContext's TrustManagers store
+        #file = ""      # Location of SSLContext's TrustManagers store
+        #password = ""  # Password for SSLContext's TrustManagers store
+        #algorithm = "" # Algorithm used by SSLContext's TrustManagers store
+      }
+    }
+    ahc {
+      #connectTimeout = 10000                              # Timeout in millis for establishing a TCP socket
+      #handshakeTimeout = 10000                            # Timeout in millis for performing TLS handshake
+      #pooledConnectionIdleTimeout = 60000                 # Timeout in millis for a connection to stay idle in the pool
+      #maxRetry = 2                                        # Number of times that a request should be tried again
+      #requestTimeout = 60000                              # Timeout in millis for performing an HTTP request
+      #enableSni = true                                    # When set to true, enable Server Name indication (SNI)
+      #enableHostnameVerification = false                  # When set to true, enable hostname verification: SSLEngine.setHttpsEndpointIdentificationAlgorithm("HTTPS")
+      #useInsecureTrustManager = true                      # Use an insecure TrustManager that trusts all server certificates
+      #filterInsecureCipherSuites = true                   # Turn to false to not filter out insecure and weak cipher suites
+      #sslEnabledProtocols = [TLSv1.2, TLSv1.1, TLSv1]     # Array of enabled protocols for HTTPS, if empty use the JDK defaults
+      #sslEnabledCipherSuites = []                         # Array of enabled cipher suites for HTTPS, if empty use the AHC defaults
+      #sslSessionCacheSize = 0                             # SSLSession cache size, set to 0 to use JDK's default
+      #sslSessionTimeout = 0                               # SSLSession timeout in seconds, set to 0 to use JDK's default (24h)
+      #disableSslSessionResumption = false                 # if true, SSLSessions won't be resumed
+      #useOpenSsl = true                                   # if OpenSSL should be used instead of JSSE
+      #useNativeTransport = false                          # if native transport should be used instead of Java NIO (requires netty-transport-native-epoll, currently Linux only)
+      #enableZeroCopy = true                               # if zero-copy upload should be used if possible
+      #tcpNoDelay = true
+      #soReuseAddress = false
+      #allocator = "pooled"                            # switch to unpooled for unpooled ByteBufAllocator
+      #maxThreadLocalCharBufferSize = 200000           # Netty's default is 16k
+    }
+    dns {
+      #queryTimeout = 5000                             # Timeout in millis of each DNS query in millis
+      #maxQueriesPerResolve = 6                        # Maximum allowed number of DNS queries for a given name resolution
+    }
+  }
+  jms {
+    #replyTimeoutScanPeriod = 1000  # scan period for timedout reply messages
+  }
+  data {
+    #writers = [console, file]      # The list of DataWriters to which Gatling write simulation data (currently supported : console, file, graphite, jdbc)
+    console {
+      #light = false                # When set to true, displays a light version without detailed request stats
+      #writePeriod = 5              # Write interval, in seconds
+    }
+    file {
+      #bufferSize = 8192            # FileDataWriter's internal data buffer size, in bytes
+    }
+    leak {
+      #noActivityTimeout = 30  # Period, in seconds, for which Gatling may have no activity before considering a leak may be happening
+    }
+    graphite {
+      #light = false              # only send the all* stats
+      #host = "localhost"         # The host where the Carbon server is located
+      #port = 2003                # The port to which the Carbon server listens to (2003 is default for plaintext, 2004 is default for pickle)
+      #protocol = "tcp"           # The protocol used to send data to Carbon (currently supported : "tcp", "udp")
+      #rootPathPrefix = "gatling" # The common prefix of all metrics sent to Graphite
+      #bufferSize = 8192          # Internal data buffer size, in bytes
+      #writePeriod = 1            # Write period, in seconds
+    }
+  }
+}
diff --git a/e2e-tests/load-tests/src/test/resources/hooks/commit-msg b/e2e-tests/load-tests/src/test/resources/hooks/commit-msg
new file mode 100644
index 0000000..b05a671
--- /dev/null
+++ b/e2e-tests/load-tests/src/test/resources/hooks/commit-msg
@@ -0,0 +1,43 @@
+#!/bin/sh
+#
+# Part of Gerrit Code Review (https://www.gerritcodereview.com/)
+#
+# 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.
+
+# avoid [[ which is not POSIX sh.
+if test "$#" != 1 ; then
+  echo "$0 requires an argument."
+  exit 1
+fi
+
+if test ! -f "$1" ; then
+  echo "file does not exist: $1"
+  exit 1
+fi
+
+if test ! -s "$1" ; then
+  echo "file is empty: $1"
+  exit 1
+fi
+
+# $RANDOM will be undefined if not using bash, so don't use set -u
+random=$( (whoami ; hostname ; date; cat $1 ; echo $RANDOM) | git hash-object --stdin)
+dest="$1.tmp.${random}"
+
+# Avoid the --in-place option which only appeared in Git 2.8
+# Avoid the --if-exists option which only appeared in Git 2.15
+cat "$1" \
+| git -c trailer.ifexists=doNothing interpret-trailers --trailer "Change-Id: I${random}" > "${dest}" \
+&& mv "${dest}" "$1"
diff --git a/e2e-tests/load-tests/src/test/scala/com/google/gerrit/scenarios/ReplayRecordsFromFeeder.scala b/e2e-tests/load-tests/src/test/scala/com/google/gerrit/scenarios/ReplayRecordsFromFeeder.scala
new file mode 100644
index 0000000..c0eab39
--- /dev/null
+++ b/e2e-tests/load-tests/src/test/scala/com/google/gerrit/scenarios/ReplayRecordsFromFeeder.scala
@@ -0,0 +1,72 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.scenarios
+
+import com.github.barbasa.gatling.git.protocol.GitProtocol
+import com.github.barbasa.gatling.git.request.builder.GitRequestBuilder
+import io.gatling.core.Predef._
+import io.gatling.core.structure.ScenarioBuilder
+import java.io._
+
+import com.github.barbasa.gatling.git.{
+  GatlingGitConfiguration,
+  GitRequestSession
+}
+import org.apache.commons.io.FileUtils
+
+import scala.concurrent.duration._
+import org.eclipse.jgit.hooks._
+
+class ReplayRecordsFromFeederScenario extends Simulation {
+
+  val gitProtocol = GitProtocol()
+  implicit val conf = GatlingGitConfiguration()
+  implicit val postMessageHook: Option[String] = Some(
+    s"hooks/${CommitMsgHook.NAME}")
+
+  val feeder = jsonFile("data/requests.json").circular
+
+  val replayCallsScenario: ScenarioBuilder =
+    scenario("Git commands")
+      .repeat(10000) {
+        feed(feeder)
+          .exec(new GitRequestBuilder(GitRequestSession("${cmd}", "${url}")))
+      }
+
+  setUp(
+    replayCallsScenario.inject(
+      nothingFor(4 seconds),
+      atOnceUsers(10),
+      rampUsers(10) during (5 seconds),
+      constantUsersPerSec(20) during (15 seconds),
+      constantUsersPerSec(20) during (15 seconds) randomized
+    ))
+    .protocols(gitProtocol)
+    .maxDuration(60 seconds)
+
+  after {
+    try {
+      //After is often called too early. Some retries should be implemented.
+      Thread.sleep(5000)
+      FileUtils.deleteDirectory(new File(conf.tmpBasePath))
+    } catch {
+      case e: IOException => {
+        System.err.println(
+          "Unable to delete temporary directory: " + conf.tmpBasePath)
+        e.printStackTrace
+      }
+    }
+  }
+}
diff --git a/java/BUILD b/java/BUILD
index 4fc4d79..77611e4 100644
--- a/java/BUILD
+++ b/java/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_binary", "java_library")
+
 java_binary(
     name = "gerrit-main-class",
     main_class = "Main",
diff --git a/java/com/google/gerrit/acceptance/BUILD b/java/com/google/gerrit/acceptance/BUILD
index ef9f4e6..eca7ea6 100644
--- a/java/com/google/gerrit/acceptance/BUILD
+++ b/java/com/google/gerrit/acceptance/BUILD
@@ -1,3 +1,4 @@
+load("@rules_java//java:defs.bzl", "java_binary", "java_library")
 load("//tools/bzl:java.bzl", "java_library2")
 load("//tools/bzl:javadoc.bzl", "java_doc")
 
@@ -126,6 +127,7 @@
         "//java/com/google/gerrit/server/git/receive",
         "//java/com/google/gerrit/server/restapi",
         "//java/com/google/gerrit/server/schema",
+        "//java/com/google/gerrit/server/util/git",
         "//java/com/google/gerrit/server/util/time",
         "//java/com/google/gerrit/sshd",
         "//lib:args4j",
diff --git a/java/com/google/gerrit/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java
index a48a278..ea63d73 100644
--- a/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/java/com/google/gerrit/acceptance/GerritServer.java
@@ -259,6 +259,8 @@
           // Silence non-critical messages from JGit.
           .put("org.eclipse.jgit.transport.PacketLineIn", Level.WARN)
           .put("org.eclipse.jgit.transport.PacketLineOut", Level.WARN)
+          .put("org.eclipse.jgit.internal.storage.file.FileSnapshot", Level.WARN)
+          .put("org.eclipse.jgit.util.FS", Level.WARN)
           .build();
 
   private static boolean forceLocalDisk() {
diff --git a/java/com/google/gerrit/acceptance/StandaloneSiteTest.java b/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
index eac3b0a..933c4e1 100644
--- a/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
+++ b/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.server.util.ManualRequestContext;
 import com.google.gerrit.server.util.OneOffRequestContext;
 import com.google.gerrit.server.util.RequestContext;
+import com.google.gerrit.server.util.git.DelegateSystemReader;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.inject.Injector;
 import com.google.inject.Module;
@@ -134,22 +135,7 @@
   private static SystemReader setFakeSystemReader(File tempDir) {
     SystemReader oldSystemReader = SystemReader.getInstance();
     SystemReader.setInstance(
-        new SystemReader() {
-          @Override
-          public String getHostname() {
-            return oldSystemReader.getHostname();
-          }
-
-          @Override
-          public String getenv(String variable) {
-            return oldSystemReader.getenv(variable);
-          }
-
-          @Override
-          public String getProperty(String key) {
-            return oldSystemReader.getProperty(key);
-          }
-
+        new DelegateSystemReader(oldSystemReader) {
           @Override
           public FileBasedConfig openUserConfig(Config parent, FS fs) {
             return new FileBasedConfig(parent, new File(tempDir, "user.config"), FS.detect());
@@ -159,16 +145,6 @@
           public FileBasedConfig openSystemConfig(Config parent, FS fs) {
             return new FileBasedConfig(parent, new File(tempDir, "system.config"), FS.detect());
           }
-
-          @Override
-          public long getCurrentTime() {
-            return oldSystemReader.getCurrentTime();
-          }
-
-          @Override
-          public int getTimezone(long when) {
-            return oldSystemReader.getTimezone(when);
-          }
         });
     return oldSystemReader;
   }
diff --git a/java/com/google/gerrit/asciidoctor/BUILD b/java/com/google/gerrit/asciidoctor/BUILD
index f5178a0..94ec20d 100644
--- a/java/com/google/gerrit/asciidoctor/BUILD
+++ b/java/com/google/gerrit/asciidoctor/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_binary", "java_library")
+
 java_binary(
     name = "asciidoc",
     main_class = "com.google.gerrit.asciidoctor.AsciiDoctor",
diff --git a/java/com/google/gerrit/common/BUILD b/java/com/google/gerrit/common/BUILD
index b35b8bf..113ff53 100644
--- a/java/com/google/gerrit/common/BUILD
+++ b/java/com/google/gerrit/common/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 ANNOTATIONS = [
     "Nullable.java",
     "UsedAt.java",
diff --git a/java/com/google/gerrit/common/data/testing/BUILD b/java/com/google/gerrit/common/data/testing/BUILD
index 32815d5..8ab01de 100644
--- a/java/com/google/gerrit/common/data/testing/BUILD
+++ b/java/com/google/gerrit/common/data/testing/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "common-data-test-util",
     testonly = True,
diff --git a/java/com/google/gerrit/elasticsearch/BUILD b/java/com/google/gerrit/elasticsearch/BUILD
index f919aad..a9b145b 100644
--- a/java/com/google/gerrit/elasticsearch/BUILD
+++ b/java/com/google/gerrit/elasticsearch/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "elasticsearch",
     srcs = glob(["**/*.java"]),
diff --git a/java/com/google/gerrit/exceptions/BUILD b/java/com/google/gerrit/exceptions/BUILD
index 50bf883..e08c3fd 100644
--- a/java/com/google/gerrit/exceptions/BUILD
+++ b/java/com/google/gerrit/exceptions/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "exceptions",
     srcs = glob(["*.java"]),
diff --git a/java/com/google/gerrit/extensions/BUILD b/java/com/google/gerrit/extensions/BUILD
index b69d2c8..0022584 100644
--- a/java/com/google/gerrit/extensions/BUILD
+++ b/java/com/google/gerrit/extensions/BUILD
@@ -1,5 +1,6 @@
-load("//lib/jgit:jgit.bzl", "JGIT_DOC_URL")
+load("@rules_java//java:defs.bzl", "java_binary", "java_library")
 load("//lib:guava.bzl", "GUAVA_DOC_URL")
+load("//lib/jgit:jgit.bzl", "JGIT_DOC_URL")
 load("//tools/bzl:javadoc.bzl", "java_doc")
 
 java_binary(
diff --git a/java/com/google/gerrit/extensions/common/testing/BUILD b/java/com/google/gerrit/extensions/common/testing/BUILD
index 7092d21..9cecb66 100644
--- a/java/com/google/gerrit/extensions/common/testing/BUILD
+++ b/java/com/google/gerrit/extensions/common/testing/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "common-test-util",
     testonly = True,
diff --git a/java/com/google/gerrit/extensions/restapi/testing/BUILD b/java/com/google/gerrit/extensions/restapi/testing/BUILD
index 3417cae2..4c44d2a 100644
--- a/java/com/google/gerrit/extensions/restapi/testing/BUILD
+++ b/java/com/google/gerrit/extensions/restapi/testing/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "restapi-test-util",
     testonly = True,
diff --git a/java/com/google/gerrit/git/BUILD b/java/com/google/gerrit/git/BUILD
index fc146dc..5ece37a 100644
--- a/java/com/google/gerrit/git/BUILD
+++ b/java/com/google/gerrit/git/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "git",
     srcs = glob(["*.java"]),
diff --git a/java/com/google/gerrit/git/testing/BUILD b/java/com/google/gerrit/git/testing/BUILD
index 497510d..13fddc1 100644
--- a/java/com/google/gerrit/git/testing/BUILD
+++ b/java/com/google/gerrit/git/testing/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 package(default_testonly = True)
 
 java_library(
diff --git a/java/com/google/gerrit/gpg/BUILD b/java/com/google/gerrit/gpg/BUILD
index 49806cf..d45a86d 100644
--- a/java/com/google/gerrit/gpg/BUILD
+++ b/java/com/google/gerrit/gpg/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "gpg",
     srcs = glob(["**/*.java"]),
diff --git a/java/com/google/gerrit/gpg/testing/BUILD b/java/com/google/gerrit/gpg/testing/BUILD
index 0282d3a..b227dd5 100644
--- a/java/com/google/gerrit/gpg/testing/BUILD
+++ b/java/com/google/gerrit/gpg/testing/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "gpg-test-util",
     testonly = True,
diff --git a/java/com/google/gerrit/httpd/BUILD b/java/com/google/gerrit/httpd/BUILD
index f86b35d5..c4b0314 100644
--- a/java/com/google/gerrit/httpd/BUILD
+++ b/java/com/google/gerrit/httpd/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "httpd",
     srcs = glob(["**/*.java"]),
diff --git a/java/com/google/gerrit/httpd/auth/oauth/BUILD b/java/com/google/gerrit/httpd/auth/oauth/BUILD
index dd3e5fc..b74a65a 100644
--- a/java/com/google/gerrit/httpd/auth/oauth/BUILD
+++ b/java/com/google/gerrit/httpd/auth/oauth/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "oauth",
     srcs = glob(["**/*.java"]),
diff --git a/java/com/google/gerrit/httpd/auth/openid/BUILD b/java/com/google/gerrit/httpd/auth/openid/BUILD
index edd12cc..ad9d3aa 100644
--- a/java/com/google/gerrit/httpd/auth/openid/BUILD
+++ b/java/com/google/gerrit/httpd/auth/openid/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "openid",
     srcs = glob(["**/*.java"]),
diff --git a/java/com/google/gerrit/httpd/init/BUILD b/java/com/google/gerrit/httpd/init/BUILD
index df072b2..09772fd 100644
--- a/java/com/google/gerrit/httpd/init/BUILD
+++ b/java/com/google/gerrit/httpd/init/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "init",
     srcs = glob(["**/*.java"]),
diff --git a/java/com/google/gerrit/index/BUILD b/java/com/google/gerrit/index/BUILD
index 7fcf342..5b6aae5 100644
--- a/java/com/google/gerrit/index/BUILD
+++ b/java/com/google/gerrit/index/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 QUERY_PARSE_EXCEPTION_SRCS = [
     "query/QueryParseException.java",
     "query/QueryRequiresAuthException.java",
diff --git a/java/com/google/gerrit/index/project/BUILD b/java/com/google/gerrit/index/project/BUILD
index f32d8c0..2c460fd 100644
--- a/java/com/google/gerrit/index/project/BUILD
+++ b/java/com/google/gerrit/index/project/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "project",
     srcs = glob(["*.java"]),
diff --git a/java/com/google/gerrit/index/query/testing/BUILD b/java/com/google/gerrit/index/query/testing/BUILD
index ee346a8..1785f49 100644
--- a/java/com/google/gerrit/index/query/testing/BUILD
+++ b/java/com/google/gerrit/index/query/testing/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 package(
     default_testonly = True,
     default_visibility = ["//visibility:public"],
diff --git a/java/com/google/gerrit/jgit/BUILD b/java/com/google/gerrit/jgit/BUILD
index 2387614..e67ebfe 100644
--- a/java/com/google/gerrit/jgit/BUILD
+++ b/java/com/google/gerrit/jgit/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "jgit",
     srcs = [
diff --git a/java/com/google/gerrit/json/BUILD b/java/com/google/gerrit/json/BUILD
index 030dddc..439f23f 100644
--- a/java/com/google/gerrit/json/BUILD
+++ b/java/com/google/gerrit/json/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "json",
     srcs = glob(["*.java"]),
diff --git a/java/com/google/gerrit/launcher/BUILD b/java/com/google/gerrit/launcher/BUILD
index bac0c53..15fa0ce 100644
--- a/java/com/google/gerrit/launcher/BUILD
+++ b/java/com/google/gerrit/launcher/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 # NOTE: GerritLauncher must be a single, self-contained class. Do not add any
 # additional srcs or deps to this rule.
 java_library(
diff --git a/java/com/google/gerrit/lifecycle/BUILD b/java/com/google/gerrit/lifecycle/BUILD
index 7ba6123..a3f3d81 100644
--- a/java/com/google/gerrit/lifecycle/BUILD
+++ b/java/com/google/gerrit/lifecycle/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "lifecycle",
     srcs = glob(["**/*.java"]),
diff --git a/java/com/google/gerrit/lucene/BUILD b/java/com/google/gerrit/lucene/BUILD
index fa4c923..40b2548 100644
--- a/java/com/google/gerrit/lucene/BUILD
+++ b/java/com/google/gerrit/lucene/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 QUERY_BUILDER = ["QueryBuilder.java"]
 
 java_library(
diff --git a/java/com/google/gerrit/lucene/GerritIndexWriterConfig.java b/java/com/google/gerrit/lucene/GerritIndexWriterConfig.java
index 75e03e3..f6b2f0e 100644
--- a/java/com/google/gerrit/lucene/GerritIndexWriterConfig.java
+++ b/java/com/google/gerrit/lucene/GerritIndexWriterConfig.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.server.config.ConfigUtil;
 import org.apache.lucene.analysis.CharArraySet;
 import org.apache.lucene.analysis.standard.StandardAnalyzer;
+import org.apache.lucene.index.ConcurrentMergeScheduler;
 import org.apache.lucene.index.IndexWriterConfig;
 import org.apache.lucene.index.IndexWriterConfig.OpenMode;
 import org.eclipse.jgit.lib.Config;
@@ -42,6 +43,21 @@
         new IndexWriterConfig(analyzer)
             .setOpenMode(OpenMode.CREATE_OR_APPEND)
             .setCommitOnClose(true);
+
+    int maxMergeCount = cfg.getInt("index", name, "maxMergeCount", -1);
+    int maxThreadCount = cfg.getInt("index", name, "maxThreadCount", -1);
+    boolean enableAutoIOThrottle = cfg.getBoolean("index", name, "enableAutoIOThrottle", true);
+    if (maxMergeCount != -1 || maxThreadCount != -1 || !enableAutoIOThrottle) {
+      ConcurrentMergeScheduler mergeScheduler = new ConcurrentMergeScheduler();
+      if (maxMergeCount != -1 || maxThreadCount != -1) {
+        mergeScheduler.setMaxMergesAndThreads(maxMergeCount, maxThreadCount);
+      }
+      if (!enableAutoIOThrottle) {
+        mergeScheduler.disableAutoIOThrottle();
+      }
+      luceneConfig.setMergeScheduler(mergeScheduler);
+    }
+
     double m = 1 << 20;
     luceneConfig.setRAMBufferSizeMB(
         cfg.getLong(
diff --git a/java/com/google/gerrit/mail/BUILD b/java/com/google/gerrit/mail/BUILD
index 90bb82c..6be5f0e 100644
--- a/java/com/google/gerrit/mail/BUILD
+++ b/java/com/google/gerrit/mail/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "mail",
     srcs = glob(["*.java"]),
diff --git a/java/com/google/gerrit/metrics/BUILD b/java/com/google/gerrit/metrics/BUILD
index 68b29a4..9cc7654 100644
--- a/java/com/google/gerrit/metrics/BUILD
+++ b/java/com/google/gerrit/metrics/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "metrics",
     srcs = glob(["**/*.java"]),
diff --git a/java/com/google/gerrit/metrics/dropwizard/BUILD b/java/com/google/gerrit/metrics/dropwizard/BUILD
index 9adb375..4b3859f 100644
--- a/java/com/google/gerrit/metrics/dropwizard/BUILD
+++ b/java/com/google/gerrit/metrics/dropwizard/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "dropwizard",
     srcs = glob(["**/*.java"]),
diff --git a/java/com/google/gerrit/pgm/BUILD b/java/com/google/gerrit/pgm/BUILD
index 02c083c..ea6e9b7 100644
--- a/java/com/google/gerrit/pgm/BUILD
+++ b/java/com/google/gerrit/pgm/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 # TODO(davido): This indirection doesn't avoid unwanted depdencies
 # in acceptance-framework and should be removed. Instead, provided_deps
 # should be used, once https://github.com/bazelbuild/bazel/issues/1402
diff --git a/java/com/google/gerrit/pgm/http/BUILD b/java/com/google/gerrit/pgm/http/BUILD
index 838c614..34115ae 100644
--- a/java/com/google/gerrit/pgm/http/BUILD
+++ b/java/com/google/gerrit/pgm/http/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "http",
     visibility = ["//visibility:public"],
diff --git a/java/com/google/gerrit/pgm/http/jetty/BUILD b/java/com/google/gerrit/pgm/http/jetty/BUILD
index a6a13dc..ea3afe1 100644
--- a/java/com/google/gerrit/pgm/http/jetty/BUILD
+++ b/java/com/google/gerrit/pgm/http/jetty/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "jetty",
     srcs = glob(["**/*.java"]),
diff --git a/java/com/google/gerrit/pgm/init/BUILD b/java/com/google/gerrit/pgm/init/BUILD
index b2a4d72..eb0d49e 100644
--- a/java/com/google/gerrit/pgm/init/BUILD
+++ b/java/com/google/gerrit/pgm/init/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "init",
     srcs = glob(["**/*.java"]),
diff --git a/java/com/google/gerrit/pgm/init/api/BUILD b/java/com/google/gerrit/pgm/init/api/BUILD
index 5b07fc6..19203fc 100644
--- a/java/com/google/gerrit/pgm/init/api/BUILD
+++ b/java/com/google/gerrit/pgm/init/api/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "api",
     srcs = glob(["**/*.java"]),
diff --git a/java/com/google/gerrit/pgm/util/BUILD b/java/com/google/gerrit/pgm/util/BUILD
index ffd1cbd..94798f7 100644
--- a/java/com/google/gerrit/pgm/util/BUILD
+++ b/java/com/google/gerrit/pgm/util/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "util",
     srcs = glob(["**/*.java"]),
diff --git a/java/com/google/gerrit/prettify/BUILD b/java/com/google/gerrit/prettify/BUILD
index 88b5b60..76afbe7 100644
--- a/java/com/google/gerrit/prettify/BUILD
+++ b/java/com/google/gerrit/prettify/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "server",
     srcs = glob(["common/**/*.java"]),
diff --git a/java/com/google/gerrit/proto/BUILD b/java/com/google/gerrit/proto/BUILD
index 4f05bf6..98558c5 100644
--- a/java/com/google/gerrit/proto/BUILD
+++ b/java/com/google/gerrit/proto/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "proto",
     srcs = ["Protos.java"],
diff --git a/java/com/google/gerrit/proto/testing/BUILD b/java/com/google/gerrit/proto/testing/BUILD
index 48115ff..acfa8f0 100644
--- a/java/com/google/gerrit/proto/testing/BUILD
+++ b/java/com/google/gerrit/proto/testing/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 package(default_testonly = True)
 
 java_library(
diff --git a/java/com/google/gerrit/reviewdb/BUILD b/java/com/google/gerrit/reviewdb/BUILD
index 8c286ce..838aee8 100644
--- a/java/com/google/gerrit/reviewdb/BUILD
+++ b/java/com/google/gerrit/reviewdb/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 package(
     default_visibility = ["//visibility:public"],
 )
diff --git a/java/com/google/gerrit/server/ApprovalsUtil.java b/java/com/google/gerrit/server/ApprovalsUtil.java
index 9befb46..c4f13f1 100644
--- a/java/com/google/gerrit/server/ApprovalsUtil.java
+++ b/java/com/google/gerrit/server/ApprovalsUtil.java
@@ -239,17 +239,28 @@
    * @param notes change notes.
    * @param update change update.
    * @param wantCCs accounts to CC.
+   * @param keepExistingReviewers whether provided accounts that are already reviewer should be kept
+   *     as reviewer or be downgraded to CC
    * @return whether a change was made.
    */
   public Collection<Account.Id> addCcs(
-      ChangeNotes notes, ChangeUpdate update, Collection<Account.Id> wantCCs) {
-    return addCcs(update, wantCCs, notes.load().getReviewers());
+      ChangeNotes notes,
+      ChangeUpdate update,
+      Collection<Account.Id> wantCCs,
+      boolean keepExistingReviewers) {
+    return addCcs(update, wantCCs, notes.load().getReviewers(), keepExistingReviewers);
   }
 
   private Collection<Account.Id> addCcs(
-      ChangeUpdate update, Collection<Account.Id> wantCCs, ReviewerSet existingReviewers) {
+      ChangeUpdate update,
+      Collection<Account.Id> wantCCs,
+      ReviewerSet existingReviewers,
+      boolean keepExistingReviewers) {
     Set<Account.Id> need = new LinkedHashSet<>(wantCCs);
-    need.removeAll(existingReviewers.all());
+    need.removeAll(existingReviewers.byState(CC));
+    if (keepExistingReviewers) {
+      need.removeAll(existingReviewers.byState(REVIEWER));
+    }
     need.removeAll(update.getReviewers().keySet());
     for (Account.Id account : need) {
       update.putReviewer(account, CC);
diff --git a/java/com/google/gerrit/server/BUILD b/java/com/google/gerrit/server/BUILD
index 6d4dfcf..0992294 100644
--- a/java/com/google/gerrit/server/BUILD
+++ b/java/com/google/gerrit/server/BUILD
@@ -1,3 +1,4 @@
+load("@rules_java//java:defs.bzl", "java_library")
 load("//tools/bzl:javadoc.bzl", "java_doc")
 
 CONSTANTS_SRC = [
diff --git a/java/com/google/gerrit/server/api/BUILD b/java/com/google/gerrit/server/api/BUILD
index b9e26de..459c16a 100644
--- a/java/com/google/gerrit/server/api/BUILD
+++ b/java/com/google/gerrit/server/api/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "api",
     srcs = glob(
diff --git a/java/com/google/gerrit/server/audit/BUILD b/java/com/google/gerrit/server/audit/BUILD
index 71cd3a1..5c2a40a 100644
--- a/java/com/google/gerrit/server/audit/BUILD
+++ b/java/com/google/gerrit/server/audit/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "audit",
     srcs = glob(
diff --git a/java/com/google/gerrit/server/cache/h2/BUILD b/java/com/google/gerrit/server/cache/h2/BUILD
index f85b498..79baefc 100644
--- a/java/com/google/gerrit/server/cache/h2/BUILD
+++ b/java/com/google/gerrit/server/cache/h2/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "h2",
     srcs = glob(["**/*.java"]),
diff --git a/java/com/google/gerrit/server/cache/mem/BUILD b/java/com/google/gerrit/server/cache/mem/BUILD
index 4106714..eb0695e 100644
--- a/java/com/google/gerrit/server/cache/mem/BUILD
+++ b/java/com/google/gerrit/server/cache/mem/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "mem",
     srcs = glob(["*.java"]),
diff --git a/java/com/google/gerrit/server/cache/serialize/BUILD b/java/com/google/gerrit/server/cache/serialize/BUILD
index a3a2054..3547605 100644
--- a/java/com/google/gerrit/server/cache/serialize/BUILD
+++ b/java/com/google/gerrit/server/cache/serialize/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "serialize",
     srcs = glob(["*.java"]),
diff --git a/java/com/google/gerrit/server/cache/testing/BUILD b/java/com/google/gerrit/server/cache/testing/BUILD
index 16cbe17..09f698c 100644
--- a/java/com/google/gerrit/server/cache/testing/BUILD
+++ b/java/com/google/gerrit/server/cache/testing/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 package(default_testonly = True)
 
 java_library(
diff --git a/java/com/google/gerrit/server/change/AddReviewersOp.java b/java/com/google/gerrit/server/change/AddReviewersOp.java
index a8ebcb2..15d8c60 100644
--- a/java/com/google/gerrit/server/change/AddReviewersOp.java
+++ b/java/com/google/gerrit/server/change/AddReviewersOp.java
@@ -64,10 +64,14 @@
      * @param accountIds account IDs to add.
      * @param addresses email addresses to add.
      * @param state resulting reviewer state.
+     * @param forGroup whether this reviewer addition adds accounts for a group
      * @return batch update operation.
      */
     AddReviewersOp create(
-        Set<Account.Id> accountIds, Collection<Address> addresses, ReviewerState state);
+        Set<Account.Id> accountIds,
+        Collection<Address> addresses,
+        ReviewerState state,
+        boolean forGroup);
   }
 
   @AutoValue
@@ -107,6 +111,7 @@
   private final Set<Account.Id> accountIds;
   private final Collection<Address> addresses;
   private final ReviewerState state;
+  private final boolean forGroup;
 
   // Unlike addedCCs, addedReviewers is a PatchSetApproval because the AddReviewerResult returned
   // via the REST API is supposed to include vote information.
@@ -130,7 +135,8 @@
       AddReviewersEmail addReviewersEmail,
       @Assisted Set<Account.Id> accountIds,
       @Assisted Collection<Address> addresses,
-      @Assisted ReviewerState state) {
+      @Assisted ReviewerState state,
+      @Assisted boolean forGroup) {
     checkArgument(state == REVIEWER || state == CC, "must be %s or %s: %s", REVIEWER, CC, state);
     this.approvalsUtil = approvalsUtil;
     this.psUtil = psUtil;
@@ -142,6 +148,7 @@
     this.accountIds = accountIds;
     this.addresses = addresses;
     this.state = state;
+    this.forGroup = forGroup;
   }
 
   // TODO(dborowitz): This mutable setter is ugly, but a) it's less ugly than adding boolean args
@@ -162,7 +169,7 @@
       if (state == CC) {
         addedCCs =
             approvalsUtil.addCcs(
-                ctx.getNotes(), ctx.getUpdate(change.currentPatchSetId()), accountIds);
+                ctx.getNotes(), ctx.getUpdate(change.currentPatchSetId()), accountIds, forGroup);
       } else {
         addedReviewers =
             approvalsUtil.addReviewers(
@@ -174,12 +181,11 @@
       }
     }
 
-    ImmutableList<Address> addressesToAdd = ImmutableList.of();
     ReviewerStateInternal internalState = ReviewerStateInternal.fromReviewerState(state);
 
     // TODO(dborowitz): This behavior should live in ApprovalsUtil or something, like addCcs does.
     ImmutableSet<Address> existing = ctx.getNotes().getReviewersByEmail().byState(internalState);
-    addressesToAdd =
+    ImmutableList<Address> addressesToAdd =
         addresses.stream().filter(a -> !existing.contains(a)).collect(toImmutableList());
 
     if (state == CC) {
diff --git a/java/com/google/gerrit/server/change/ReviewerAdder.java b/java/com/google/gerrit/server/change/ReviewerAdder.java
index 1c4f63c..9e76888 100644
--- a/java/com/google/gerrit/server/change/ReviewerAdder.java
+++ b/java/com/google/gerrit/server/change/ReviewerAdder.java
@@ -237,7 +237,8 @@
         revision.getUser(),
         ImmutableSet.of(user.getAccountId()),
         null,
-        true);
+        true,
+        false);
   }
 
   @Nullable
@@ -260,7 +261,13 @@
 
     if (isValidReviewer(notes.getChange().getDest(), reviewerUser.getAccount())) {
       return new ReviewerAddition(
-          input, notes, user, ImmutableSet.of(reviewerUser.getAccountId()), null, exactMatchFound);
+          input,
+          notes,
+          user,
+          ImmutableSet.of(reviewerUser.getAccountId()),
+          null,
+          exactMatchFound,
+          false);
     }
     return fail(
         input,
@@ -344,7 +351,7 @@
       }
     }
 
-    return new ReviewerAddition(input, notes, user, reviewers, null, true);
+    return new ReviewerAddition(input, notes, user, reviewers, null, true, true);
   }
 
   @Nullable
@@ -366,7 +373,7 @@
           FailureType.NOT_FOUND,
           MessageFormat.format(ChangeMessages.get().reviewerInvalid, input.reviewer));
     }
-    return new ReviewerAddition(input, notes, user, null, ImmutableList.of(adr), true);
+    return new ReviewerAddition(input, notes, user, null, ImmutableList.of(adr), true, false);
   }
 
   private boolean isValidReviewer(BranchNameKey branch, Account member)
@@ -421,7 +428,8 @@
         CurrentUser caller,
         @Nullable Iterable<Account.Id> reviewers,
         @Nullable Iterable<Address> reviewersByEmail,
-        boolean exactMatchFound) {
+        boolean exactMatchFound,
+        boolean forGroup) {
       checkArgument(
           reviewers != null || reviewersByEmail != null,
           "must have either reviewers or reviewersByEmail");
@@ -435,7 +443,7 @@
       this.reviewersByEmail =
           reviewersByEmail == null ? ImmutableSet.of() : ImmutableSet.copyOf(reviewersByEmail);
       this.caller = caller.asIdentifiedUser();
-      op = addReviewersOpFactory.create(this.reviewers, this.reviewersByEmail, state());
+      op = addReviewersOpFactory.create(this.reviewers, this.reviewersByEmail, state(), forGroup);
       this.exactMatchFound = exactMatchFound;
     }
 
diff --git a/java/com/google/gerrit/server/edit/ChangeEditModifier.java b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
index fcd38c3..661e376 100644
--- a/java/com/google/gerrit/server/edit/ChangeEditModifier.java
+++ b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
@@ -371,7 +371,7 @@
     if (optionalChangeEdit.isPresent()) {
       ChangeEdit changeEdit = optionalChangeEdit.get();
       newTreeId = merge(repository, changeEdit, newTreeId);
-      if (ObjectId.equals(newTreeId, changeEdit.getEditCommit().getTree())) {
+      if (ObjectId.isEqual(newTreeId, changeEdit.getEditCommit().getTree())) {
         // Modifications are already contained in the change edit.
         return changeEdit;
       }
@@ -474,7 +474,7 @@
     treeCreator.addTreeModifications(treeModifications);
     ObjectId newTreeId = treeCreator.createNewTreeAndGetId(repository);
 
-    if (ObjectId.equals(newTreeId, baseCommit.getTree())) {
+    if (ObjectId.isEqual(newTreeId, baseCommit.getTree())) {
       throw new InvalidChangeOperationException("no changes were made");
     }
     return newTreeId;
diff --git a/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java b/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java
index d91e2e8..0adacd8 100644
--- a/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java
+++ b/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.extensions.restapi.RawInput;
 import java.io.IOException;
 import java.io.InputStream;
+import java.time.Instant;
 import java.util.Collections;
 import java.util.List;
 import org.eclipse.jgit.dircache.DirCacheEditor;
@@ -79,7 +80,7 @@
       try {
         if (dirCacheEntry.getFileMode() == FileMode.GITLINK) {
           dirCacheEntry.setLength(0);
-          dirCacheEntry.setLastModified(0);
+          dirCacheEntry.setLastModified(Instant.EPOCH);
           ObjectId newObjectId = ObjectId.fromString(getNewContentBytes(), 0);
           dirCacheEntry.setObjectId(newObjectId);
         } else {
diff --git a/java/com/google/gerrit/server/git/SystemReaderInstaller.java b/java/com/google/gerrit/server/git/SystemReaderInstaller.java
index 1043210..b6cc108 100644
--- a/java/com/google/gerrit/server/git/SystemReaderInstaller.java
+++ b/java/com/google/gerrit/server/git/SystemReaderInstaller.java
@@ -17,6 +17,7 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.util.git.DelegateSystemReader;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import org.eclipse.jgit.lib.Config;
@@ -47,41 +48,11 @@
   private SystemReader customReader() {
     SystemReader current = SystemReader.getInstance();
 
-    return new SystemReader() {
-      @Override
-      public String getHostname() {
-        return current.getHostname();
-      }
-
-      @Override
-      public String getenv(String variable) {
-        return current.getenv(variable);
-      }
-
-      @Override
-      public String getProperty(String key) {
-        return current.getProperty(key);
-      }
-
-      @Override
-      public FileBasedConfig openUserConfig(Config parent, FS fs) {
-        return current.openUserConfig(parent, fs);
-      }
-
+    return new DelegateSystemReader(current) {
       @Override
       public FileBasedConfig openSystemConfig(Config parent, FS fs) {
         return new FileBasedConfig(parent, site.jgit_config.toFile(), FS.DETECTED);
       }
-
-      @Override
-      public long getCurrentTime() {
-        return current.getCurrentTime();
-      }
-
-      @Override
-      public int getTimezone(long when) {
-        return current.getTimezone(when);
-      }
     };
   }
 }
diff --git a/java/com/google/gerrit/server/git/receive/BUILD b/java/com/google/gerrit/server/git/receive/BUILD
index a4f4d93..3f7d864 100644
--- a/java/com/google/gerrit/server/git/receive/BUILD
+++ b/java/com/google/gerrit/server/git/receive/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "receive",
     srcs = glob(["**/*.java"]),
diff --git a/java/com/google/gerrit/server/group/db/testing/BUILD b/java/com/google/gerrit/server/group/db/testing/BUILD
index c13abba..b5d5a43 100644
--- a/java/com/google/gerrit/server/group/db/testing/BUILD
+++ b/java/com/google/gerrit/server/group/db/testing/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 package(default_visibility = ["//visibility:public"])
 
 java_library(
diff --git a/java/com/google/gerrit/server/group/testing/BUILD b/java/com/google/gerrit/server/group/testing/BUILD
index 3ef712c..9b6d8de 100644
--- a/java/com/google/gerrit/server/group/testing/BUILD
+++ b/java/com/google/gerrit/server/group/testing/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 package(default_visibility = ["//visibility:public"])
 
 java_library(
diff --git a/java/com/google/gerrit/server/ioutil/BUILD b/java/com/google/gerrit/server/ioutil/BUILD
index ea91929..ed58d5b 100644
--- a/java/com/google/gerrit/server/ioutil/BUILD
+++ b/java/com/google/gerrit/server/ioutil/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "ioutil",
     srcs = glob(["**/*.java"]),
diff --git a/java/com/google/gerrit/server/logging/BUILD b/java/com/google/gerrit/server/logging/BUILD
index f78ff5f..c214d69 100644
--- a/java/com/google/gerrit/server/logging/BUILD
+++ b/java/com/google/gerrit/server/logging/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "logging",
     srcs = glob(
diff --git a/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java b/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
index 9c8bc1b..9cc1c84 100644
--- a/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
@@ -153,6 +153,7 @@
     return self();
   }
 
+  @Nullable
   protected ObjectId readRef(Repository repo) throws IOException {
     Ref ref = repo.getRefDatabase().exactRef(getRefName());
     return ref != null ? ref.getObjectId() : null;
diff --git a/java/com/google/gerrit/server/patch/PatchListLoader.java b/java/com/google/gerrit/server/patch/PatchListLoader.java
index 08de537..b639f96 100644
--- a/java/com/google/gerrit/server/patch/PatchListLoader.java
+++ b/java/com/google/gerrit/server/patch/PatchListLoader.java
@@ -314,12 +314,12 @@
   }
 
   private static boolean areParentChild(RevCommit commitA, RevCommit commitB) {
-    return ObjectId.equals(commitA.getParent(0), commitB)
-        || ObjectId.equals(commitB.getParent(0), commitA);
+    return ObjectId.isEqual(commitA.getParent(0), commitB)
+        || ObjectId.isEqual(commitB.getParent(0), commitA);
   }
 
   private static boolean haveCommonParent(RevCommit commitA, RevCommit commitB) {
-    return ObjectId.equals(commitA.getParent(0), commitB.getParent(0));
+    return ObjectId.isEqual(commitA.getParent(0), commitB.getParent(0));
   }
 
   private static Set<String> getTouchedFilePaths(PatchListEntry patchListEntry) {
diff --git a/java/com/google/gerrit/server/project/testing/BUILD b/java/com/google/gerrit/server/project/testing/BUILD
index f221e00..968e3da 100644
--- a/java/com/google/gerrit/server/project/testing/BUILD
+++ b/java/com/google/gerrit/server/project/testing/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "project-test-util",
     testonly = True,
diff --git a/java/com/google/gerrit/server/restapi/BUILD b/java/com/google/gerrit/server/restapi/BUILD
index f2d6e0f..9a2d11b 100644
--- a/java/com/google/gerrit/server/restapi/BUILD
+++ b/java/com/google/gerrit/server/restapi/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 package(
     default_visibility = ["//visibility:public"],
 )
diff --git a/java/com/google/gerrit/server/schema/BUILD b/java/com/google/gerrit/server/schema/BUILD
index aa552ed..fe07791 100644
--- a/java/com/google/gerrit/server/schema/BUILD
+++ b/java/com/google/gerrit/server/schema/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "schema",
     srcs = glob(
diff --git a/java/com/google/gerrit/server/schema/testing/BUILD b/java/com/google/gerrit/server/schema/testing/BUILD
index c520f43..d641c47 100644
--- a/java/com/google/gerrit/server/schema/testing/BUILD
+++ b/java/com/google/gerrit/server/schema/testing/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 package(default_visibility = ["//visibility:public"])
 
 java_library(
diff --git a/java/com/google/gerrit/server/securestore/testing/BUILD b/java/com/google/gerrit/server/securestore/testing/BUILD
index 9b76b9e..c2582b9 100644
--- a/java/com/google/gerrit/server/securestore/testing/BUILD
+++ b/java/com/google/gerrit/server/securestore/testing/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 package(default_testonly = True)
 
 java_library(
diff --git a/java/com/google/gerrit/server/util/git/BUILD b/java/com/google/gerrit/server/util/git/BUILD
index 81ca9cd..a8ae918 100644
--- a/java/com/google/gerrit/server/util/git/BUILD
+++ b/java/com/google/gerrit/server/util/git/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "git",
     srcs = glob(["**/*.java"]),
diff --git a/java/com/google/gerrit/server/util/git/DelegateSystemReader.java b/java/com/google/gerrit/server/util/git/DelegateSystemReader.java
new file mode 100644
index 0000000..67e8dec
--- /dev/null
+++ b/java/com/google/gerrit/server/util/git/DelegateSystemReader.java
@@ -0,0 +1,63 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.util.git;
+
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.SystemReader;
+
+public class DelegateSystemReader extends SystemReader {
+  private final SystemReader delegate;
+
+  public DelegateSystemReader(SystemReader delegate) {
+    this.delegate = delegate;
+  }
+
+  @Override
+  public String getHostname() {
+    return delegate.getHostname();
+  }
+
+  @Override
+  public String getenv(String variable) {
+    return delegate.getenv(variable);
+  }
+
+  @Override
+  public String getProperty(String key) {
+    return delegate.getProperty(key);
+  }
+
+  @Override
+  public FileBasedConfig openUserConfig(Config parent, FS fs) {
+    return delegate.openUserConfig(parent, fs);
+  }
+
+  @Override
+  public FileBasedConfig openSystemConfig(Config parent, FS fs) {
+    return delegate.openSystemConfig(parent, fs);
+  }
+
+  @Override
+  public long getCurrentTime() {
+    return delegate.getCurrentTime();
+  }
+
+  @Override
+  public int getTimezone(long when) {
+    return delegate.getTimezone(when);
+  }
+}
diff --git a/java/com/google/gerrit/server/util/time/BUILD b/java/com/google/gerrit/server/util/time/BUILD
index c7cd89e..d00b42d 100644
--- a/java/com/google/gerrit/server/util/time/BUILD
+++ b/java/com/google/gerrit/server/util/time/BUILD
@@ -1,9 +1,12 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "time",
     srcs = glob(["**/*.java"]),
     visibility = ["//visibility:public"],
     deps = [
         "//java/com/google/gerrit/common:annotations",
+        "//java/com/google/gerrit/server/util/git",
         "//lib:guava",
         "//lib/jgit/org.eclipse.jgit:jgit",
     ],
diff --git a/java/com/google/gerrit/server/util/time/TimeUtil.java b/java/com/google/gerrit/server/util/time/TimeUtil.java
index b9d2d8a..639d0a6 100644
--- a/java/com/google/gerrit/server/util/time/TimeUtil.java
+++ b/java/com/google/gerrit/server/util/time/TimeUtil.java
@@ -17,12 +17,10 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.gerrit.common.UsedAt;
 import com.google.gerrit.common.UsedAt.Project;
+import com.google.gerrit.server.util.git.DelegateSystemReader;
 import java.sql.Timestamp;
 import java.time.Instant;
 import java.util.function.LongSupplier;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
-import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.SystemReader;
 
 /** Static utility methods for dealing with dates and times. */
@@ -76,47 +74,15 @@
     SystemReader.setInstance(null);
   }
 
-  private static class GerritSystemReader extends SystemReader {
-    SystemReader delegate;
-
-    GerritSystemReader(SystemReader delegate) {
-      this.delegate = delegate;
-    }
-
-    @Override
-    public String getHostname() {
-      return delegate.getHostname();
-    }
-
-    @Override
-    public String getenv(String variable) {
-      return delegate.getenv(variable);
-    }
-
-    @Override
-    public String getProperty(String key) {
-      return delegate.getProperty(key);
-    }
-
-    @Override
-    public FileBasedConfig openUserConfig(Config parent, FS fs) {
-      return delegate.openUserConfig(parent, fs);
-    }
-
-    @Override
-    public FileBasedConfig openSystemConfig(Config parent, FS fs) {
-      return delegate.openSystemConfig(parent, fs);
+  static class GerritSystemReader extends DelegateSystemReader {
+    GerritSystemReader(SystemReader reader) {
+      super(reader);
     }
 
     @Override
     public long getCurrentTime() {
       return currentMillisSupplier.getAsLong();
     }
-
-    @Override
-    public int getTimezone(long when) {
-      return delegate.getTimezone(when);
-    }
   }
 
   private TimeUtil() {}
diff --git a/java/com/google/gerrit/sshd/BUILD b/java/com/google/gerrit/sshd/BUILD
index 3a69554..33bdf69 100644
--- a/java/com/google/gerrit/sshd/BUILD
+++ b/java/com/google/gerrit/sshd/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "sshd",
     srcs = glob(["**/*.java"]),
diff --git a/java/com/google/gerrit/sshd/commands/FlushCaches.java b/java/com/google/gerrit/sshd/commands/FlushCaches.java
index df56cf4..98562b0 100644
--- a/java/com/google/gerrit/sshd/commands/FlushCaches.java
+++ b/java/com/google/gerrit/sshd/commands/FlushCaches.java
@@ -89,7 +89,8 @@
   @SuppressWarnings("unchecked")
   private void doList() {
     for (String name :
-        (List<String>) listCaches.setFormat(OutputFormat.LIST).apply(new ConfigResource())) {
+        (List<String>)
+            listCaches.setFormat(OutputFormat.LIST).apply(new ConfigResource()).value()) {
       stderr.print(name);
       stderr.print('\n');
     }
diff --git a/java/com/google/gerrit/sshd/commands/ShowCaches.java b/java/com/google/gerrit/sshd/commands/ShowCaches.java
index c19e790..3c617b0 100644
--- a/java/com/google/gerrit/sshd/commands/ShowCaches.java
+++ b/java/com/google/gerrit/sshd/commands/ShowCaches.java
@@ -194,7 +194,8 @@
 
   private Collection<CacheInfo> getCaches() {
     @SuppressWarnings("unchecked")
-    Map<String, CacheInfo> caches = (Map<String, CacheInfo>) listCaches.apply(new ConfigResource());
+    Map<String, CacheInfo> caches =
+        (Map<String, CacheInfo>) listCaches.apply(new ConfigResource()).value();
     for (Map.Entry<String, CacheInfo> entry : caches.entrySet()) {
       CacheInfo cache = entry.getValue();
       cache.name = entry.getKey();
diff --git a/java/com/google/gerrit/sshd/commands/UploadArchive.java b/java/com/google/gerrit/sshd/commands/UploadArchive.java
index a58e472..c25a1a8 100644
--- a/java/com/google/gerrit/sshd/commands/UploadArchive.java
+++ b/java/com/google/gerrit/sshd/commands/UploadArchive.java
@@ -139,7 +139,7 @@
     PacketLineIn packetIn = new PacketLineIn(in);
     for (; ; ) {
       String s = packetIn.readString();
-      if (isPacketLineEnd(s)) {
+      if (PacketLineIn.isEnd(s)) {
         break;
       }
       if (!s.startsWith(argCmd)) {
@@ -163,12 +163,6 @@
     }
   }
 
-  // JGit API depends on reference equality with sentinel.
-  @SuppressWarnings({"ReferenceEquality", "StringEquality"})
-  private static boolean isPacketLineEnd(String s) {
-    return s == PacketLineIn.END;
-  }
-
   @Override
   protected void runImpl() throws IOException, PermissionBackendException, Failure {
     PacketLineOut packetOut = new PacketLineOut(out);
diff --git a/java/com/google/gerrit/testing/BUILD b/java/com/google/gerrit/testing/BUILD
index 27065aa..f5298ea 100644
--- a/java/com/google/gerrit/testing/BUILD
+++ b/java/com/google/gerrit/testing/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "gerrit-test-util",
     testonly = True,
diff --git a/java/com/google/gerrit/truth/BUILD b/java/com/google/gerrit/truth/BUILD
index 4727da1..7c0e743 100644
--- a/java/com/google/gerrit/truth/BUILD
+++ b/java/com/google/gerrit/truth/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "truth",
     testonly = True,
diff --git a/java/com/google/gerrit/util/cli/BUILD b/java/com/google/gerrit/util/cli/BUILD
index b9b9bba..e4f2c21 100644
--- a/java/com/google/gerrit/util/cli/BUILD
+++ b/java/com/google/gerrit/util/cli/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "cli",
     srcs = glob(["**/*.java"]),
diff --git a/java/com/google/gerrit/util/http/BUILD b/java/com/google/gerrit/util/http/BUILD
index 30d3adc..5ecb7a1 100644
--- a/java/com/google/gerrit/util/http/BUILD
+++ b/java/com/google/gerrit/util/http/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "http",
     srcs = glob(["**/*.java"]),
diff --git a/java/com/google/gerrit/util/ssl/BUILD b/java/com/google/gerrit/util/ssl/BUILD
index 4f65b61..e0641c7 100644
--- a/java/com/google/gerrit/util/ssl/BUILD
+++ b/java/com/google/gerrit/util/ssl/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "ssl",
     srcs = glob(["**/*.java"]),
diff --git a/java/gerrit/BUILD b/java/gerrit/BUILD
index f416f11..d7e2306 100644
--- a/java/gerrit/BUILD
+++ b/java/gerrit/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "prolog-predicates",
     srcs = glob(["**/*.java"]),
diff --git a/java/org/apache/commons/net/BUILD b/java/org/apache/commons/net/BUILD
index 4951933..c322ecd 100644
--- a/java/org/apache/commons/net/BUILD
+++ b/java/org/apache/commons/net/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "net",
     srcs = glob(["**/*.java"]),
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index b04fb33..9fefbe9 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -158,6 +158,7 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.ChangeMessagesUtil;
+import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.change.ChangeETagComputation;
 import com.google.gerrit.server.change.ChangeResource;
@@ -453,7 +454,7 @@
   }
 
   @Test
-  public void pendingReviewersInNoteDb() throws Exception {
+  public void pendingReviewers() throws Exception {
     ConfigInput conf = new ConfigInput();
     conf.enableReviewerByEmail = InheritableBoolean.TRUE;
     gApi.projects().name(project.get()).config(conf);
@@ -869,6 +870,18 @@
   }
 
   @Test
+  public void cantRevertNonMergedCommit() throws Exception {
+    PushOneCommit.Result result = createChange();
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () -> gApi.changes().id(result.getChangeId()).revert());
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains("change is " + ChangeUtil.status(result.getChange().change()));
+  }
+
+  @Test
   public void cantCreateRevertWithoutProjectWritePermission() throws Exception {
     PushOneCommit.Result r = createChange();
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
@@ -1905,10 +1918,7 @@
     assertMailReplyTo(m, admin.email());
     ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
 
-    // When NoteDb is enabled adding a reviewer records that user as reviewer
-    // in NoteDb. When NoteDb is disabled adding a reviewer results in a dummy 0
-    // approval on the change which is treated as CC when the ChangeInfo is
-    // created.
+    // Adding a reviewer records that user as reviewer.
     Collection<AccountInfo> reviewers = c.reviewers.get(REVIEWER);
     assertThat(reviewers).isNotNull();
     assertThat(reviewers).hasSize(1);
@@ -2020,10 +2030,7 @@
     assertMailReplyTo(m, email);
     ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
 
-    // When NoteDb is enabled adding a reviewer records that user as reviewer
-    // in NoteDb. When NoteDb is disabled adding a reviewer results in a dummy 0
-    // approval on the change which is treated as CC when the ChangeInfo is
-    // created.
+    // Adding a reviewer records that user as reviewer.
     Collection<AccountInfo> reviewers = c.reviewers.get(REVIEWER);
     assertThat(reviewers).isNotNull();
     assertThat(reviewers).hasSize(1);
@@ -2084,10 +2091,7 @@
     assertMailReplyTo(m, myGroupUserEmail);
     ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
 
-    // When NoteDb is enabled adding a reviewer records that user as reviewer
-    // in NoteDb. When NoteDb is disabled adding a reviewer results in a dummy 0
-    // approval on the change which is treated as CC when the ChangeInfo is
-    // created.
+    // Adding a reviewer records that user as reviewer.
     Collection<AccountInfo> reviewers = c.reviewers.get(REVIEWER);
     assertThat(reviewers).isNotNull();
     assertThat(reviewers).hasSize(1);
@@ -2115,10 +2119,7 @@
     // There should be no email notification when adding self
     assertThat(sender.getMessages()).isEmpty();
 
-    // When NoteDb is enabled adding a reviewer records that user as reviewer
-    // in NoteDb. When NoteDb is disabled adding a reviewer results in a dummy 0
-    // approval on the change which is treated as CC when the ChangeInfo is
-    // created.
+    // Adding a reviewer records that user as reviewer.
     ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
     Collection<AccountInfo> reviewers = c.reviewers.get(REVIEWER);
     assertThat(reviewers).isNotNull();
@@ -2174,7 +2175,7 @@
         .containsExactly(user.id().get());
 
     // Further test: remove the vote, then comment again. The user should be
-    // implicitly re-added to the ReviewerSet, as a CC if we're using NoteDb.
+    // implicitly re-added to the ReviewerSet, as a CC.
     requestScopeOperations.setApiUser(admin.id());
     gApi.changes().id(r.getChangeId()).reviewer(user.id().toString()).remove();
     c = gApi.changes().id(r.getChangeId()).get();
@@ -3164,7 +3165,7 @@
   }
 
   @Test
-  public void noteDbCommitsOnPatchSetCreation() throws Exception {
+  public void commitsOnPatchSetCreation() throws Exception {
     PushOneCommit.Result r = createChange();
     pushFactory
         .create(admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "4711", r.getChangeId())
@@ -4353,10 +4354,7 @@
     public boolean updateChange(ChangeContext ctx) throws Exception {
       Change change = ctx.getChange();
 
-      // Change status in database.
-      change.setStatus(newStatus);
-
-      // Change status in NoteDb.
+      // Change status.
       PatchSet.Id currentPatchSetId = change.currentPatchSetId();
       ctx.getUpdate(currentPatchSetId).setStatus(newStatus);
 
diff --git a/javatests/com/google/gerrit/acceptance/api/group/BUILD b/javatests/com/google/gerrit/acceptance/api/group/BUILD
index a12342a..e311e25 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/BUILD
+++ b/javatests/com/google/gerrit/acceptance/api/group/BUILD
@@ -1,3 +1,4 @@
+load("@rules_java//java:defs.bzl", "java_library")
 load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
 
 acceptance_tests(
diff --git a/javatests/com/google/gerrit/acceptance/git/BUILD b/javatests/com/google/gerrit/acceptance/git/BUILD
index 0de307a..dfaf9e3 100644
--- a/javatests/com/google/gerrit/acceptance/git/BUILD
+++ b/javatests/com/google/gerrit/acceptance/git/BUILD
@@ -1,3 +1,4 @@
+load("@rules_java//java:defs.bzl", "java_library")
 load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
 
 [acceptance_tests(
diff --git a/javatests/com/google/gerrit/acceptance/pgm/BUILD b/javatests/com/google/gerrit/acceptance/pgm/BUILD
index e0ed78a..d15c6ce 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/BUILD
+++ b/javatests/com/google/gerrit/acceptance/pgm/BUILD
@@ -1,3 +1,4 @@
+load("@rules_java//java:defs.bzl", "java_library")
 load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
 
 acceptance_tests(
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/BUILD b/javatests/com/google/gerrit/acceptance/rest/account/BUILD
index 17a6053..e801dcc 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/BUILD
+++ b/javatests/com/google/gerrit/acceptance/rest/account/BUILD
@@ -1,3 +1,4 @@
+load("@rules_java//java:defs.bzl", "java_library")
 load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
 
 acceptance_tests(
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/BUILD b/javatests/com/google/gerrit/acceptance/rest/change/BUILD
index 9a65378..7ccf10f 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/BUILD
+++ b/javatests/com/google/gerrit/acceptance/rest/change/BUILD
@@ -1,3 +1,4 @@
+load("@rules_java//java:defs.bzl", "java_library")
 load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
 
 SUBMIT_UTIL_SRCS = glob(["AbstractSubmit*.java"])
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
index e300c91..96b96eb 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
 import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
 import static com.google.gerrit.extensions.client.ReviewerState.CC;
 import static com.google.gerrit.extensions.client.ReviewerState.REMOVED;
@@ -390,8 +391,7 @@
     assertThat(result.reviewers).isNotNull();
     assertThat(result.reviewers).hasSize(2);
 
-    // Verify reviewer and CC were added. If not in NoteDb read mode, both
-    // parties will be returned as CCed.
+    // Verify reviewer and CC were added.
     ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
     assertReviewers(c, REVIEWER, admin, user);
     assertReviewers(c, CC, observer);
@@ -492,7 +492,7 @@
   }
 
   @Test
-  public void noteDbAddReviewerToReviewerChangeInfo() throws Exception {
+  public void addReviewerToReviewerChangeInfo() throws Exception {
     PushOneCommit.Result r = createChange();
     String changeId = r.getChangeId();
     AddReviewerInput in = new AddReviewerInput();
@@ -506,7 +506,7 @@
     gApi.changes().id(changeId).current().review(ReviewInput.dislike());
 
     requestScopeOperations.setApiUser(user.id());
-    // NoteDb adds reviewer to a change on every review.
+    // By posting a review the user is added as reviewer.
     gApi.changes().id(changeId).current().review(ReviewInput.dislike());
 
     deleteReviewer(changeId, user).assertNoContent();
@@ -756,6 +756,81 @@
     assertThat(gApi.changes().id(r.getChangeId()).addReviewer(input).ccs).isEmpty();
   }
 
+  @Test
+  public void moveCcToReviewer() throws Exception {
+    // Create a change and add 'user' as CC.
+    String changeId = createChange().getChangeId();
+    AddReviewerInput reviewerInput = new AddReviewerInput();
+    reviewerInput.reviewer = user.email();
+    reviewerInput.state = ReviewerState.CC;
+    gApi.changes().id(changeId).addReviewer(reviewerInput);
+
+    // Verify that 'user' is a CC on the change and that there are no reviewers.
+    ChangeInfo c = gApi.changes().id(changeId).get();
+    Collection<AccountInfo> ccs = c.reviewers.get(CC);
+    assertThat(ccs).isNotNull();
+    assertThat(ccs).hasSize(1);
+    assertThat(ccs.iterator().next()._accountId).isEqualTo(user.id().get());
+    assertThat(c.reviewers.get(REVIEWER)).isNull();
+
+    // Move 'user' from CC to reviewer.
+    gApi.changes().id(changeId).addReviewer(user.id().toString());
+
+    // Verify that 'user' is a reviewer on the change now and that there are no CCs.
+    c = gApi.changes().id(changeId).get();
+    Collection<AccountInfo> reviewers = c.reviewers.get(REVIEWER);
+    assertThat(reviewers).isNotNull();
+    assertThat(reviewers).hasSize(1);
+    assertThat(reviewers.iterator().next()._accountId).isEqualTo(user.id().get());
+    assertThat(c.reviewers.get(CC)).isNull();
+  }
+
+  @Test
+  public void moveReviewerToCc() throws Exception {
+    // Allow everyone to approve changes.
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(REGISTERED_USERS).range(-2, 2))
+        .update();
+
+    // Create a change and add 'user' as reviewer.
+    String changeId = createChange().getChangeId();
+    gApi.changes().id(changeId).addReviewer(user.id().toString());
+
+    // Verify that 'user' is a reviewer on the change and that there are no CCs.
+    ChangeInfo c = gApi.changes().id(changeId).get();
+    Collection<AccountInfo> reviewers = c.reviewers.get(REVIEWER);
+    assertThat(reviewers).isNotNull();
+    assertThat(reviewers).hasSize(1);
+    assertThat(reviewers.iterator().next()._accountId).isEqualTo(user.id().get());
+    assertThat(c.reviewers.get(CC)).isNull();
+
+    // Let 'user' approve the change and verify that the change has the approval.
+    requestScopeOperations.setApiUser(user.id());
+    approve(changeId);
+    c = gApi.changes().id(changeId).get();
+    assertThat(c.labels.get("Code-Review").approved._accountId).isEqualTo(user.id().get());
+
+    // Move 'user' from reviewer to CC.
+    requestScopeOperations.setApiUser(admin.id());
+    AddReviewerInput reviewerInput = new AddReviewerInput();
+    reviewerInput.reviewer = user.id().toString();
+    reviewerInput.state = CC;
+    gApi.changes().id(changeId).addReviewer(reviewerInput);
+
+    // Verify that 'user' is a CC on the change now and that there are no reviewers.
+    c = gApi.changes().id(changeId).get();
+    Collection<AccountInfo> ccs = c.reviewers.get(CC);
+    assertThat(ccs).isNotNull();
+    assertThat(ccs).hasSize(1);
+    assertThat(ccs.iterator().next()._accountId).isEqualTo(user.id().get());
+    assertThat(c.reviewers.get(REVIEWER)).isNull();
+
+    // Verify that the approval of 'user' is still there.
+    assertThat(c.labels.get("Code-Review").approved._accountId).isEqualTo(user.id().get());
+  }
+
   private void assertThatUserIsOnlyReviewer(String changeId) throws Exception {
     AccountInfo userInfo = new AccountInfo(user.fullName(), user.getEmailAddress().getEmail());
     userInfo._accountId = user.id().get();
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/BUILD b/javatests/com/google/gerrit/acceptance/rest/project/BUILD
index 131c24a..200b26a 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/BUILD
+++ b/javatests/com/google/gerrit/acceptance/rest/project/BUILD
@@ -1,3 +1,4 @@
+load("@rules_java//java:defs.bzl", "java_library")
 load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
 
 acceptance_tests(
diff --git a/javatests/com/google/gerrit/acceptance/rest/util/BUILD b/javatests/com/google/gerrit/acceptance/rest/util/BUILD
index cc72e8a..1d3fe65 100644
--- a/javatests/com/google/gerrit/acceptance/rest/util/BUILD
+++ b/javatests/com/google/gerrit/acceptance/rest/util/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "util",
     testonly = True,
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/BUILD b/javatests/com/google/gerrit/acceptance/server/mail/BUILD
index e21789b..5d7e65e 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/BUILD
+++ b/javatests/com/google/gerrit/acceptance/server/mail/BUILD
@@ -1,3 +1,4 @@
+load("@rules_java//java:defs.bzl", "java_library")
 load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
 
 DEPS = [
diff --git a/javatests/com/google/gerrit/acceptance/ssh/BUILD b/javatests/com/google/gerrit/acceptance/ssh/BUILD
index 00a0914..5634322 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/BUILD
+++ b/javatests/com/google/gerrit/acceptance/ssh/BUILD
@@ -1,3 +1,4 @@
+load("@rules_java//java:defs.bzl", "java_library")
 load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
 
 java_library(
diff --git a/javatests/com/google/gerrit/elasticsearch/BUILD b/javatests/com/google/gerrit/elasticsearch/BUILD
index a2bd092..e50f2b5 100644
--- a/javatests/com/google/gerrit/elasticsearch/BUILD
+++ b/javatests/com/google/gerrit/elasticsearch/BUILD
@@ -1,3 +1,4 @@
+load("@rules_java//java:defs.bzl", "java_library")
 load("//tools/bzl:junit.bzl", "junit_tests")
 
 java_library(
diff --git a/javatests/com/google/gerrit/server/BUILD b/javatests/com/google/gerrit/server/BUILD
index f6ed5ef..1bb22e4 100644
--- a/javatests/com/google/gerrit/server/BUILD
+++ b/javatests/com/google/gerrit/server/BUILD
@@ -1,3 +1,4 @@
+load("@rules_java//java:defs.bzl", "java_library")
 load("//tools/bzl:junit.bzl", "junit_tests")
 
 CUSTOM_TRUTH_SUBJECTS = glob([
diff --git a/javatests/com/google/gerrit/server/query/account/BUILD b/javatests/com/google/gerrit/server/query/account/BUILD
index e41d390..7b72f4e 100644
--- a/javatests/com/google/gerrit/server/query/account/BUILD
+++ b/javatests/com/google/gerrit/server/query/account/BUILD
@@ -1,3 +1,4 @@
+load("@rules_java//java:defs.bzl", "java_library")
 load("//tools/bzl:junit.bzl", "junit_tests")
 
 ABSTRACT_QUERY_TEST = ["AbstractQueryAccountsTest.java"]
diff --git a/javatests/com/google/gerrit/server/query/change/BUILD b/javatests/com/google/gerrit/server/query/change/BUILD
index a128593..67b7c47 100644
--- a/javatests/com/google/gerrit/server/query/change/BUILD
+++ b/javatests/com/google/gerrit/server/query/change/BUILD
@@ -1,3 +1,4 @@
+load("@rules_java//java:defs.bzl", "java_library")
 load("//tools/bzl:junit.bzl", "junit_tests")
 
 ABSTRACT_QUERY_TEST = ["AbstractQueryChangesTest.java"]
diff --git a/javatests/com/google/gerrit/server/query/group/BUILD b/javatests/com/google/gerrit/server/query/group/BUILD
index 3f147c9..1271f4e 100644
--- a/javatests/com/google/gerrit/server/query/group/BUILD
+++ b/javatests/com/google/gerrit/server/query/group/BUILD
@@ -1,3 +1,4 @@
+load("@rules_java//java:defs.bzl", "java_library")
 load("//tools/bzl:junit.bzl", "junit_tests")
 
 ABSTRACT_QUERY_TEST = ["AbstractQueryGroupsTest.java"]
diff --git a/javatests/com/google/gerrit/server/query/project/BUILD b/javatests/com/google/gerrit/server/query/project/BUILD
index 4ce1c00..e978be6 100644
--- a/javatests/com/google/gerrit/server/query/project/BUILD
+++ b/javatests/com/google/gerrit/server/query/project/BUILD
@@ -1,3 +1,4 @@
+load("@rules_java//java:defs.bzl", "java_library")
 load("//tools/bzl:junit.bzl", "junit_tests")
 
 ABSTRACT_QUERY_TEST = ["AbstractQueryProjectsTest.java"]
diff --git a/javatests/com/google/gerrit/util/http/testutil/BUILD b/javatests/com/google/gerrit/util/http/testutil/BUILD
index adae68e..5cb94c6 100644
--- a/javatests/com/google/gerrit/util/http/testutil/BUILD
+++ b/javatests/com/google/gerrit/util/http/testutil/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "testutil",
     testonly = True,
diff --git a/lib/BUILD b/lib/BUILD
index f98f6fe..ab2bad9 100644
--- a/lib/BUILD
+++ b/lib/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 exports_files(glob([
     "LICENSE-*",
 ]))
diff --git a/lib/antlr/BUILD b/lib/antlr/BUILD
index c35c2b5..076aea9 100644
--- a/lib/antlr/BUILD
+++ b/lib/antlr/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_binary", "java_library")
+
 package(default_visibility = ["//java/com/google/gerrit/index:__pkg__"])
 
 [java_library(
diff --git a/lib/asciidoctor/BUILD b/lib/asciidoctor/BUILD
index 62b1114..b46c08d 100644
--- a/lib/asciidoctor/BUILD
+++ b/lib/asciidoctor/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "asciidoctor",
     data = ["//lib:LICENSE-asciidoctor"],
diff --git a/lib/auto/BUILD b/lib/auto/BUILD
index 1e722bc..b60a101 100644
--- a/lib/auto/BUILD
+++ b/lib/auto/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library", "java_plugin")
+
 java_plugin(
     name = "auto-annotation-plugin",
     processor_class = "com.google.auto.value.processor.AutoAnnotationProcessor",
diff --git a/lib/bouncycastle/BUILD b/lib/bouncycastle/BUILD
index cf3e996..43ba6e1 100644
--- a/lib/bouncycastle/BUILD
+++ b/lib/bouncycastle/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "bcprov",
     data = ["//lib:LICENSE-bouncycastle"],
diff --git a/lib/commons/BUILD b/lib/commons/BUILD
index e8de396..38b1b6d 100644
--- a/lib/commons/BUILD
+++ b/lib/commons/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 package(default_visibility = ["//visibility:public"])
 
 java_library(
diff --git a/lib/dropwizard/BUILD b/lib/dropwizard/BUILD
index 4ae12f1..174b7ad 100644
--- a/lib/dropwizard/BUILD
+++ b/lib/dropwizard/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "dropwizard-core",
     data = ["//lib:LICENSE-Apache2.0"],
diff --git a/lib/easymock/BUILD b/lib/easymock/BUILD
index 352d2a7..90c9673 100644
--- a/lib/easymock/BUILD
+++ b/lib/easymock/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "easymock",
     data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"],
diff --git a/lib/elasticsearch-rest-client/BUILD b/lib/elasticsearch-rest-client/BUILD
index 8df3c70..e323263 100644
--- a/lib/elasticsearch-rest-client/BUILD
+++ b/lib/elasticsearch-rest-client/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 package(default_visibility = ["//visibility:public"])
 
 java_library(
diff --git a/lib/flogger/BUILD b/lib/flogger/BUILD
index c41e12f..35c3c62 100644
--- a/lib/flogger/BUILD
+++ b/lib/flogger/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "api",
     data = ["//lib:LICENSE-Apache2.0"],
diff --git a/lib/gitiles/BUILD b/lib/gitiles/BUILD
index b1bbca1..6e03801 100644
--- a/lib/gitiles/BUILD
+++ b/lib/gitiles/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "gitiles",
     visibility = ["//visibility:public"],
diff --git a/lib/greenmail/BUILD b/lib/greenmail/BUILD
index 9cbd0eb..e8845e2 100644
--- a/lib/greenmail/BUILD
+++ b/lib/greenmail/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 package(default_visibility = ["//visibility:public"])
 
 POST_JDK8_DEPS = [":javax-activation"]
diff --git a/lib/guice/BUILD b/lib/guice/BUILD
index 7f384e2..f73984b 100644
--- a/lib/guice/BUILD
+++ b/lib/guice/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "guice",
     data = ["//lib:LICENSE-Apache2.0"],
diff --git a/lib/httpcomponents/BUILD b/lib/httpcomponents/BUILD
index 03d9b68..07d4bb9 100644
--- a/lib/httpcomponents/BUILD
+++ b/lib/httpcomponents/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 package(default_visibility = ["//visibility:public"])
 
 java_library(
diff --git a/lib/jackson/BUILD b/lib/jackson/BUILD
index 0034748..3eed77a 100644
--- a/lib/jackson/BUILD
+++ b/lib/jackson/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "jackson-core",
     data = ["//lib:LICENSE-Apache2.0"],
diff --git a/lib/jetty/BUILD b/lib/jetty/BUILD
index b78ac58..6417385 100644
--- a/lib/jetty/BUILD
+++ b/lib/jetty/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "servlet",
     data = ["//lib:LICENSE-Apache2.0"],
diff --git a/lib/jgit/jgit.bzl b/lib/jgit/jgit.bzl
index 0f52913..b3d026d 100644
--- a/lib/jgit/jgit.bzl
+++ b/lib/jgit/jgit.bzl
@@ -1,6 +1,6 @@
 load("//tools/bzl:maven_jar.bzl", "MAVEN_CENTRAL", "maven_jar")
 
-_JGIT_VERS = "5.3.1.201904271842-r"
+_JGIT_VERS = "5.4.3.201909031940-r"
 
 _DOC_VERS = _JGIT_VERS  # Set to _JGIT_VERS unless using a snapshot
 
@@ -40,25 +40,25 @@
         name = "jgit-lib",
         artifact = "org.eclipse.jgit:org.eclipse.jgit:" + _JGIT_VERS,
         repository = _JGIT_REPO,
-        sha1 = "dba85014483315fa426259bc1b8ccda9373a624b",
+        sha1 = "10322c4e103485f8b4873cbbf982342f9c3d7989",
     )
     maven_jar(
         name = "jgit-servlet",
         artifact = "org.eclipse.jgit:org.eclipse.jgit.http.server:" + _JGIT_VERS,
         repository = _JGIT_REPO,
-        sha1 = "3287341fca859340a00b51cb5dd3b78b8e532b39",
+        sha1 = "59d0c943343f30612e4e2a5a3bf1b95b56e00207",
     )
     maven_jar(
         name = "jgit-archive",
         artifact = "org.eclipse.jgit:org.eclipse.jgit.archive:" + _JGIT_VERS,
         repository = _JGIT_REPO,
-        sha1 = "3585027e83fb44a5de2c10ae9ddbf976593bf080",
+        sha1 = "21dc4a10882dc667c83bf82a563a6fc4d7719456",
     )
     maven_jar(
         name = "jgit-junit",
         artifact = "org.eclipse.jgit:org.eclipse.jgit.junit:" + _JGIT_VERS,
         repository = _JGIT_REPO,
-        sha1 = "3d9ba7e610d6ab5d08dcb1e4ba448b592a34de77",
+        sha1 = "71659fc1a1729b7c67846dac8cd6a762fa72002a",
     )
 
 def jgit_dep(name):
diff --git a/lib/jgit/org.eclipse.jgit.archive/BUILD b/lib/jgit/org.eclipse.jgit.archive/BUILD
index 2742623..151cd71 100644
--- a/lib/jgit/org.eclipse.jgit.archive/BUILD
+++ b/lib/jgit/org.eclipse.jgit.archive/BUILD
@@ -1,3 +1,4 @@
+load("@rules_java//java:defs.bzl", "java_library")
 load("//lib/jgit:jgit.bzl", "jgit_dep")
 
 java_library(
diff --git a/lib/jgit/org.eclipse.jgit.http.server/BUILD b/lib/jgit/org.eclipse.jgit.http.server/BUILD
index 001ad8b..fd634a5 100644
--- a/lib/jgit/org.eclipse.jgit.http.server/BUILD
+++ b/lib/jgit/org.eclipse.jgit.http.server/BUILD
@@ -1,3 +1,4 @@
+load("@rules_java//java:defs.bzl", "java_library")
 load("//lib/jgit:jgit.bzl", "jgit_dep")
 
 java_library(
diff --git a/lib/jgit/org.eclipse.jgit.junit/BUILD b/lib/jgit/org.eclipse.jgit.junit/BUILD
index 29d80d3..abc522b 100644
--- a/lib/jgit/org.eclipse.jgit.junit/BUILD
+++ b/lib/jgit/org.eclipse.jgit.junit/BUILD
@@ -1,3 +1,4 @@
+load("@rules_java//java:defs.bzl", "java_library")
 load("//lib/jgit:jgit.bzl", "jgit_dep")
 
 java_library(
diff --git a/lib/jgit/org.eclipse.jgit/BUILD b/lib/jgit/org.eclipse.jgit/BUILD
index dc11171..c1f2607 100644
--- a/lib/jgit/org.eclipse.jgit/BUILD
+++ b/lib/jgit/org.eclipse.jgit/BUILD
@@ -1,3 +1,4 @@
+load("@rules_java//java:defs.bzl", "java_library")
 load("//lib/jgit:jgit.bzl", "jgit_dep")
 
 java_library(
diff --git a/lib/jsoup/BUILD b/lib/jsoup/BUILD
index 3142dac..7171901 100644
--- a/lib/jsoup/BUILD
+++ b/lib/jsoup/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "jsoup",
     data = ["//lib:LICENSE-jsoup"],
diff --git a/lib/log/BUILD b/lib/log/BUILD
index 8e4c927..128e8ba 100644
--- a/lib/log/BUILD
+++ b/lib/log/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "api",
     data = ["//lib:LICENSE-slf4j"],
diff --git a/lib/lucene/BUILD b/lib/lucene/BUILD
index adb5030..b8b2457 100644
--- a/lib/lucene/BUILD
+++ b/lib/lucene/BUILD
@@ -1,3 +1,4 @@
+load("@rules_java//java:defs.bzl", "java_library")
 load("//tools/bzl:maven.bzl", "merge_maven_jars")
 
 package(default_visibility = ["//visibility:public"])
diff --git a/lib/mail/BUILD b/lib/mail/BUILD
index eca2b6b..489f544 100644
--- a/lib/mail/BUILD
+++ b/lib/mail/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "mail",
     data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"],
diff --git a/lib/mime4j/BUILD b/lib/mime4j/BUILD
index ee407c3..577661d 100644
--- a/lib/mime4j/BUILD
+++ b/lib/mime4j/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "core",
     data = ["//lib:LICENSE-Apache2.0"],
diff --git a/lib/mina/BUILD b/lib/mina/BUILD
index 6ee7e41..5ad47cd 100644
--- a/lib/mina/BUILD
+++ b/lib/mina/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "sshd",
     data = ["//lib:LICENSE-Apache2.0"],
diff --git a/lib/mockito/BUILD b/lib/mockito/BUILD
index fa4839b..7af9669 100644
--- a/lib/mockito/BUILD
+++ b/lib/mockito/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 package(
     default_testonly = True,
     default_visibility = ["//visibility:private"],
@@ -18,13 +20,13 @@
 java_library(
     name = "byte-buddy",
     data = ["//lib:LICENSE-Apache2.0"],
-    exports = ["@byte-buddy//jar"],
+    exports = ["@bytebuddy//jar"],
 )
 
 java_library(
     name = "byte-buddy-agent",
     data = ["//lib:LICENSE-Apache2.0"],
-    exports = ["@byte-buddy-agent//jar"],
+    exports = ["@bytebuddy-agent//jar"],
 )
 
 java_library(
diff --git a/lib/openid/BUILD b/lib/openid/BUILD
index faa073b..c27e8ab 100644
--- a/lib/openid/BUILD
+++ b/lib/openid/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "consumer",
     data = ["//lib:LICENSE-Apache2.0"],
diff --git a/lib/ow2/BUILD b/lib/ow2/BUILD
index 5a82572..7fe7e2d 100644
--- a/lib/ow2/BUILD
+++ b/lib/ow2/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "ow2-asm",
     data = ["//lib:LICENSE-ow2"],
diff --git a/lib/powermock/BUILD b/lib/powermock/BUILD
index 57880f4..39df164 100644
--- a/lib/powermock/BUILD
+++ b/lib/powermock/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "powermock-module-junit4",
     data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"],
diff --git a/lib/prolog/BUILD b/lib/prolog/BUILD
index 8518af7..fa55682 100644
--- a/lib/prolog/BUILD
+++ b/lib/prolog/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_binary", "java_library")
+
 java_library(
     name = "runtime",
     data = ["//lib:LICENSE-prologcafe"],
@@ -42,14 +44,14 @@
 
 java_binary(
     name = "compiler-bin",
-    main_class = "BuckPrologCompiler",
+    main_class = "BazelPrologCompiler",
     visibility = ["//visibility:public"],
     runtime_deps = [":compiler-lib"],
 )
 
 java_library(
     name = "compiler-lib",
-    srcs = ["java/BuckPrologCompiler.java"],
+    srcs = ["java/BazelPrologCompiler.java"],
     visibility = ["//visibility:public"],
     deps = [
         ":compiler",
diff --git a/lib/prolog/java/BuckPrologCompiler.java b/lib/prolog/java/BazelPrologCompiler.java
similarity index 98%
rename from lib/prolog/java/BuckPrologCompiler.java
rename to lib/prolog/java/BazelPrologCompiler.java
index cc3e39e..37ea696 100644
--- a/lib/prolog/java/BuckPrologCompiler.java
+++ b/lib/prolog/java/BazelPrologCompiler.java
@@ -21,7 +21,7 @@
 import java.util.jar.JarEntry;
 import java.util.jar.JarOutputStream;
 
-public class BuckPrologCompiler {
+public class BazelPrologCompiler {
   private static File tmpdir;
 
   public static void main(String[] argv) throws IOException, CompileException {
diff --git a/lib/prolog/prolog.bzl b/lib/prolog/prolog.bzl
index 4d4dd3a..ffc3198 100644
--- a/lib/prolog/prolog.bzl
+++ b/lib/prolog/prolog.bzl
@@ -12,6 +12,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+load("@rules_java//java:defs.bzl", "java_library")
+
 def prolog_cafe_library(
         name,
         srcs,
@@ -26,7 +28,7 @@
         tools = ["//lib/prolog:compiler-bin"],
         outs = [name + ".srcjar"],
     )
-    native.java_library(
+    java_library(
         name = name,
         srcs = [":" + name + "__pl2j"],
         deps = ["//lib/prolog:runtime-neverlink"] + deps,
diff --git a/lib/testcontainers/BUILD b/lib/testcontainers/BUILD
index 25ca327..a37b733 100644
--- a/lib/testcontainers/BUILD
+++ b/lib/testcontainers/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "duct-tape",
     testonly = True,
diff --git a/lib/truth/BUILD b/lib/truth/BUILD
index db5bc48..bb30945 100644
--- a/lib/truth/BUILD
+++ b/lib/truth/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
 java_library(
     name = "truth",
     data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"],
diff --git a/plugins/BUILD b/plugins/BUILD
index 7d81213..3e93768 100644
--- a/plugins/BUILD
+++ b/plugins/BUILD
@@ -1,3 +1,4 @@
+load("@rules_java//java:defs.bzl", "java_binary", "java_library")
 load("//tools/bzl:genrule2.bzl", "genrule2")
 load("//tools/bzl:javadoc.bzl", "java_doc")
 load(
diff --git a/plugins/delete-project b/plugins/delete-project
index b618043..3a4b095 160000
--- a/plugins/delete-project
+++ b/plugins/delete-project
@@ -1 +1 @@
-Subproject commit b618043544ebc62a0730aa1bfc1a1e26011b471a
+Subproject commit 3a4b0955529588dbc071bc43164c468b29d79129
diff --git a/plugins/gitiles b/plugins/gitiles
index 3764262..bdbed9a 160000
--- a/plugins/gitiles
+++ b/plugins/gitiles
@@ -1 +1 @@
-Subproject commit 37642627f0a4e6a3b2990ec754120b5055de6d41
+Subproject commit bdbed9af9bb2b77cd7fc8681da2dcee7e8f30264
diff --git a/plugins/plugin-manager b/plugins/plugin-manager
index 833889d..b4aad55 160000
--- a/plugins/plugin-manager
+++ b/plugins/plugin-manager
@@ -1 +1 @@
-Subproject commit 833889d327a159b5ccea7064f4fcff3f94d4b26e
+Subproject commit b4aad554e56d9359fc0b17997f260169f685c7f2
diff --git a/plugins/replication b/plugins/replication
index 9b10716..f57df92 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 9b10716f0afaf58e29e591d22859120c4e35a702
+Subproject commit f57df9252c3caf2a5251b0577f3cc9bbd9cee7e9
diff --git a/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.html b/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.html
index db11937..182d242 100644
--- a/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.html
+++ b/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.html
@@ -18,8 +18,6 @@
 (function(window) {
   'use strict';
 
-  const ACCOUNT_CAPABILITIES = ['createProject', 'createGroup', 'viewPlugins'];
-
   const ADMIN_LINKS = [{
     name: 'Repositories',
     noBaseUrl: true,
@@ -65,7 +63,7 @@
         return Promise.resolve(this._filterLinks(link => link.viewableToAll,
             getAdminMenuLinks, opt_options));
       }
-      return getAccountCapabilities(ACCOUNT_CAPABILITIES)
+      return getAccountCapabilities()
           .then(capabilities => {
             return this._filterLinks(link => {
               return !link.capability ||
@@ -97,9 +95,10 @@
       links.push(...getAdminMenuLinks().map(link => ({
         url: link.url,
         name: link.text,
+        capability: link.capability || null,
         noBaseUrl: !isExernalLink(link),
         view: null,
-        viewableToAll: true,
+        viewableToAll: !link.capability,
         target: isExernalLink(link) ? '_blank' : null,
       })));
 
diff --git a/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior_test.html b/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior_test.html
index 60817aa..f1e28d1 100644
--- a/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior_test.html
@@ -310,5 +310,61 @@
         testAdminLinks(account, options, expected, done);
       });
     });
+
+
+    suite('view plugin screen with plugin capability', () => {
+      const account = {
+        name: 'test-user',
+      };
+      let expected;
+
+      setup(() => {
+        capabilityStub.returns(Promise.resolve({pluginCapability: true}));
+        expected = {};
+      });
+
+      test('with plugin with capabilities', done => {
+        let options;
+        const generatedLinks = [
+          {text: 'without capability', url: '/without'},
+          {text: 'with capability', url: '/with', capability: 'pluginCapability'},
+        ];
+        menuLinkStub.returns(generatedLinks);
+        expected = Object.assign(expected, {
+          totalLength: 4,
+          pluginGeneratedLinks: generatedLinks,
+        });
+        testAdminLinks(account, options, expected, done);
+      });
+    });
+
+
+    suite('view plugin screen without plugin capability', () => {
+      const account = {
+        name: 'test-user',
+      };
+      let expected;
+
+      setup(() => {
+        capabilityStub.returns(Promise.resolve({}));
+        expected = {};
+      });
+
+      test('with plugin with capabilities', done => {
+        let options;
+        const generatedLinks = [
+          {text: 'without capability', url: '/without'},
+          {text: 'with capability',
+            url: '/with',
+            capability: 'pluginCapability'},
+        ];
+        menuLinkStub.returns(generatedLinks);
+        expected = Object.assign(expected, {
+          totalLength: 3,
+          pluginGeneratedLinks: [generatedLinks[0]],
+        });
+        testAdminLinks(account, options, expected, done);
+      });
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html
index 178d056..984be19 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html
@@ -160,6 +160,7 @@
       return element.reload().then(() => {
         assert.equal(element._filteredLinks.length, 3);
         assert.deepEqual(element._filteredLinks[1], {
+          capability: null,
           url: '/internal/link/url',
           name: 'internal link text',
           noBaseUrl: true,
@@ -168,6 +169,7 @@
           target: null,
         });
         assert.deepEqual(element._filteredLinks[2], {
+          capability: null,
           url: 'http://external/link/url',
           name: 'external link text',
           noBaseUrl: false,
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 e72b788..455019f 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
@@ -245,9 +245,13 @@
       if (!changes || !changes.length) {
         return;
       }
-      if (USER_QUERY_PATTERN.test(this._query) && changes[0].owner.email) {
-        this._userId = changes[0].owner.email;
-        return;
+      if (USER_QUERY_PATTERN.test(this._query)) {
+        const owner = changes[0].owner;
+        const userId = owner._account_id ? owner._account_id : owner.email;
+        if (userId) {
+          this._userId = userId;
+          return;
+        }
       }
       if (REPO_QUERY_PATTERN.test(this._query)) {
         this._repo = changes[0].project;
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js
index dc945d8..93464b7 100644
--- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js
@@ -80,8 +80,11 @@
     },
 
     _computeDashboardUrl(accountDetails) {
-      if (!accountDetails || !accountDetails.email) { return null; }
-      return Gerrit.Nav.getUrlForUserDashboard(accountDetails.email);
+      if (!accountDetails) { return null; }
+      const id = accountDetails._account_id;
+      const email = accountDetails.email;
+      if (!id && !email ) { return null; }
+      return Gerrit.Nav.getUrlForUserDashboard(id ? id : email);
     },
 
     _computeDashboardLinkClass(showDashboardLink, loggedIn) {
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 60ccac1..08bb700 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
@@ -72,7 +72,6 @@
         position: relative;
       }
       .linksTitle {
-        color: var(--header-text-color);
         display: inline-block;
         font-weight: var(--font-weight-bold);
         position: relative;
@@ -127,9 +126,6 @@
       :host([logged-in]) gr-account-dropdown {
         display: inline;
       }
-      iron-icon {
-        color: var(--header-text-color);
-      }
       .accountContainer {
         align-items: center;
         display: flex;
@@ -139,7 +135,6 @@
         white-space: nowrap;
       }
       .loginButton, .registerButton {
-        color: var(--header-text-color);
         padding: .5em 1em;
       }
       .dropdown-trigger {
@@ -149,6 +144,18 @@
         background-color: var(--view-background-color);
         box-shadow: 0 1px 5px rgba(0, 0, 0, .3);
       }
+      /*
+       * We are not using :host to do this, because :host has a lowest css priority
+       * compared to others. This means that using :host to do this would break styles.
+       */
+      .linksTitle,
+      .bigTitle,
+      .loginButton,
+      .registerButton,
+      iron-icon,
+      gr-account-dropdown {
+        color: var(--header-text-color);
+      }
       #mobileSearch {
         display: none;
       }
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
index ca494d2..2bd4a62 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
@@ -109,6 +109,9 @@
 
   const pending = [];
 
+  const loadedPlugins = [];
+  const detectedExtensions = [];
+
   const onError = function(oldOnError, msg, url, line, column, error) {
     if (oldOnError) {
       oldOnError(msg, url, line, column, error);
@@ -116,7 +119,13 @@
     if (error) {
       line = line || error.lineNumber;
       column = column || error.columnNumber;
-      msg = msg || error.toString();
+      let shortenedErrorStack = msg;
+      if (error.stack) {
+        const errorStackLines = error.stack.split('\n');
+        shortenedErrorStack = errorStackLines.slice(0,
+            Math.min(3, errorStackLines.length)).join('\n');
+      }
+      msg = shortenedErrorStack || error.toString();
     }
     const payload = {
       url,
@@ -184,6 +193,7 @@
     reporter(...args) {
       const report = (this._isMetricsPluginLoaded() && !pending.length) ?
         this.defaultReporter : this.cachingReporter;
+      args.splice(4, 0, loadedPlugins, detectedExtensions);
       report.apply(this, args);
     },
 
@@ -193,16 +203,23 @@
      * @param {string} category
      * @param {string} eventName
      * @param {string|number} eventValue
+     * @param {Array} plugins
+     * @param {Array} extensions
      * @param {boolean|undefined} opt_noLog If true, the event will not be
      *     logged to the JS console.
      */
-    defaultReporter(type, category, eventName, eventValue, opt_noLog) {
+    defaultReporter(type, category, eventName, eventValue,
+        loadedPlugins, detectedExtensions, opt_noLog) {
       const detail = {
         type,
         category,
         name: eventName,
         value: eventValue,
       };
+      if (category === TIMING.CATEGORY_UI_LATENCY) {
+        detail.loadedPlugins = loadedPlugins;
+        detail.detectedExtensions = detectedExtensions;
+      }
       document.dispatchEvent(new CustomEvent(type, {detail}));
       if (opt_noLog) { return; }
       if (type === ERROR.TYPE && category === ERROR.CATEGORY) {
@@ -223,10 +240,13 @@
      * @param {string} category
      * @param {string} eventName
      * @param {string|number} eventValue
+     * @param {Array} plugins
+     * @param {Array} extensions
      * @param {boolean|undefined} opt_noLog If true, the event will not be
      *     logged to the JS console.
      */
-    cachingReporter(type, category, eventName, eventValue, opt_noLog) {
+    cachingReporter(type, category, eventName, eventValue,
+        plugins, extensions, opt_noLog) {
       if (type === ERROR.TYPE && category === ERROR.CATEGORY) {
         console.error(eventValue && eventValue.error || eventName);
       }
@@ -236,9 +256,11 @@
             this.reporter(...args);
           }
         }
-        this.reporter(type, category, eventName, eventValue, opt_noLog);
+        this.reporter(type, category, eventName, eventValue,
+            plugins, extensions, opt_noLog);
       } else {
-        pending.push([type, category, eventName, eventValue, opt_noLog]);
+        pending.push([type, category, eventName, eventValue,
+          plugins, extensions, opt_noLog]);
       }
     },
 
@@ -331,12 +353,16 @@
 
     reportExtension(name) {
       this.reporter(EXTENSION.TYPE, EXTENSION.DETECTED, name);
+      if (!detectedExtensions.includes(name)) {
+        detectedExtensions.push(name);
+      }
     },
 
     pluginLoaded(name) {
       if (name.startsWith('metrics-')) {
         this.timeEnd(TIMER.METRICS_PLUGIN_LOADED);
       }
+      loadedPlugins.push(name);
     },
 
     pluginsLoaded(pluginsList) {
@@ -481,7 +507,7 @@
 
     reportErrorDialog(message) {
       this.reporter(ERROR_DIALOG.TYPE, ERROR_DIALOG.CATEGORY,
-          'ErrorDialog: ' + message);
+          'ErrorDialog: ' + message, {error: new Error(message)});
     },
   });
 
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
index f505311..805cef0 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
@@ -265,7 +265,7 @@
       test('pluginsLoaded reports time', () => {
         sandbox.stub(element, 'now').returns(42);
         element.pluginsLoaded();
-        assert.isTrue(element.defaultReporter.calledWithExactly(
+        assert.isTrue(element.defaultReporter.calledWith(
             'timing-report', 'UI Latency', 'PluginsLoaded', 42
         ));
       });
@@ -287,6 +287,19 @@
         assert.isTrue(element.defaultReporter.called);
       });
 
+      test('reports plugins in timing events', () => {
+        element.pluginsLoaded = [];
+        sandbox.stub(element, 'now').returns(42);
+        element.pluginLoaded('metrics-xyz1');
+        // element.pluginLoaded('foo');
+        element.time('timeAction');
+        element.timeEnd('timeAction');
+        assert.isTrue(element.defaultReporter.getCall(1).calledWith(
+            'timing-report', 'UI Latency', 'timeAction', 0,
+            ['metrics-xyz1']
+        ));
+      });
+
       test('reports if metrics plugin xyz is loaded', () => {
         element.pluginLoaded('metrics-xyz');
         assert.isTrue(element.defaultReporter.called);
@@ -341,6 +354,7 @@
 
       test('is reported', () => {
         const error = new Error('bar');
+        error.stack = undefined;
         emulateThrow('bar', 'http://url', 4, 2, error);
         assert.isTrue(reporter.calledWith('error', 'exception', 'bar'));
         const payload = reporter.lastCall.args[3];
@@ -352,6 +366,15 @@
         });
       });
 
+      test('is reported with 3 lines of stack', () => {
+        const error = new Error('bar');
+        emulateThrow('bar', 'http://url', 4, 2, error);
+        const expectedStack = error.stack.split('\n').slice(0, 3)
+            .join('\n');
+        assert.isTrue(reporter.calledWith('error', 'exception',
+            expectedStack));
+      });
+
       test('prevent default event handler', () => {
         assert.isTrue(emulateThrow());
       });
diff --git a/polygerrit-ui/app/elements/gr-app-it_test.html b/polygerrit-ui/app/elements/custom-dark-theme_test.html
similarity index 79%
copy from polygerrit-ui/app/elements/gr-app-it_test.html
copy to polygerrit-ui/app/elements/custom-dark-theme_test.html
index d62c082..4cf35f1 100644
--- a/polygerrit-ui/app/elements/gr-app-it_test.html
+++ b/polygerrit-ui/app/elements/custom-dark-theme_test.html
@@ -24,7 +24,7 @@
 <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
 <script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../test/common-test-setup.html"/>
-<link rel="import" href="gr-app.html">
+<link rel="import" href="./gr-app.html">
 
 <script>void(0);</script>
 
@@ -35,7 +35,7 @@
 </test-fixture>
 
 <script>
-  suite('gr-app integration tests', () => {
+  suite('gr-app custom dark theme tests', () => {
     let sandbox;
     let element;
 
@@ -63,13 +63,22 @@
         getVersion() { return Promise.resolve(42); },
         getLoggedIn() { return Promise.resolve(false); },
       });
+
+      window.localStorage.setItem('dark-theme', 'true');
+
       element = fixture('element');
 
-      const importSpy = sandbox.spy(element.$.externalStyle, '_import');
+      const importSpy = sandbox.spy(
+          element.$['app-element'].$.externalStyleForAll,
+          '_import');
+      const importForThemeSpy = sandbox.spy(
+          element.$['app-element'].$.externalStyleForTheme,
+          '_import');
       Gerrit.awaitPluginsLoaded().then(() => {
-        Promise.all(importSpy.returnValues).then(() => {
-          flush(done);
-        });
+        Promise.all(importSpy.returnValues.concat(importForThemeSpy.returnValues))
+            .then(() => {
+              flush(done);
+            });
       });
     });
 
@@ -77,21 +86,16 @@
       sandbox.restore();
     });
 
-    test('applies --primary-text-color', () => {
+    test('applies the right theme', () => {
       assert.equal(
           util.getComputedStyleValue('--primary-text-color', element),
-          '#F00BAA');
-    });
-
-    test('applies --header-background-color', () => {
+          'red');
       assert.equal(
           util.getComputedStyleValue('--header-background-color', element),
-          '#F01BAA');
-    });
-    test('applies --footer-background-color', () => {
+          'black');
       assert.equal(
           util.getComputedStyleValue('--footer-background-color', element),
-          '#F02BAA');
+          'yellow');
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/gr-app-it_test.html b/polygerrit-ui/app/elements/custom-light-theme_test.html
similarity index 82%
rename from polygerrit-ui/app/elements/gr-app-it_test.html
rename to polygerrit-ui/app/elements/custom-light-theme_test.html
index d62c082..e346af5 100644
--- a/polygerrit-ui/app/elements/gr-app-it_test.html
+++ b/polygerrit-ui/app/elements/custom-light-theme_test.html
@@ -35,7 +35,7 @@
 </test-fixture>
 
 <script>
-  suite('gr-app integration tests', () => {
+  suite('gr-app custom light theme tests', () => {
     let sandbox;
     let element;
 
@@ -63,13 +63,22 @@
         getVersion() { return Promise.resolve(42); },
         getLoggedIn() { return Promise.resolve(false); },
       });
+
+      window.localStorage.removeItem('dark-theme');
+
       element = fixture('element');
 
-      const importSpy = sandbox.spy(element.$.externalStyle, '_import');
+      const importSpy = sandbox.spy(
+          element.$['app-element'].$.externalStyleForAll,
+          '_import');
+      const importForThemeSpy = sandbox.spy(
+          element.$['app-element'].$.externalStyleForTheme,
+          '_import');
       Gerrit.awaitPluginsLoaded().then(() => {
-        Promise.all(importSpy.returnValues).then(() => {
-          flush(done);
-        });
+        Promise.all(importSpy.returnValues.concat(importForThemeSpy.returnValues))
+            .then(() => {
+              flush(done);
+            });
       });
     });
 
@@ -77,18 +86,13 @@
       sandbox.restore();
     });
 
-    test('applies --primary-text-color', () => {
+    test('applies the right theme', () => {
       assert.equal(
           util.getComputedStyleValue('--primary-text-color', element),
           '#F00BAA');
-    });
-
-    test('applies --header-background-color', () => {
       assert.equal(
           util.getComputedStyleValue('--header-background-color', element),
           '#F01BAA');
-    });
-    test('applies --footer-background-color', () => {
       assert.equal(
           util.getComputedStyleValue('--footer-background-color', element),
           '#F02BAA');
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-range-normalizer.js b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-range-normalizer.js
index eb4123d..556395c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-range-normalizer.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-range-normalizer.js
@@ -54,7 +54,7 @@
       if (element.nodeName === '#text') {
         element = element.parentElement;
       }
-      while (!element.classList.contains('contentText')) {
+      while (element && !element.classList.contains('contentText')) {
         if (element.parentElement === null) {
           return target;
         }
@@ -80,7 +80,7 @@
         if (n === child) {
           break;
         }
-        if (n.childNodes && n.childNodes.length !== 0) {
+        if (n && n.childNodes && n.childNodes.length !== 0) {
           const arr = [];
           for (const childNode of n.childNodes) {
             arr.push(childNode);
@@ -101,7 +101,9 @@
      * @return {number} The length of the text.
      */
     _getLength(node) {
-      return node.textContent.replace(REGEX_ASTRAL_SYMBOL, '_').length;
+      return node
+        ? node.textContent.replace(REGEX_ASTRAL_SYMBOL, '_').length
+        : 0;
     },
   };
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js
index 9a9a6d7..f6f48b3 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js
@@ -34,13 +34,13 @@
 
     getFocusStops() {
       return {
-        start: this.$.contextSelect,
+        start: this.$.diffPreferences.$.contextSelect,
         end: this.$.saveButton,
       };
     },
 
     resetFocus() {
-      this.$.contextSelect.focus();
+      this.$.diffPreferences.$.contextSelect.focus();
     },
 
     _computeHeaderClass(changed) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.html b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.html
index 83b6f55..6469382 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.html
@@ -20,6 +20,31 @@
 
 <dom-module id="gr-diff-selection">
   <template>
+    <style include="shared-styles">
+      /** Select and copy for Polymer 1*/
+      .contentWrapper ::content .content,
+      .contentWrapper ::content .contextControl,
+      .contentWrapper ::content .blame {
+        -webkit-user-select: none;
+        -moz-user-select: none;
+        -ms-user-select: none;
+        user-select: none;
+      }
+
+      :host-context(.selected-left:not(.selected-comment)) .contentWrapper ::content .side-by-side .left + .content .contentText,
+      :host-context(.selected-right:not(.selected-comment)) .contentWrapper ::content .side-by-side .right + .content .contentText,
+      :host-context(.selected-left:not(.selected-comment)) .contentWrapper ::content .unified .left.lineNum ~ .content:not(.both) .contentText,
+      :host-context(.selected-right:not(.selected-comment)) .contentWrapper ::content .unified .right.lineNum ~ .content .contentText,
+      :host-context(.selected-left.selected-comment) .contentWrapper ::content .side-by-side .left + .content .message,
+      :host-context(.selected-right.selected-comment) .contentWrapper ::content .side-by-side .right + .content .message :not(.collapsedContent),
+      :host-context(.selected-comment) .contentWrapper ::content .unified .message :not(.collapsedContent),
+      :host-context(.selected-blame) .contentWrapper ::content .blame {
+        -webkit-user-select: text;
+        -moz-user-select: text;
+        -ms-user-select: text;
+        user-select: text;
+      }
+    </style>
     <div class="contentWrapper">
       <slot></slot>
     </div>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
index 072a0d7..ff7593b 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
@@ -73,7 +73,28 @@
       this._linesCache = getNewCache();
     },
 
+    _handleDownOnRangeComment(node) {
+      // Keep the original behavior in polymer 1
+      if (!window.POLYMER2) return false;
+      if (node &&
+          node.nodeName &&
+          node.nodeName.toLowerCase() === 'gr-comment-thread') {
+        this._setClasses([
+          SelectionClass.COMMENT,
+          node.commentSide === 'left' ?
+          SelectionClass.LEFT :
+          SelectionClass.RIGHT,
+        ]);
+        return true;
+      }
+      return false;
+    },
+
     _handleDown(e) {
+      // Handle the down event on comment thread in Polymer 2
+      const handled = this._handleDownOnRangeComment(e.target);
+      if (handled) return;
+
       const lineEl = this.diffBuilder.getLineElByChild(e.target);
       const blameSelected = this._elementDescendedFromClass(e.target, 'blame');
       if (!lineEl && !blameSelected) { return; }
@@ -140,6 +161,10 @@
     },
 
     _handleCopy(e) {
+      // Let the browser handle the copy event for polymer 2
+      // as selection across shadow DOM will be hard to process
+      if (window.POLYMER2) return;
+
       let commentSelected = false;
       const target = this._getCopyEventTarget(e);
       if (target.type === 'textarea') { return; }
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 cbb653b..bc8af9d 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -42,6 +42,7 @@
         max-width: var(--content-width, 80ch);
         white-space: normal;
       }
+
       .thread-group {
         display: block;
         max-width: var(--content-width, 80ch);
@@ -307,7 +308,8 @@
         background: linear-gradient(to right bottom, #FFD1A4 0%, #FFD1A4 50%, #E0F2F1 50%, #E0F2F1 100%);
       }
 
-      /** Select to copy */
+      /** BEGIN: Select and copy for Polymer 2 */
+      /** Below was copied and modified from the original css in gr-diff-selection.html */
       .content,
       .contextControl,
       .blame {
@@ -331,6 +333,16 @@
         user-select: text;
       }
 
+      /** Make comments selectable */
+      .selected-left ::slotted(gr-comment-thread[comment-side=left]),
+      .selected-right ::slotted(gr-comment-thread[comment-side=right]) {
+        -webkit-user-select: text;
+        -moz-user-select: text;
+        -ms-user-select: text;
+        user-select: text;
+      }
+      /** END: Select and copy for Polymer 2 */
+
       .whitespace-change-only-message {
         background-color: var(--diff-context-control-background-color);
         border: 1px solid var(--diff-context-control-border-color);
diff --git a/polygerrit-ui/app/elements/gr-app-element.html b/polygerrit-ui/app/elements/gr-app-element.html
index 46c4502..a42622e 100644
--- a/polygerrit-ui/app/elements/gr-app-element.html
+++ b/polygerrit-ui/app/elements/gr-app-element.html
@@ -225,7 +225,8 @@
         config="[[_serverConfig]]">
     </gr-plugin-host>
     <gr-lib-loader id="libLoader"></gr-lib-loader>
-    <gr-external-style id="externalStyle" name="app-theme"></gr-external-style>
+    <gr-external-style id="externalStyleForAll" name="app-theme"></gr-external-style>
+    <gr-external-style id="externalStyleForTheme" name="[[getThemeEndpoint()]]"></gr-external-style>
   </template>
   <script src="gr-app-element.js" crossorigin="anonymous"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/gr-app-element.js b/polygerrit-ui/app/elements/gr-app-element.js
index 01da895..3c146f8 100644
--- a/polygerrit-ui/app/elements/gr-app-element.js
+++ b/polygerrit-ui/app/elements/gr-app-element.js
@@ -460,5 +460,12 @@
 
       return false;
     },
+
+    getThemeEndpoint() {
+      // For now, we only have dark mode and light mode
+      return window.localStorage.getItem('dark-theme') ?
+        'app-theme-dark' :
+        'app-theme-light';
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api.js b/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api.js
index 3959186..d1f8e56 100644
--- a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api.js
+++ b/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api.js
@@ -30,8 +30,8 @@
    * @param {string} text
    * @param {string} url
    */
-  GrAdminApi.prototype.addMenuLink = function(text, url) {
-    this._menuLinks.push({text, url});
+  GrAdminApi.prototype.addMenuLink = function(text, url, opt_capability) {
+    this._menuLinks.push({text, url, capability: opt_capability || null});
   };
 
   GrAdminApi.prototype.getMenuLinks = function() {
diff --git a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.html b/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.html
index 6883e7e..9cdcf76 100644
--- a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.html
@@ -56,7 +56,15 @@
       adminApi.addMenuLink('text', 'url');
       const links = adminApi.getMenuLinks();
       assert.equal(links.length, 1);
-      assert.deepEqual(links[0], {text: 'text', url: 'url'});
+      assert.deepEqual(links[0], {text: 'text', url: 'url', capability: null});
+    });
+
+    test('addMenuLinkWithCapability', () => {
+      adminApi.addMenuLink('text', 'url', 'capability');
+      const links = adminApi.getMenuLinks();
+      assert.equal(links.length, 1);
+      assert.deepEqual(links[0],
+          {text: 'text', url: 'url', capability: 'capability'});
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.html b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.html
index f747719..498dfb8 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.html
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.html
@@ -232,6 +232,16 @@
       #deleteBtn.showDeleteButtons {
         display: block;
       }
+
+      /** Disable select for the caret and actions */
+      .actions,
+      .show-hide {
+        -webkit-user-select: none;
+        -moz-user-select: none;
+        -ms-user-select: none;
+        user-select: none;
+      }
+
     </style>
     <div id="container" class="container">
       <div class="header" id="header" on-tap="_handleToggleCollapsed">
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html
index 55e09ea..1c77e07 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html
@@ -135,6 +135,7 @@
       }
     </style>
     <gr-button
+        disabled="[[disabled]]"
         down-arrow
         link
         id="trigger"
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js
index b6d909d..d87afed 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js
@@ -62,6 +62,10 @@
       /** @type {!Array<!Defs.item>} */
       items: Object,
       text: String,
+      disabled: {
+        type: Boolean,
+        value: false,
+      },
       value: {
         type: String,
         notify: true,
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.js
index e885bd4..fb8a66c 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.js
@@ -51,7 +51,11 @@
   };
 
   GrPluginRestApi.prototype.fetchJSON = function(req) {
-    return getRestApi()._fetchJSON(req);
+    // TODO(dhruvsri): find better implementation for fetchJSON
+    const api = getRestApi();
+    const fetchJSON = api._fetchJSON ||
+                      api._restApiHelper.fetchJSON.bind(api._restApiHelper);
+    return fetchJSON(req);
   };
 
   GrPluginRestApi.prototype.getRepos =
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
index 3483d0e..968db93 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
@@ -69,6 +69,10 @@
           match: 'hash:(.+)',
           html: '<a href="#/awesomesauce">$1</a>',
         },
+        baseurl: {
+          match: 'test (.+)',
+          html: '<a href="/r/awesomesauce">$1</a>',
+        },
         disabledconfig: {
           match: 'foo:(.+)',
           link: 'https://google.com/search?q=$1',
@@ -206,6 +210,15 @@
     test('html with base url', () => {
       window.CANONICAL_PATH = '/r';
 
+      element.content = 'test foo';
+      const linkEl = element.$.output.childNodes[0];
+      assert.isTrue(linkEl.href.endsWith('/r/awesomesauce'));
+      assert.equal(linkEl.textContent, 'foo');
+    });
+
+    test('hash html with base url', () => {
+      window.CANONICAL_PATH = '/r';
+
       element.content = 'hash:foo';
       const linkEl = element.$.output.childNodes[0];
       assert.isTrue(linkEl.href.endsWith('/r/awesomesauce'));
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js b/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js
index c5ac00e..897512b 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js
@@ -142,7 +142,7 @@
           htmlOutput = a;
         } else if (html) {
           const fragment = document.createDocumentFragment();
-      // Create temporary div to hold the nodes in.
+          // Create temporary div to hold the nodes in.
           const div = document.createElement('div');
           div.innerHTML = html;
           while (div.firstChild) {
@@ -174,7 +174,7 @@
       function(text, href, position, length, outputArray) {
         if (!text || this.hasOverlap(position, length, outputArray)) { return; }
         if (!!this.baseUrl && href.startsWith('/') &&
-              !href.startsWith(this.baseUrl)) {
+             !href.startsWith(this.baseUrl)) {
           href = this.baseUrl + href;
         }
         this.addItem(text, href, null, position, length, outputArray);
diff --git a/polygerrit-ui/app/elements/shared/gr-select/gr-select.js b/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
index 357ab40..ecf542f 100644
--- a/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
+++ b/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
@@ -60,6 +60,10 @@
       this.bindValue = this.nativeSelect.value;
     },
 
+    focus() {
+      this.nativeSelect.focus();
+    },
+
     ready() {
       // If not set via the property, set bind-value to the element value.
       if (this.bindValue == undefined) {
diff --git a/polygerrit-ui/app/elements/test/plugin.html b/polygerrit-ui/app/elements/test/plugin.html
index a0d7467..ecd9007 100644
--- a/polygerrit-ui/app/elements/test/plugin.html
+++ b/polygerrit-ui/app/elements/test/plugin.html
@@ -1,8 +1,10 @@
 <dom-module id="my-plugin">
   <script>
-    Gerrit.install(plugin =>
-      plugin.registerStyleModule('app-theme', 'myplugin-app-theme')
-    );
+    Gerrit.install(plugin => {
+      plugin.registerStyleModule('app-theme', 'myplugin-app-theme');
+      plugin.registerStyleModule('app-theme-light', 'myplugin-app-theme-light');
+      plugin.registerStyleModule('app-theme-dark', 'myplugin-app-theme-dark');
+    });
   </script>
 </dom-module>
 
@@ -11,6 +13,15 @@
     <style>
       html {
         --primary-text-color: #F00BAA;
+      }
+    </style>
+  </template>
+</dom-module>
+
+<dom-module id="myplugin-app-theme-light">
+  <template>
+    <style>
+      html {
         --header-background-color: #F01BAA;
         --header-title-content: "MyGerrit";
         --footer-background-color: #F02BAA;
@@ -18,3 +29,16 @@
     </style>
   </template>
 </dom-module>
+
+<dom-module id="myplugin-app-theme-dark">
+  <template>
+    <style>
+      html {
+        --primary-text-color: red;
+        --header-background-color: black;
+        --header-title-content: "MyGerrit Dark";
+        --footer-background-color: yellow;
+      }
+    </style>
+  </template>
+</dom-module>
diff --git a/polygerrit-ui/app/run_test.sh b/polygerrit-ui/app/run_test.sh
index 3d92e11..e9be18d 100755
--- a/polygerrit-ui/app/run_test.sh
+++ b/polygerrit-ui/app/run_test.sh
@@ -32,9 +32,15 @@
     exit 1
 fi
 
+bazel_bin=$(which bazelisk 2>/dev/null)
+if [[ -z "$bazel_bin" ]]; then
+    echo "Warning: bazelisk is not installed; falling back to bazel."
+    bazel_bin=bazel
+fi
+
 # WCT tests are not hermetic, and need extra environment variables.
 # TODO(hanwen): does $DISPLAY even work on OSX?
-bazel test \
+${bazel_bin} test \
       --test_env="HOME=$HOME" \
       --test_env="WCT=${wct_bin}" \
       --test_env="WCT_ARGS=${WCT_ARGS}" \
diff --git a/polygerrit-ui/app/styles/themes/dark-theme.html b/polygerrit-ui/app/styles/themes/dark-theme.html
index f513f3c..8cf1b13 100644
--- a/polygerrit-ui/app/styles/themes/dark-theme.html
+++ b/polygerrit-ui/app/styles/themes/dark-theme.html
@@ -1,3 +1,19 @@
+<!--
+@license
+Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
 <dom-module id="dark-theme">
   <custom-style><style is="custom-style">
     html {
diff --git a/proto/BUILD b/proto/BUILD
index cef28a1..57be265 100644
--- a/proto/BUILD
+++ b/proto/BUILD
@@ -1,3 +1,6 @@
+load("@rules_java//java:defs.bzl", "java_proto_library")
+load("@rules_proto//proto:defs.bzl", "proto_library")
+
 proto_library(
     name = "cache_proto",
     srcs = ["cache.proto"],
diff --git a/proto/testing/BUILD b/proto/testing/BUILD
index b9032cf..f32d745 100644
--- a/proto/testing/BUILD
+++ b/proto/testing/BUILD
@@ -1,3 +1,6 @@
+load("@rules_java//java:defs.bzl", "java_proto_library")
+load("@rules_proto//proto:defs.bzl", "proto_library")
+
 proto_library(
     name = "test_proto",
     testonly = 1,
diff --git a/resources/BUILD b/resources/BUILD
index 18d8df6..b53ae4c 100644
--- a/resources/BUILD
+++ b/resources/BUILD
@@ -1,3 +1,4 @@
+load("@rules_java//java:defs.bzl", "java_import")
 load("//tools/bzl:genrule2.bzl", "genrule2")
 
 java_import(
diff --git a/tools/BUILD b/tools/BUILD
index 3d0959b2..29626d9 100644
--- a/tools/BUILD
+++ b/tools/BUILD
@@ -3,6 +3,8 @@
     "JDK9_JVM_OPTS",
     "default_java_toolchain",
 )
+load("@rules_java//java:defs.bzl", "java_package_configuration")
+load("@rules_python//python:defs.bzl", "py_binary")
 
 exports_files(["nongoogle.bzl"])
 
diff --git a/tools/bzl/bazelisk_version.bzl b/tools/bzl/bazelisk_version.bzl
new file mode 100644
index 0000000..d8b3d10
--- /dev/null
+++ b/tools/bzl/bazelisk_version.bzl
@@ -0,0 +1,16 @@
+_template = """
+load("@bazel_skylib//lib:versions.bzl", "versions")
+
+def check_bazel_version():
+  versions.check(minimum_bazel_version = "{version}")
+""".strip()
+
+def _impl(repository_ctx):
+    repository_ctx.symlink(Label("@//:.bazelversion"), ".bazelversion")
+    bazelversion = repository_ctx.read(".bazelversion").strip()
+
+    repository_ctx.file("BUILD", executable = False)
+
+    repository_ctx.file("check.bzl", executable = False, content = _template.format(version = bazelversion))
+
+bazelisk_version = repository_rule(implementation = _impl)
diff --git a/tools/bzl/classpath.bzl b/tools/bzl/classpath.bzl
index 0d43be7..3be7a12 100644
--- a/tools/bzl/classpath.bzl
+++ b/tools/bzl/classpath.bzl
@@ -1,10 +1,10 @@
 def _classpath_collector(ctx):
     all = []
     for d in ctx.attr.deps:
-        if hasattr(d, "java"):
-            all.append(d.java.transitive_runtime_deps)
-            if hasattr(d.java.compilation_info, "runtime_classpath"):
-                all.append(d.java.compilation_info.runtime_classpath)
+        if JavaInfo in d:
+            all.append(d[JavaInfo].transitive_runtime_deps)
+            if hasattr(d[JavaInfo].compilation_info, "runtime_classpath"):
+                all.append(d[JavaInfo].compilation_info.runtime_classpath)
         elif hasattr(d, "files"):
             all.append(d.files)
 
diff --git a/tools/bzl/java.bzl b/tools/bzl/java.bzl
index 7c41fbe..8996b69 100644
--- a/tools/bzl/java.bzl
+++ b/tools/bzl/java.bzl
@@ -15,11 +15,13 @@
 # Syntactic sugar for native java_library() rule:
 #   accept exported_deps attributes
 
+load("@rules_java//java:defs.bzl", "java_library")
+
 def java_library2(deps = [], exported_deps = [], exports = [], **kwargs):
     if exported_deps:
         deps = deps + exported_deps
         exports = exports + exported_deps
-    native.java_library(
+    java_library(
         deps = deps,
         exports = exports,
         **kwargs
diff --git a/tools/bzl/javadoc.bzl b/tools/bzl/javadoc.bzl
index 754bd96..77c2d4a 100644
--- a/tools/bzl/javadoc.bzl
+++ b/tools/bzl/javadoc.bzl
@@ -17,8 +17,11 @@
 def _impl(ctx):
     zip_output = ctx.outputs.zip
 
-    transitive_jars = depset(transitive = [j.java.transitive_deps for j in ctx.attr.libs])
-    source_jars = depset(transitive = [j.java.source_jars for j in ctx.attr.libs])
+    transitive_jars = depset(transitive = [j[JavaInfo].transitive_deps for j in ctx.attr.libs])
+
+    # TODO(davido): Remove list to depset conversion on source_jars, when this issue is fixed:
+    # https://github.com/bazelbuild/bazel/issues/4221
+    source_jars = depset(transitive = [depset(j[JavaInfo].source_jars) for j in ctx.attr.libs])
 
     transitive_jar_paths = [j.path for j in transitive_jars.to_list()]
     dir = ctx.outputs.zip.path + ".dir"
diff --git a/tools/bzl/junit.bzl b/tools/bzl/junit.bzl
index 1a30997..1cf82ea 100644
--- a/tools/bzl/junit.bzl
+++ b/tools/bzl/junit.bzl
@@ -18,6 +18,8 @@
 
 # See https://github.com/bazelbuild/bazel/issues/1017 for background.
 
+load("@rules_java//java:defs.bzl", "java_test")
+
 _OUTPUT = """import org.junit.runners.Suite;
 import org.junit.runner.RunWith;
 
@@ -55,7 +57,7 @@
         ctx.attr.outname,
     ))
 
-_GenSuite = rule(
+_gen_suite = rule(
     attrs = {
         "srcs": attr.label_list(allow_files = True),
         "outname": attr.string(),
@@ -73,7 +75,7 @@
 
 def junit_tests(name, srcs, **kwargs):
     s_name = name.replace("-", "_") + "TestSuite"
-    _GenSuite(
+    _gen_suite(
         name = s_name,
         srcs = srcs,
         outname = s_name,
@@ -84,7 +86,7 @@
         "//:java_next": POST_JDK8_OPTS,
         "//conditions:default": [],
     })
-    native.java_test(
+    java_test(
         name = name,
         test_class = s_name,
         srcs = srcs + [":" + s_name],
diff --git a/tools/bzl/maven.bzl b/tools/bzl/maven.bzl
index 71aa91c..36e3084e 100644
--- a/tools/bzl/maven.bzl
+++ b/tools/bzl/maven.bzl
@@ -14,6 +14,8 @@
 
 # Merge maven files
 
+load("@rules_java//java:defs.bzl", "java_import")
+
 def cmd(jars):
     return ("$(location //tools:merge_jars) $@ " +
             " ".join(["$(location %s)" % j for j in jars]))
@@ -25,7 +27,7 @@
         tools = srcs + ["//tools:merge_jars"],
         outs = ["%s__merged.jar" % name],
     )
-    native.java_import(
+    java_import(
         name = name,
         jars = [":%s__merged_bin" % name],
         **kwargs
diff --git a/tools/bzl/pkg_war.bzl b/tools/bzl/pkg_war.bzl
index 90dc607..ab753bd 100644
--- a/tools/bzl/pkg_war.bzl
+++ b/tools/bzl/pkg_war.bzl
@@ -102,8 +102,8 @@
     transitive_context_libs = []
     if ctx.attr.context:
         for jar in ctx.attr.context:
-            if hasattr(jar, "java"):
-                transitive_context_libs.append(jar.java.transitive_runtime_deps)
+            if JavaInfo in jar:
+                transitive_context_libs.append(jar[JavaInfo].transitive_runtime_deps)
             elif hasattr(jar, "files"):
                 transitive_context_libs.append(jar.files)
 
diff --git a/tools/bzl/plugin.bzl b/tools/bzl/plugin.bzl
index 066fe43..ed64d1b 100644
--- a/tools/bzl/plugin.bzl
+++ b/tools/bzl/plugin.bzl
@@ -1,3 +1,4 @@
+load("@rules_java//java:defs.bzl", "java_binary", "java_library")
 load("//tools/bzl:genrule2.bzl", "genrule2")
 
 PLUGIN_DEPS = ["//plugins:plugin-lib"]
@@ -21,7 +22,7 @@
         dir_name = None,
         target_suffix = "",
         **kwargs):
-    native.java_library(
+    java_library(
         name = name + "__plugin",
         srcs = srcs,
         resources = resources,
@@ -35,7 +36,7 @@
     if not dir_name:
         dir_name = name
 
-    native.java_binary(
+    java_binary(
         name = "%s__non_stamped" % name,
         deploy_manifest_lines = manifest_entries + ["Gerrit-ApiType: plugin"],
         main_class = "Dummy",
diff --git a/tools/eclipse/BUILD b/tools/eclipse/BUILD
index 814a56f..e091fc1 100644
--- a/tools/eclipse/BUILD
+++ b/tools/eclipse/BUILD
@@ -1,3 +1,4 @@
+load("@rules_java//java:defs.bzl", "java_library")
 load("//tools/bzl:classpath.bzl", "classpath_collector")
 load("//tools/bzl:pkg_war.bzl", "LIBS", "PGMLIBS")
 load(